diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..56c0a8b79b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,75 @@ +ARG GO_VERSION=1.17.5 +FROM golang:$GO_VERSION-bullseye as builder + +ARG CHANNEL=nightly +ARG URL= +ARG BRANCH= +ARG SHA= + +# Basic dependencies. +ENV HOME /node +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get update && \ + apt-get install -y \ + apt-utils \ + bsdmainutils \ + curl \ + git \ + git-core \ + python3 + +COPY ./docker/files/ /node/files +COPY ./installer/genesis /node/files/run/genesis +COPY ./cmd/updater/update.sh /node/files/build/update.sh +COPY ./installer/config.json.example /node/files/build/config.json + +RUN find /node/files + +# Install algod binaries. +RUN /node/files/build/install.sh \ + -p "/node/bin" \ + -d "/node/data" \ + -c "${CHANNEL}" \ + -u "${URL}" \ + -b "${BRANCH}" \ + -s "${SHA}" + +# Copy binaries into a clean image +# TODO: We don't need most of the binaries. +# Should we delete everything except goal/algod/algocfg/tealdbg? +FROM debian:bullseye-slim as final +COPY --from=builder "/node/bin/" "/node/bin" +COPY --from=builder "/node/data/" "/node/dataTemplate" +COPY --from=builder "/node/files/run" "/node/run" + +ENV BIN_DIR="/node/bin" +ENV PATH="$BIN_DIR:${PATH}" +ENV ALGOD_PORT=8080 +ENV ALGORAND_DATA="/algod/data" +RUN mkdir -p "$ALGORAND_DATA" +WORKDIR /node/data + +# curl is needed to lookup the fast catchup url +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# TODO: This works fine, but causes problems when mounting a volume +# Use algorand user instead of root +#RUN groupadd -r algorand && \ +# useradd --no-log-init -r -g algorand algorand && \ +# chown -R algorand.algorand /node && \ +# chown -R algorand.algorand /algod +#USER algorand + +# Algod REST API +EXPOSE $ALGOD_PORT + +# Algod Gossip Port +EXPOSE 4160 + +# Prometheus Metrics +EXPOSE 9100 + +CMD ["/node/run/run.sh"] +#CMD ["/bin/bash"] diff --git a/Makefile b/Makefile index a7613fbf42..52c955b38f 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,7 @@ GOLDFLAGS := $(GOLDFLAGS_BASE) \ UNIT_TEST_SOURCES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && go list ./... | grep -v /go-algorand/test/ )) ALGOD_API_PACKAGES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && cd daemon/algod/api; go list ./... )) -MSGP_GENERATE := ./protocol ./protocol/test ./crypto ./crypto/merklearray ./crypto/merklesignature ./crypto/stateproof ./data/basics ./data/transactions ./data/stateproofmsg ./data/committee ./data/bookkeeping ./data/hashable ./agreement ./rpcs ./node ./ledger ./ledger/ledgercore ./stateproof ./data/account ./daemon/algod/api/spec/v2 +MSGP_GENERATE := ./protocol ./protocol/test ./crypto ./crypto/merklearray ./crypto/merklesignature ./crypto/stateproof ./data/basics ./data/transactions ./data/stateproofmsg ./data/committee ./data/bookkeeping ./data/hashable ./agreement ./rpcs ./node ./ledger ./ledger/ledgercore ./ledger/store ./stateproof ./data/account ./daemon/algod/api/spec/v2 default: build @@ -99,7 +99,7 @@ fix: build $(GOPATH1)/bin/algofix */ lint: deps - $(GOPATH1)/bin/golangci-lint run -c .golangci.yml + $(GOPATH1)/bin/golangci-lint run -c .golangci.yml check_shell: find . -type f -name "*.sh" -exec shellcheck {} + @@ -147,21 +147,6 @@ deps: # artifacts -# Regenerate algod swagger spec files -ALGOD_API_SWAGGER_SPEC := daemon/algod/api/swagger.json -ALGOD_API_FILES := $(shell find daemon/algod/api/server/common daemon/algod/api/server/v1 daemon/algod/api/spec/v1 -type f) \ - daemon/algod/api/server/router.go -ALGOD_API_SWAGGER_INJECT := daemon/algod/api/server/lib/bundledSpecInject.go - -# Note that swagger.json requires the go-swagger dep. -$(ALGOD_API_SWAGGER_SPEC): $(ALGOD_API_FILES) crypto/libs/$(OS_TYPE)/$(ARCH)/lib/libsodium.a - cd daemon/algod/api && \ - PATH=$(GOPATH1)/bin:$$PATH \ - go generate ./... - -$(ALGOD_API_SWAGGER_INJECT): deps $(ALGOD_API_SWAGGER_SPEC) $(ALGOD_API_SWAGGER_SPEC).validated - ./daemon/algod/api/server/lib/bundle_swagger_json.sh - # Regenerate kmd swagger spec files KMD_API_SWAGGER_SPEC := daemon/kmd/api/swagger.json KMD_API_FILES := $(shell find daemon/kmd/api/ -type f | grep -v $(KMD_API_SWAGGER_SPEC)) @@ -191,15 +176,13 @@ $(KMD_API_SWAGGER_INJECT): deps $(KMD_API_SWAGGER_SPEC) $(KMD_API_SWAGGER_SPEC). # generated files we should make sure we clean GENERATED_FILES := \ - $(ALGOD_API_SWAGGER_INJECT) \ $(KMD_API_SWAGGER_INJECT) \ - $(ALGOD_API_SWAGGER_SPEC) $(ALGOD_API_SWAGGER_SPEC).validated \ $(KMD_API_SWAGGER_SPEC) $(KMD_API_SWAGGER_SPEC).validated -rebuild_swagger: deps +rebuild_kmd_swagger: deps rm -f $(GENERATED_FILES) # we need to invoke the make here since we want to ensure that the deletion and re-creating are sequential - make $(KMD_API_SWAGGER_INJECT) $(ALGOD_API_SWAGGER_INJECT) + make $(KMD_API_SWAGGER_INJECT) # develop @@ -327,7 +310,7 @@ dump: $(addprefix gen/,$(addsuffix /genesis.dump, $(NETWORKS))) install: build scripts/dev_install.sh -p $(GOPATH1)/bin -.PHONY: default fmt lint check_shell sanity cover prof deps build test fulltest shorttest clean cleango deploy node_exporter install %gen gen NONGO_BIN check-go-version rebuild_swagger +.PHONY: default fmt lint check_shell sanity cover prof deps build test fulltest shorttest clean cleango deploy node_exporter install %gen gen NONGO_BIN check-go-version rebuild_kmd_swagger ###### TARGETS FOR CICD PROCESS ###### include ./scripts/release/mule/Makefile.mule diff --git a/buildnumber.dat b/buildnumber.dat index 0cfbf08886..573541ac97 100644 --- a/buildnumber.dat +++ b/buildnumber.dat @@ -1 +1 @@ -2 +0 diff --git a/catchup/service.go b/catchup/service.go index 1043718fe5..9198228256 100644 --- a/catchup/service.go +++ b/catchup/service.go @@ -43,6 +43,9 @@ const blockQueryPeerLimit = 10 // this should be at least the number of relays const catchupRetryLimit = 500 +// ErrSyncRoundInvalid is returned when the sync round requested is behind the current ledger round +var ErrSyncRoundInvalid = errors.New("requested sync round cannot be less than the latest round") + // PendingUnmatchedCertificate is a single certificate that is being waited upon to have its corresponding block fetched. type PendingUnmatchedCertificate struct { Cert agreement.Certificate @@ -64,7 +67,10 @@ type Ledger interface { // Service represents the catchup service. Once started and until it is stopped, it ensures that the ledger is up to date with network. type Service struct { - syncStartNS int64 // at top of struct to keep 64 bit aligned for atomic.* ops + syncStartNS int64 // at top of struct to keep 64 bit aligned for atomic.* ops + // disableSyncRound, provided externally, is the first round we will _not_ fetch from the network + // any round >= disableSyncRound will not be fetched. If set to 0, it will be disregarded. + disableSyncRound uint64 cfg config.Local ledger Ledger ctx context.Context @@ -147,6 +153,26 @@ func (s *Service) IsSynchronizing() (synchronizing bool, initialSync bool) { return } +// SetDisableSyncRound attempts to set the first round we _do_not_ want to fetch from the network +// Blocks from disableSyncRound or any round after disableSyncRound will not be fetched while this is set +func (s *Service) SetDisableSyncRound(rnd uint64) error { + if basics.Round(rnd) < s.ledger.LastRound() { + return ErrSyncRoundInvalid + } + atomic.StoreUint64(&s.disableSyncRound, rnd) + return nil +} + +// UnsetDisableSyncRound removes any previously set disabled sync round +func (s *Service) UnsetDisableSyncRound() { + atomic.StoreUint64(&s.disableSyncRound, 0) +} + +// GetDisableSyncRound returns the disabled sync round +func (s *Service) GetDisableSyncRound() uint64 { + return atomic.LoadUint64(&s.disableSyncRound) +} + // SynchronizingTime returns the time we've been performing a catchup operation (0 if not currently catching up) func (s *Service) SynchronizingTime() time.Duration { startNS := atomic.LoadInt64(&s.syncStartNS) @@ -202,6 +228,10 @@ func (s *Service) innerFetch(r basics.Round, peer network.Peer) (blk *bookkeepin // - If the block is already in the ledger (e.g. if agreement service has already written it) // - If the retrieval of the previous block was unsuccessful func (s *Service) fetchAndWrite(r basics.Round, prevFetchCompleteChan chan bool, lookbackComplete chan bool, peerSelector *peerSelector) bool { + // If sync-ing this round is not intended, don't fetch it + if dontSyncRound := s.GetDisableSyncRound(); dontSyncRound != 0 && r >= basics.Round(dontSyncRound) { + return false + } i := 0 hasLookback := false for true { diff --git a/catchup/service_test.go b/catchup/service_test.go index f364b7a465..efa341c347 100644 --- a/catchup/service_test.go +++ b/catchup/service_test.go @@ -181,6 +181,96 @@ func (cl *periodicSyncLogger) Warnf(s string, args ...interface{}) { cl.Logger.Warnf(s, args...) } +func TestSyncRound(t *testing.T) { + partitiontest.PartitionTest(t) + + // Make Ledger + local := new(mockedLedger) + local.blocks = append(local.blocks, bookkeeping.Block{}) + + remote, _, blk, err := buildTestLedger(t, bookkeeping.Block{}) + if err != nil { + t.Fatal(err) + return + } + addBlocks(t, remote, blk, 10) + + // Create a network and block service + blockServiceConfig := config.GetDefaultLocal() + net := &httpTestPeerSource{} + ls := rpcs.MakeBlockService(logging.Base(), blockServiceConfig, remote, net, "test genesisID") + + nodeA := basicRPCNode{} + nodeA.RegisterHTTPHandler(rpcs.BlockServiceBlockPath, ls) + nodeA.start() + defer nodeA.stop() + rootURL := nodeA.rootURL() + net.addPeer(rootURL) + + auth := &mockedAuthenticator{fail: true} + initialLocalRound := local.LastRound() + require.True(t, 0 == initialLocalRound) + + // Make Service + localCfg := config.GetDefaultLocal() + s := MakeService(logging.Base(), localCfg, net, local, auth, nil, nil) + s.log = &periodicSyncLogger{Logger: logging.Base()} + s.deadlineTimeout = 2 * time.Second + + // Set disable round success + err = s.SetDisableSyncRound(3) + require.NoError(t, err) + + s.Start() + defer s.Stop() + // wait past the initial sync - which is known to fail due to the above "auth" + time.Sleep(s.deadlineTimeout*2 - 200*time.Millisecond) + require.Equal(t, initialLocalRound, local.LastRound()) + auth.alter(-1, false) + + // wait until the catchup is done. Since we've might have missed the sleep window, we need to wait + // until the synchronization is complete. + waitStart := time.Now() + for time.Since(waitStart) < 2*s.deadlineTimeout { + if remote.LastRound() == local.LastRound() { + break + } + time.Sleep(20 * time.Millisecond) + } + // Assert that the last block is the one we expect--i.e. disableSyncRound - 1 + rnd := s.GetDisableSyncRound() + rr, lr := basics.Round(rnd-1), local.LastRound() + require.Equal(t, rr, lr) + + for r := basics.Round(1); r < rr; r++ { + localBlock, err := local.Block(r) + require.NoError(t, err) + remoteBlock, err := remote.Block(r) + require.NoError(t, err) + require.Equal(t, remoteBlock.Hash(), localBlock.Hash()) + } + + // unset syncRound and make sure we finish catching up + s.UnsetDisableSyncRound() + // wait until the catchup is done + waitStart = time.Now() + for time.Now().Sub(waitStart) < 8*s.deadlineTimeout { + if remote.LastRound() == local.LastRound() { + break + } + time.Sleep(20 * time.Millisecond) + } + rr, lr = remote.LastRound(), local.LastRound() + require.Equal(t, rr, lr) + for r := basics.Round(1); r < remote.LastRound(); r++ { + localBlock, err := local.Block(r) + require.NoError(t, err) + remoteBlock, err := remote.Block(r) + require.NoError(t, err) + require.Equal(t, remoteBlock.Hash(), localBlock.Hash()) + } +} + func TestPeriodicSync(t *testing.T) { partitiontest.PartitionTest(t) diff --git a/cmd/catchpointdump/database.go b/cmd/catchpointdump/database.go index dfbe7bdf5a..227ff7a6ac 100644 --- a/cmd/catchpointdump/database.go +++ b/cmd/catchpointdump/database.go @@ -26,6 +26,7 @@ import ( "github.com/algorand/go-algorand/crypto/merkletrie" "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/util/db" ) @@ -106,11 +107,11 @@ func checkDatabase(databaseName string, outFile *os.File) error { var stats merkletrie.Stats err = dbAccessor.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - committer, err := ledger.MakeMerkleCommitter(tx, ledgerTrackerStaging) + committer, err := store.MakeMerkleCommitter(tx, ledgerTrackerStaging) if err != nil { return err } - trie, err := merkletrie.MakeTrie(committer, ledger.TrieMemoryConfig) + trie, err := merkletrie.MakeTrie(committer, store.TrieMemoryConfig) if err != nil { return err } diff --git a/cmd/catchpointdump/file.go b/cmd/catchpointdump/file.go index aed2dfc1fc..97766ff971 100644 --- a/cmd/catchpointdump/file.go +++ b/cmd/catchpointdump/file.go @@ -39,6 +39,7 @@ import ( "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" @@ -318,6 +319,8 @@ func printAccountsDatabase(databaseName string, stagingTables bool, fileHeader l totals.RewardsLevel) } return dbAccessor.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + arw := store.NewAccountsSQLReaderWriter(tx) + fmt.Printf("\n") printDumpingCatchpointProgressLine(0, 50, 0) @@ -417,7 +420,7 @@ func printAccountsDatabase(databaseName string, stagingTables bool, fileHeader l progress++ acctCount++ } - _, err = ledger.LoadAllFullAccounts(context.Background(), tx, balancesTable, resourcesTable, acctCb) + _, err = arw.LoadAllFullAccounts(context.Background(), balancesTable, resourcesTable, acctCb) if err != nil { return } diff --git a/cmd/goal/account.go b/cmd/goal/account.go index 5cb3f416ca..6598422aa4 100644 --- a/cmd/goal/account.go +++ b/cmd/goal/account.go @@ -64,6 +64,7 @@ var ( dumpOutFile string listAccountInfo bool onlyShowAssetIds bool + partKeyIDToDelete string ) func init() { @@ -83,6 +84,7 @@ func init() { accountCmd.AddCommand(importRootKeysCmd) accountCmd.AddCommand(accountMultisigCmd) accountCmd.AddCommand(markNonparticipatingCmd) + accountCmd.AddCommand(deletePartKeyCmd) accountMultisigCmd.AddCommand(newMultisigCmd) accountMultisigCmd.AddCommand(deleteMultisigCmd) @@ -211,6 +213,11 @@ func init() { 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") + + // deletePartkeyCmd flags + deletePartKeyCmd.Flags().StringVarP(&partKeyIDToDelete, "partkeyid", "", "", "Participation Key ID to delete") + rewardsCmd.MarkFlagRequired("partkeyid") + } func scLeaseBytes(cmd *cobra.Command) (leaseBytes [32]byte) { @@ -342,6 +349,24 @@ var newCmd = &cobra.Command{ }, } +var deletePartKeyCmd = &cobra.Command{ + Use: "deletepartkey", + Short: "Delete a participation key", + Long: `Delete the indicated participation key.`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, args []string) { + dataDir := ensureSingleDataDir() + + client := ensureAlgodClient(dataDir) + + err := client.RemoveParticipationKey(partKeyIDToDelete) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + }, +} + var deleteCmd = &cobra.Command{ Use: "delete", Short: "Delete an account", diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index b91425e408..84aef5e699 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -1229,9 +1229,6 @@ var dryrunRemoteCmd = &cobra.Command{ trace = *txnResult.LogicSigTrace } } - if txnResult.Cost != nil { - fmt.Fprintf(os.Stdout, "tx[%d] cost: %d\n", i, *txnResult.Cost) - } if txnResult.BudgetConsumed != nil { fmt.Fprintf(os.Stdout, "tx[%d] budget consumed: %d\n", i, *txnResult.BudgetConsumed) } diff --git a/cmd/updater/update.sh b/cmd/updater/update.sh index 1da213ccf0..cc4403ffd1 100755 --- a/cmd/updater/update.sh +++ b/cmd/updater/update.sh @@ -1,7 +1,7 @@ #!/bin/bash # shellcheck disable=2009,2093,2164 -UPDATER_MIN_VERSION="3.8.0" +UPDATER_MIN_VERSION="3.12.2" UPDATER_CHANNEL="stable" FILENAME=$(basename -- "$0") SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" diff --git a/config/config.go b/config/config.go index 8e8dc4c276..c1c9893ca6 100644 --- a/config/config.go +++ b/config/config.go @@ -240,6 +240,11 @@ const ( dnssecTelemetryAddr ) +const ( + txFilterRawMsg = 1 + txFilterCanonical = 2 +) + const ( catchupValidationModeCertificate = 1 catchupValidationModePaysetHash = 2 diff --git a/config/config_test.go b/config/config_test.go index 4434cc3ae7..db35269ad6 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -580,3 +580,27 @@ func TestGetNonDefaultConfigValues(t *testing.T) { // check unmodified defaults assert.Empty(t, GetNonDefaultConfigValues(GetDefaultLocal(), []string{"AgreementIncomingBundlesQueueLength", "TxPoolSize"})) } + +func TestLocal_TxFiltering(t *testing.T) { + cfg := GetDefaultLocal() + + // ensure the default + require.True(t, cfg.TxFilterRawMsgEnabled()) + require.False(t, cfg.TxFilterCanonicalEnabled()) + + cfg.TxIncomingFilteringFlags = 0 + require.False(t, cfg.TxFilterRawMsgEnabled()) + require.False(t, cfg.TxFilterCanonicalEnabled()) + + cfg.TxIncomingFilteringFlags = 1 + require.True(t, cfg.TxFilterRawMsgEnabled()) + require.False(t, cfg.TxFilterCanonicalEnabled()) + + cfg.TxIncomingFilteringFlags = 2 + require.False(t, cfg.TxFilterRawMsgEnabled()) + require.True(t, cfg.TxFilterCanonicalEnabled()) + + cfg.TxIncomingFilteringFlags = 3 + require.True(t, cfg.TxFilterRawMsgEnabled()) + require.True(t, cfg.TxFilterCanonicalEnabled()) +} diff --git a/config/localTemplate.go b/config/localTemplate.go index ba9d97db7c..6be2f0e1a4 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -41,7 +41,7 @@ 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. 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" version[10]:"10" version[11]:"11" version[12]:"12" version[13]:"13" version[14]:"14" version[15]:"15" version[16]:"16" version[17]:"17" version[18]:"18" version[19]:"19" version[20]:"20" version[21]:"21" version[22]:"22" version[23]:"23" version[24]:"24" version[25]:"25"` + 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" version[10]:"10" version[11]:"11" version[12]:"12" version[13]:"13" version[14]:"14" version[15]:"15" version[16]:"16" version[17]:"17" version[18]:"18" version[19]:"19" version[20]:"20" version[21]:"21" version[22]:"22" version[23]:"23" version[24]:"24" version[25]:"25" version[26]:"26"` // environmental (may be overridden) // When enabled, stores blocks indefinitely, otherwise, only the most recent blocks @@ -460,6 +460,13 @@ type Local struct { // MaxAPIBoxPerApplication defines the maximum total number of boxes per application that will be returned // in GetApplicationBoxes REST API responses. MaxAPIBoxPerApplication uint64 `version[25]:"100000"` + + // TxIncomingFilteringFlags instructs algod filtering incoming tx messages + // Flag values: + // 0x00 - disabled + // 0x01 (txFilterRawMsg) - check for raw tx message duplicates + // 0x02 (txFilterCanonical) - check for canonical tx group duplicates + TxIncomingFilteringFlags uint32 `version[26]:"1"` } // DNSBootstrapArray returns an array of one or more DNS Bootstrap identifiers @@ -548,3 +555,13 @@ func (cfg Local) CatchupVerifyTransactionSignatures() bool { func (cfg Local) CatchupVerifyApplyData() bool { return cfg.CatchupBlockValidateMode&catchupValidationModeVerifyApplyData != 0 } + +// TxFilterRawMsgEnabled returns true if raw tx filtering is enabled +func (cfg Local) TxFilterRawMsgEnabled() bool { + return cfg.TxIncomingFilteringFlags&txFilterRawMsg != 0 +} + +// TxFilterCanonicalEnabled returns true if canonical tx group filtering is enabled +func (cfg Local) TxFilterCanonicalEnabled() bool { + return cfg.TxIncomingFilteringFlags&txFilterCanonical != 0 +} diff --git a/config/local_defaults.go b/config/local_defaults.go index 487149da9c..412c4a3dd2 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -20,7 +20,7 @@ package config var defaultLocal = Local{ - Version: 25, + Version: 26, AccountUpdatesStatsInterval: 5000000000, AccountsRebuildSynchronousMode: 1, AgreementIncomingBundlesQueueLength: 7, @@ -119,6 +119,7 @@ var defaultLocal = Local{ TelemetryToLog: true, TransactionSyncDataExchangeRate: 0, TransactionSyncSignificantMessageThreshold: 0, + TxIncomingFilteringFlags: 1, TxPoolExponentialIncreaseFactor: 2, TxPoolSize: 75000, TxSyncIntervalSeconds: 60, diff --git a/config/version.go b/config/version.go index d35a252ad1..6d1a91c3f2 100644 --- a/config/version.go +++ b/config/version.go @@ -33,7 +33,7 @@ const VersionMajor = 3 // 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 = 12 +const VersionMinor = 13 // Version is the type holding our full version information. type Version struct { diff --git a/crypto/batchverifier.go b/crypto/batchverifier.go index 96cf5e8503..a358d65e38 100644 --- a/crypto/batchverifier.go +++ b/crypto/batchverifier.go @@ -104,7 +104,7 @@ func (b *BatchVerifier) expand() { b.signatures = signatures } -// getNumberOfEnqueuedSignatures returns the number of signatures current enqueue onto the bacth verifier object +// getNumberOfEnqueuedSignatures returns the number of signatures current enqueue onto the batch verifier object func (b *BatchVerifier) getNumberOfEnqueuedSignatures() int { return len(b.messages) } diff --git a/daemon/algod/api/Makefile b/daemon/algod/api/Makefile index 12bf34fe97..811b18dc6f 100644 --- a/daemon/algod/api/Makefile +++ b/daemon/algod/api/Makefile @@ -2,7 +2,7 @@ GOPATH := $(shell go env GOPATH) GOPATH1 := $(firstword $(subst :, ,$(GOPATH))) # `make all` or just `make` should be appropriate for dev work -all: server/v2/generated/model/types.go server/v2/generated/nonparticipating/public/routes.go server/v2/generated/nonparticipating/private/routes.go server/v2/generated/participating/public/routes.go server/v2/generated/participating/private/routes.go +all: server/v2/generated/model/types.go server/v2/generated/nonparticipating/public/routes.go server/v2/generated/nonparticipating/private/routes.go server/v2/generated/participating/public/routes.go server/v2/generated/participating/private/routes.go server/v2/generated/data/routes.go # `make generate` should be able to replace old `generate.sh` script and be appropriate for build system use generate: oapi-codegen all @@ -19,6 +19,10 @@ server/v2/generated/participating/public/routes.go: algod.oas3.yml server/v2/generated/participating/private/routes.go: algod.oas3.yml $(GOPATH1)/bin/oapi-codegen -config ./server/v2/generated/participating/private/private_routes.yml algod.oas3.yml + +server/v2/generated/data/routes.go: algod.oas3.yml + $(GOPATH1)/bin/oapi-codegen -config ./server/v2/generated/data/data_routes.yml algod.oas3.yml + server/v2/generated/model/types.go: algod.oas3.yml $(GOPATH1)/bin/oapi-codegen -config ./server/v2/generated/model/model_types.yml algod.oas3.yml @@ -31,6 +35,6 @@ oapi-codegen: .PHONY ../../../scripts/buildtools/install_buildtools.sh -o github.com/algorand/oapi-codegen -c github.com/algorand/oapi-codegen/cmd/oapi-codegen clean: - rm -rf server/v2/generated/model/types.go server/v2/generated/nonparticipating/public/routes.go server/v2/generated/nonparticipating/private/routes.go server/v2/generated/participating/public/routes.go server/v2/generated/participating/private/routes.go algod.oas3.yml + rm -rf server/v2/generated/model/types.go server/v2/generated/nonparticipating/public/routes.go server/v2/generated/nonparticipating/private/routes.go server/v2/generated/participating/public/routes.go server/v2/generated/participating/private/routes.go server/v2/generated/data/routes.go algod.oas3.yml .PHONY: diff --git a/daemon/algod/api/README.md b/daemon/algod/api/README.md index 3502fcd120..15c382e0c4 100644 --- a/daemon/algod/api/README.md +++ b/daemon/algod/api/README.md @@ -20,69 +20,3 @@ Specifically, `uint64` types aren't strictly supported by OpenAPI. So we added a ## Why do we have algod.oas2.json and algod.oas3.yml? We chose to maintain V2 and V3 versions of the spec because OpenAPI v3 doesn't seem to be widely supported. Some tools worked better with V3 and others with V2, so having both available has been useful. To reduce developer burdon, the v2 specfile is automatically converted v3 using [converter.swagger.io](http://converter.swagger.io/). - -# Comments below are for v1 endpoints and are deprecated - -## Components: - -- `swagger.json` defines the API schema. However, server code in `api/v1/...` -currently serves as the ground truth, as the schema is generated from server code. - - to generate `swagger.json`, run `make build`. You may need to have `go-swagger` - installed. You can get it by running `make deps`. -- `api/client` is a package for internal (or external) libraries to interact with - the REST API. In particular, it should minimize dependencies. - - we currently use a non-swagger generated client. Why? The swagger generated client - pulls in too many dependencies (go-openapi, for instance) and unnecessary - functionality. Testing the swagger spec must be done another way. It seems that - unwrapped json raw types are sent on the wire (so not wrapped by responses), - so we don't need to decode them into responses. -- `api/v1/...` contains an implementation for the server. The swagger schema is auto-generated -(`cd api/; swagger generate spec -o ./swagger.json`) from server implementation code. -`api/v1/handlers` and `api/v1/models` should never be directly imported by external clients. - - or, run `go generate` in the `api` folder. - - -## Debugging/Engineering Notes: - -- `go-swagger` does not generate `x-nullable` properties on model fields. We want them -so that we can generate models without pointers. (This is more compatible with the -current model we use. We may want to use pointers instead, eventually) - - make sure you populate the `default` property in order to generate a model - without a pointer field -- `go-swagger` does not support OpenAPI 3.0. It only supports OpenAPI 2.0. There -does not seem to be another tool that allows us to generate a swagger spec from -code. It may be worth writing our own, eventually. -- `go-swagger` does not support embedded structs. - - in fact, `go-swagger` is generally very strange. The source -> spec generation - looks fairly immature. Here are some (undocumented) tips: - - every `swagger:response` type must contain a single field (e.g. `Body` or - `Payload`) that is the actual data type you want to return. So the `response` - type is a wrapper, which makes sense, except the clients that `go-swagger` - generate automatically unwrap the underlying value. So this is very weird, - and undocumented. - - `swagger:route` is a less powerful version of the `swagger:operation` - annotation. - However, `swagger:operation` is much more finicky and not mature. When defining - the annotation, make sure it is precise yaml, and start the yaml section with - `---`. This means keeping track of tabs and whitespaces. This seems to be the - easiest way to define parameters without having to make explicit structs - (which we may want to do eventually anyways). -- don't deal with `go-swagger` codegen docs. Refer directly to -`https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathItemObject`/ -- `go-swagger` does not support `regex` in path parameter path templating. -- complex parameter schemas are only supported in parameters `in:body` -- responses are distinct from definition objects (e.g. the former has a`description` -field, and headers). We always want to return a response in an operation. Returning -a model seems to work, but does not seem advised. -- `go-swagger` assumes `x-isnullable: true` and generates pointer files. If we ever -want to use a swagger generated client internally this may be a problem. Note that -`go-swagger` doesn't support a corresponding `x-isnullable` annotation. We can get around -that by using the `default` annotation and then find-and-replacing an `x-isnullable` into -the actual spec: -```//go:generate sed -i "" -e "s/\"default/\"x-nullable\": false, \"default/" ./swagger.json -//go:generate sed -i "" -e "s/object\",/object\", \"x-nullable\": false,/" ./swagger.json -``` -- go-swagger does not seem to support simple string responses. They always get wrapped. (oh well) - e.g. [https://github.com/go-swagger/go-swagger/issues/1635] -- I've hardcoded a keylength into the spec for now, until I figure out how to tie that programatically -back into the server code (perhaps with a find-and-replace). diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index 3961ad0b84..5d031442aa 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -1367,6 +1367,71 @@ } ] }, + "/v2/deltas/{round}": { + "get": { + "description": "Get ledger deltas for a round.", + "tags": [ + "private", + "data" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http" + ], + "summary": "Get a LedgerStateDelta object for a given round", + "operationId": "GetLedgerStateDelta", + "parameters": [ + { + "type": "integer", + "description": "The round for which the deltas are desired.", + "name": "round", + "in": "path", + "required": true, + "minimum": 0 + } + ], + "responses": { + "200": { + "$ref": "#/responses/LedgerStateDeltaResponse" + }, + "401": { + "description": "Invalid API Token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Could not find a delta for round", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "408": { + "description": "timed out on request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "503": { + "description": "Service Temporarily Unavailable", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Unknown Error" + } + } + } + }, "/v2/stateproofs/{round}": { "get": { "tags": [ @@ -1792,6 +1857,152 @@ } ] }, + "/v2/ledger/sync": { + "delete": { + "description": "Unset the ledger sync round.", + "tags": [ + "private", + "data" + ], + "schemes": [ + "http" + ], + "summary": "Removes minimum sync round restriction from the ledger.", + "operationId": "UnsetSyncRound", + "responses": { + "200": { + "type": "object" + }, + "400": { + "description": "Sync round not set.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "401": { + "description": "Invalid API Token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "503": { + "description": "Service Temporarily Unavailable", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Unknown Error" + } + } + }, + "get": { + "description": "Gets the minimum sync round for the ledger.", + "tags": [ + "private", + "data" + ], + "schemes": [ + "http" + ], + "summary": "Returns the minimum sync round the ledger is keeping in cache.", + "operationId": "GetSyncRound", + "responses": { + "200": { + "$ref": "#/responses/GetSyncRoundResponse" + }, + "400": { + "description": "Sync round not set.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "401": { + "description": "Invalid API Token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "503": { + "description": "Service Temporarily Unavailable", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Unknown Error" + } + } + } + }, + "/v2/ledger/sync/{round}": { + "post": { + "description": "Sets the minimum sync round on the ledger.", + "tags": [ + "private", + "data" + ], + "schemes": [ + "http" + ], + "summary": "Given a round, tells the ledger to keep that round in its cache.", + "operationId": "SetSyncRound", + "parameters": [ + { + "type": "integer", + "description": "The round for which the deltas are desired.", + "name": "round", + "in": "path", + "required": true, + "minimum": 0 + } + ], + "responses": { + "200": { + "type": "object" + }, + "400": { + "description": "Can not set sync round to an earlier round than the current round.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "401": { + "description": "Invalid API Token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "503": { + "description": "Service Temporarily Unavailable", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Unknown Error" + } + } + } + }, "/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). This endpoint is only enabled when a node's configuration file sets EnableDeveloperAPI to true.", @@ -2225,6 +2436,274 @@ } } }, + "LedgerStateDelta": { + "description": "Contains ledger updates.", + "type": "object", + "required": [ + ], + "properties": { + "accts": { + "description": "AccountDeltas object", + "$ref": "#/definitions/AccountDeltas" + }, + "kv-mods": { + "description": "Array of KV Deltas", + "type": "array", + "items": { + "$ref": "#/definitions/KvDelta" + } + }, + "tx-leases": { + "description": "List of transaction leases", + "type": "array", + "items": { + "$ref": "#/definitions/TxLease" + } + }, + "modified-apps": { + "description": "List of modified Apps", + "type": "array", + "items": { + "$ref": "#/definitions/ModifiedApp" + } + }, + "modified-assets": { + "description": "List of modified Assets", + "type": "array", + "items": { + "$ref": "#/definitions/ModifiedAsset" + } + }, + "state-proof-next": { + "description": "Next round for which we expect a state proof", + "type": "integer" + }, + "prev-timestamp": { + "description": "Previous block timestamp", + "type": "integer" + }, + "totals": { + "description": "Account Totals", + "$ref": "#/definitions/AccountTotals" + } + } + }, + "AccountTotals": { + "description": "Total Algos in the system grouped by account status", + "type": "object", + "required": [ + "online", + "offline", + "not-participating", + "rewards-level" + ], + "properties": { + "online": { + "description": "Amount of stake in online accounts", + "type": "integer" + }, + "offline": { + "description": "Amount of stake in offline accounts", + "type": "integer" + }, + "not-participating": { + "description": "Amount of stake in non-participating accounts", + "type": "integer" + }, + "rewards-level": { + "description": "Total number of algos received per reward unit since genesis", + "type": "integer" + } + } + }, + "AccountDeltas": { + "description": "Exposes deltas for account based resources in a single round", + "type": "object", + "properties": { + "accounts": { + "description": "Array of Account updates for the round", + "type": "array", + "items": { + "$ref": "#/definitions/AccountBalanceRecord" + } + }, + "apps": { + "description": "Array of App updates for the round.", + "type": "array", + "items": { + "$ref": "#/definitions/AppResourceRecord" + } + }, + "assets": { + "description": "Array of Asset updates for the round.", + "type": "array", + "items": { + "$ref": "#/definitions/AssetResourceRecord" + } + } + } + }, + "TxLease": { + "description": "", + "type": "object", + "required": [ + "sender", + "lease", + "expiration" + ], + "properties": { + "sender": { + "description": "Address of the lease sender", + "type": "string" + }, + "lease": { + "description": "Lease data", + "type": "string", + "format": "byte" + }, + "expiration": { + "description": "Round that the lease expires", + "type": "integer" + } + } + }, + "ModifiedAsset": { + "description": "Asset which was created or deleted.", + "type": "object", + "required": [ + "id", + "created", + "creator" + ], + "properties": { + "id": { + "description": "Asset Id", + "type": "integer" + }, + "created": { + "description": "Created if true, deleted if false", + "type": "boolean" + }, + "creator": { + "description": "Address of the creator.", + "type": "string" + } + } + }, + "ModifiedApp": { + "description": "App which was created or deleted.", + "type": "object", + "required": [ + "id", + "created", + "creator" + ], + "properties": { + "id": { + "description": "App Id", + "type": "integer" + }, + "created": { + "description": "Created if true, deleted if false", + "type": "boolean" + }, + "creator": { + "description": "Address of the creator.", + "type": "string" + } + } + }, + "AccountBalanceRecord": { + "description": "Account and its address", + "type": "object", + "required": [ + "address", + "account-data" + ], + "properties": { + "address": { + "description": "Address of the updated account.", + "type": "string" + }, + "account-data": { + "description": "Updated account data.", + "$ref": "#/definitions/Account" + } + } + }, + "AppResourceRecord": { + "description": "Represents AppParams and AppLocalStateDelta in deltas", + "type": "object", + "required": [ + "app-index", + "address", + "app-deleted", + "app-local-state-deleted" + ], + "properties": { + "app-index": { + "description": "App index", + "type": "integer", + "x-algorand-format": "uint64" + }, + "address": { + "description": "App account address", + "type": "string" + }, + "app-deleted": { + "description": "Whether the app was deleted", + "type": "boolean" + }, + "app-local-state-deleted": { + "description": "Whether the app local state was deleted", + "type": "boolean" + }, + "app-params": { + "description": "App params", + "$ref": "#/definitions/ApplicationParams" + }, + "app-local-state": { + "description": "App local state", + "$ref": "#/definitions/ApplicationLocalState" + } + } + }, + "AssetResourceRecord": { + "description": "Represents AssetParams and AssetHolding in deltas", + "required": [ + "asset-index", + "address", + "asset-deleted", + "asset-holding-deleted" + ], + "properties": { + "asset-index": { + "description": "Index of the asset", + "type": "integer", + "x-algorand-format": "uint64" + }, + "address": { + "description": "Account address of the asset", + "type": "string" + }, + "asset-deleted": { + "description": "Whether the asset was deleted", + "type": "boolean" + }, + "asset-params": { + "description": "Asset params", + "$ref": "#/definitions/AssetParams" + }, + "asset-holding-deleted": { + "description": "Whether the asset holding was deleted", + "type": "boolean" + }, + "asset-holding": { + "description": "The asset holding", + "$ref": "#/definitions/AssetHolding" + } + } + }, "AccountParticipation": { "description": "AccountParticipation describes the parameters used by this account in consensus protocol.", "type": "object", @@ -2744,10 +3223,6 @@ "budget-consumed": { "description": "Budget consumed during execution of app call transaction.", "type": "integer" - }, - "cost": { - "description": "Net cost of app execution. Field is DEPRECATED and is subject for removal. Instead, use `budget-added` and `budget-consumed.", - "type": "integer" } } }, @@ -2882,6 +3357,22 @@ } } }, + "KvDelta": { + "description": "A single Delta containing the key, the previous value and the current value for a single round.", + "type": "object", + "properties": { + "key": { + "description": "The key, base64 encoded.", + "type": "string", + "format": "byte" + }, + "value": { + "description": "The new value of the KV store entry, base64 encoded.", + "type": "string", + "format": "byte" + } + } + }, "Version": { "description": "algod version information.", "type": "object", @@ -3279,6 +3770,27 @@ } }, "responses": { + "GetSyncRoundResponse": { + "description": "Response containing the ledger's minimum sync round", + "schema": { + "type": "object", + "required": [ + "round" + ], + "properties": { + "round": { + "description": "The minimum sync round for the ledger.", + "type": "integer" + } + } + } + }, + "LedgerStateDeltaResponse": { + "description": "Contains ledger deltas", + "schema": { + "$ref": "#/definitions/LedgerStateDelta" + } + }, "LightBlockHeaderProofResponse": { "description": "Proof of a light block header.", "schema": { @@ -3681,8 +4193,7 @@ "description": "The minimum transaction fee (not per byte) required for the\ntxn to validate for the current network protocol.", "type": "integer" } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + } } }, "ApplicationResponse": { diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index 0dd7a6c2e8..0f50ac905a 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -497,6 +497,35 @@ }, "description": "DryrunResponse contains per-txn debug information from a dryrun." }, + "GetSyncRoundResponse": { + "content": { + "application/json": { + "schema": { + "properties": { + "round": { + "description": "The minimum sync round for the ledger.", + "type": "integer" + } + }, + "required": [ + "round" + ], + "type": "object" + } + } + }, + "description": "Response containing the ledger's minimum sync round" + }, + "LedgerStateDeltaResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LedgerStateDelta" + } + } + }, + "description": "Contains ledger deltas" + }, "LightBlockHeaderProofResponse": { "content": { "application/json": { @@ -773,8 +802,7 @@ "last-round", "min-fee" ], - "type": "object", - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + "type": "object" } } }, @@ -971,6 +999,50 @@ ], "type": "object" }, + "AccountBalanceRecord": { + "description": "Account and its address", + "properties": { + "account-data": { + "$ref": "#/components/schemas/Account" + }, + "address": { + "description": "Address of the updated account.", + "type": "string" + } + }, + "required": [ + "account-data", + "address" + ], + "type": "object" + }, + "AccountDeltas": { + "description": "Exposes deltas for account based resources in a single round", + "properties": { + "accounts": { + "description": "Array of Account updates for the round", + "items": { + "$ref": "#/components/schemas/AccountBalanceRecord" + }, + "type": "array" + }, + "apps": { + "description": "Array of App updates for the round.", + "items": { + "$ref": "#/components/schemas/AppResourceRecord" + }, + "type": "array" + }, + "assets": { + "description": "Array of Asset updates for the round.", + "items": { + "$ref": "#/components/schemas/AssetResourceRecord" + }, + "type": "array" + } + }, + "type": "object" + }, "AccountParticipation": { "description": "AccountParticipation describes the parameters used by this account in consensus protocol.", "properties": { @@ -1030,6 +1102,69 @@ ], "type": "object" }, + "AccountTotals": { + "description": "Total Algos in the system grouped by account status", + "properties": { + "not-participating": { + "description": "Amount of stake in non-participating accounts", + "type": "integer" + }, + "offline": { + "description": "Amount of stake in offline accounts", + "type": "integer" + }, + "online": { + "description": "Amount of stake in online accounts", + "type": "integer" + }, + "rewards-level": { + "description": "Total number of algos received per reward unit since genesis", + "type": "integer" + } + }, + "required": [ + "not-participating", + "offline", + "online", + "rewards-level" + ], + "type": "object" + }, + "AppResourceRecord": { + "description": "Represents AppParams and AppLocalStateDelta in deltas", + "properties": { + "address": { + "description": "App account address", + "type": "string" + }, + "app-deleted": { + "description": "Whether the app was deleted", + "type": "boolean" + }, + "app-index": { + "description": "App index", + "type": "integer", + "x-algorand-format": "uint64" + }, + "app-local-state": { + "$ref": "#/components/schemas/ApplicationLocalState" + }, + "app-local-state-deleted": { + "description": "Whether the app local state was deleted", + "type": "boolean" + }, + "app-params": { + "$ref": "#/components/schemas/ApplicationParams" + } + }, + "required": [ + "address", + "app-deleted", + "app-index", + "app-local-state-deleted" + ], + "type": "object" + }, "Application": { "description": "Application index and its parameters", "properties": { @@ -1252,6 +1387,41 @@ ], "type": "object" }, + "AssetResourceRecord": { + "description": "Represents AssetParams and AssetHolding in deltas", + "properties": { + "address": { + "description": "Account address of the asset", + "type": "string" + }, + "asset-deleted": { + "description": "Whether the asset was deleted", + "type": "boolean" + }, + "asset-holding": { + "$ref": "#/components/schemas/AssetHolding" + }, + "asset-holding-deleted": { + "description": "Whether the asset holding was deleted", + "type": "boolean" + }, + "asset-index": { + "description": "Index of the asset", + "type": "integer", + "x-algorand-format": "uint64" + }, + "asset-params": { + "$ref": "#/components/schemas/AssetParams" + } + }, + "required": [ + "address", + "asset-deleted", + "asset-holding-deleted", + "asset-index" + ], + "type": "object" + }, "Box": { "description": "Box name and its content.", "properties": { @@ -1464,10 +1634,6 @@ "description": "Budget consumed during execution of app call transaction.", "type": "integer" }, - "cost": { - "description": "Net cost of app execution. Field is DEPRECATED and is subject for removal. Instead, use `budget-added` and `budget-consumed.", - "type": "integer" - }, "disassembly": { "description": "Disassembled program line by line.", "items": { @@ -1571,6 +1737,72 @@ ], "type": "object" }, + "KvDelta": { + "description": "A single Delta containing the key, the previous value and the current value for a single round.", + "properties": { + "key": { + "description": "The key, base64 encoded.", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + }, + "value": { + "description": "The new value of the KV store entry, base64 encoded.", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + } + }, + "type": "object" + }, + "LedgerStateDelta": { + "description": "Contains ledger updates.", + "properties": { + "accts": { + "$ref": "#/components/schemas/AccountDeltas" + }, + "kv-mods": { + "description": "Array of KV Deltas", + "items": { + "$ref": "#/components/schemas/KvDelta" + }, + "type": "array" + }, + "modified-apps": { + "description": "List of modified Apps", + "items": { + "$ref": "#/components/schemas/ModifiedApp" + }, + "type": "array" + }, + "modified-assets": { + "description": "List of modified Assets", + "items": { + "$ref": "#/components/schemas/ModifiedAsset" + }, + "type": "array" + }, + "prev-timestamp": { + "description": "Previous block timestamp", + "type": "integer" + }, + "state-proof-next": { + "description": "Next round for which we expect a state proof", + "type": "integer" + }, + "totals": { + "$ref": "#/components/schemas/AccountTotals" + }, + "tx-leases": { + "description": "List of transaction leases", + "items": { + "$ref": "#/components/schemas/TxLease" + }, + "type": "array" + } + }, + "type": "object" + }, "LightBlockHeaderProof": { "description": "Proof of membership and position of a light block header.", "properties": { @@ -1596,6 +1828,52 @@ ], "type": "object" }, + "ModifiedApp": { + "description": "App which was created or deleted.", + "properties": { + "created": { + "description": "Created if true, deleted if false", + "type": "boolean" + }, + "creator": { + "description": "Address of the creator.", + "type": "string" + }, + "id": { + "description": "App Id", + "type": "integer" + } + }, + "required": [ + "created", + "creator", + "id" + ], + "type": "object" + }, + "ModifiedAsset": { + "description": "Asset which was created or deleted.", + "properties": { + "created": { + "description": "Created if true, deleted if false", + "type": "boolean" + }, + "creator": { + "description": "Address of the creator.", + "type": "string" + }, + "id": { + "description": "Asset Id", + "type": "integer" + } + }, + "required": [ + "created", + "creator", + "id" + ], + "type": "object" + }, "ParticipationKey": { "description": "Represents a participation key used by the node.", "properties": { @@ -1832,6 +2110,31 @@ ], "type": "object" }, + "TxLease": { + "description": "", + "properties": { + "expiration": { + "description": "Round that the lease expires", + "type": "integer" + }, + "lease": { + "description": "Lease data", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + }, + "sender": { + "description": "Address of the lease sender", + "type": "string" + } + }, + "required": [ + "expiration", + "lease", + "sender" + ], + "type": "object" + }, "Version": { "description": "algod version information.", "properties": { @@ -3516,6 +3819,95 @@ ] } }, + "/v2/deltas/{round}": { + "get": { + "description": "Get ledger deltas for a round.", + "operationId": "GetLedgerStateDelta", + "parameters": [ + { + "description": "The round for which the deltas are desired.", + "in": "path", + "name": "round", + "required": true, + "schema": { + "minimum": 0, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LedgerStateDelta" + } + } + }, + "description": "Contains ledger deltas" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid API Token" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Could not find a delta for round" + }, + "408": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "timed out on request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Temporarily Unavailable" + }, + "default": { + "content": {}, + "description": "Unknown Error" + } + }, + "summary": "Get a LedgerStateDelta object for a given round", + "tags": [ + "private", + "data" + ] + } + }, "/v2/ledger/supply": { "get": { "operationId": "GetSupply", @@ -3572,6 +3964,212 @@ ] } }, + "/v2/ledger/sync": { + "delete": { + "description": "Unset the ledger sync round.", + "operationId": "UnsetSyncRound", + "responses": { + "200": { + "content": {} + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Sync round not set." + }, + "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" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Temporarily Unavailable" + }, + "default": { + "content": {}, + "description": "Unknown Error" + } + }, + "summary": "Removes minimum sync round restriction from the ledger.", + "tags": [ + "private", + "data" + ] + }, + "get": { + "description": "Gets the minimum sync round for the ledger.", + "operationId": "GetSyncRound", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "round": { + "description": "The minimum sync round for the ledger.", + "type": "integer" + } + }, + "required": [ + "round" + ], + "type": "object" + } + } + }, + "description": "Response containing the ledger's minimum sync round" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Sync round not set." + }, + "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" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Temporarily Unavailable" + }, + "default": { + "content": {}, + "description": "Unknown Error" + } + }, + "summary": "Returns the minimum sync round the ledger is keeping in cache.", + "tags": [ + "private", + "data" + ] + } + }, + "/v2/ledger/sync/{round}": { + "post": { + "description": "Sets the minimum sync round on the ledger.", + "operationId": "SetSyncRound", + "parameters": [ + { + "description": "The round for which the deltas are desired.", + "in": "path", + "name": "round", + "required": true, + "schema": { + "minimum": 0, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": {} + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Can not set sync round to an earlier round than the current round." + }, + "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" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Temporarily Unavailable" + }, + "default": { + "content": {}, + "description": "Unknown Error" + } + }, + "summary": "Given a round, tells the ledger to keep that round in its cache.", + "tags": [ + "private", + "data" + ] + } + }, "/v2/participation": { "get": { "description": "Return a list of participation keys", @@ -4801,8 +5399,7 @@ "last-round", "min-fee" ], - "type": "object", - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + "type": "object" } } }, diff --git a/daemon/algod/api/client/restClient.go b/daemon/algod/api/client/restClient.go index 7d56a0ab79..a0ac7d1f2a 100644 --- a/daemon/algod/api/client/restClient.go +++ b/daemon/algod/api/client/restClient.go @@ -163,7 +163,8 @@ type RawResponse interface { } // submitForm is a helper used for submitting (ex.) GETs and POSTs to the server -func (client RestClient) submitForm(response interface{}, path string, request interface{}, requestMethod string, encodeJSON bool, decodeJSON bool) error { +// if expectNoContent is true, then it is expected that the response received will have a content length of zero +func (client RestClient) submitForm(response interface{}, path string, request interface{}, requestMethod string, encodeJSON bool, decodeJSON bool, expectNoContent bool) error { var err error queryURL := client.serverURL queryURL.Path = path @@ -218,6 +219,13 @@ func (client RestClient) submitForm(response interface{}, path string, request i return err } + if expectNoContent { + if resp.ContentLength == 0 { + return nil + } + return fmt.Errorf("expected empty response but got response of %d bytes", resp.ContentLength) + } + if decodeJSON { dec := json.NewDecoder(resp.Body) return dec.Decode(&response) @@ -240,20 +248,26 @@ func (client RestClient) submitForm(response interface{}, path string, request i // get performs a GET request to the specific path against the server func (client RestClient) get(response interface{}, path string, request interface{}) error { - return client.submitForm(response, path, request, "GET", false /* encodeJSON */, true /* decodeJSON */) + return client.submitForm(response, path, request, "GET", false /* encodeJSON */, true /* decodeJSON */, false) +} + +// delete performs a DELETE request to the specific path against the server +// when expectNoContent is true, then no content is expected to be returned from the endpoint +func (client RestClient) delete(response interface{}, path string, request interface{}, expectNoContent bool) error { + return client.submitForm(response, path, request, "DELETE", false /* encodeJSON */, true /* decodeJSON */, expectNoContent) } // getRaw behaves identically to get but doesn't json decode the response, and // the response must implement the RawResponse interface func (client RestClient) getRaw(response RawResponse, path string, request interface{}) error { - return client.submitForm(response, path, request, "GET", false /* encodeJSON */, false /* decodeJSON */) + return client.submitForm(response, path, request, "GET", false /* encodeJSON */, false /* decodeJSON */, false) } // post sends a POST request to the given path with the given request object. // No query parameters will be sent if request is nil. // response must be a pointer to an object as post writes the response there. func (client RestClient) post(response interface{}, path string, request interface{}) error { - return client.submitForm(response, path, request, "POST", true /* encodeJSON */, true /* decodeJSON */) + return client.submitForm(response, path, request, "POST", true /* encodeJSON */, true /* decodeJSON */, false) } // Status retrieves the StatusResponse from the running node @@ -530,13 +544,13 @@ func (client RestClient) Shutdown() (err error) { // AbortCatchup aborts the currently running catchup func (client RestClient) AbortCatchup(catchpointLabel string) (response model.CatchpointAbortResponse, err error) { - err = client.submitForm(&response, fmt.Sprintf("/v2/catchup/%s", catchpointLabel), nil, "DELETE", false, true) + err = client.submitForm(&response, fmt.Sprintf("/v2/catchup/%s", catchpointLabel), nil, "DELETE", false, true, false) return } // Catchup start catching up to the give catchpoint label func (client RestClient) Catchup(catchpointLabel string) (response model.CatchpointStartResponse, err error) { - err = client.submitForm(&response, fmt.Sprintf("/v2/catchup/%s", catchpointLabel), nil, "POST", false, true) + err = client.submitForm(&response, fmt.Sprintf("/v2/catchup/%s", catchpointLabel), nil, "POST", false, true, false) return } @@ -554,7 +568,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 model.CompileResponse - err = client.submitForm(&compileResponse, "/v2/teal/compile", program, "POST", false, true) + err = client.submitForm(&compileResponse, "/v2/teal/compile", program, "POST", false, true, false) if err != nil { return nil, crypto.Digest{}, err } @@ -610,7 +624,7 @@ func (client RestClient) doGetWithQuery(ctx context.Context, path string, queryA // 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 */) + err = client.submitForm(&blob, "/v2/teal/dryrun", data, "POST", false /* encodeJSON */, false /* decodeJSON */, false) response = blob return } @@ -651,3 +665,10 @@ func (client RestClient) GetParticipationKeyByID(participationID string) (respon err = client.get(&response, fmt.Sprintf("/v2/participation/%s", participationID), nil) return } + +// RemoveParticipationKeyByID removes a particiption key by its ID +func (client RestClient) RemoveParticipationKeyByID(participationID string) (err error) { + err = client.delete(nil, fmt.Sprintf("/v2/participation/%s", participationID), nil, true) + return + +} diff --git a/daemon/algod/api/server/common/handlers.go b/daemon/algod/api/server/common/handlers.go index e32cb2645c..f9e2c47476 100644 --- a/daemon/algod/api/server/common/handlers.go +++ b/daemon/algod/api/server/common/handlers.go @@ -112,7 +112,7 @@ func VersionsHandler(ctx lib.ReqContext, context echo.Context) { w.WriteHeader(http.StatusOK) response := VersionsResponse{ Body: common.Version{ - Versions: []string{"v1", "v2"}, + Versions: []string{"v2"}, GenesisID: ctx.Node.GenesisID(), GenesisHash: gh[:], Build: common.BuildVersion{ diff --git a/daemon/algod/api/server/lib/bundle_swagger_json.sh b/daemon/algod/api/server/lib/bundle_swagger_json.sh deleted file mode 100755 index d1c7057d62..0000000000 --- a/daemon/algod/api/server/lib/bundle_swagger_json.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -THISDIR=$(dirname $0) - -cat < $THISDIR/bundledSpecInject.go -// Code generated by bundle_swagger_json.sh, along with swagger.json. DO NOT EDIT. -package lib - -func init() { - SwaggerSpecJSON = string([]byte{ - $(cat $THISDIR/../../swagger.json | hexdump -v -e '1/1 "0x%02X, "' | fmt -w 100) - }) -} - -EOM diff --git a/daemon/algod/api/server/lib/bundledSpecInject.go b/daemon/algod/api/server/lib/bundledSpecInject.go deleted file mode 100644 index 50fc14c2ee..0000000000 --- a/daemon/algod/api/server/lib/bundledSpecInject.go +++ /dev/null @@ -1,4155 +0,0 @@ -// Code generated by bundle_swagger_json.sh, along with swagger.json. DO NOT EDIT. -package lib - -func init() { - SwaggerSpecJSON = string([]byte{ - 0x7B, 0x0A, 0x20, 0x20, 0x22, 0x63, 0x6F, 0x6E, 0x73, 0x75, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, - 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, - 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, - 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, - 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, - 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x22, 0x73, 0x77, 0x61, 0x67, - 0x67, 0x65, 0x72, 0x22, 0x3A, 0x20, 0x22, 0x32, 0x2E, 0x30, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x22, - 0x69, 0x6E, 0x66, 0x6F, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x50, 0x49, - 0x20, 0x45, 0x6E, 0x64, 0x70, 0x6F, 0x69, 0x6E, 0x74, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6C, - 0x67, 0x6F, 0x44, 0x20, 0x4F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x74, 0x6C, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x41, 0x6C, 0x67, 0x6F, 0x64, 0x20, 0x52, 0x45, 0x53, 0x54, 0x20, 0x41, 0x50, 0x49, 0x2E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x63, 0x74, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x65, 0x6D, 0x61, 0x69, 0x6C, 0x22, - 0x3A, 0x20, 0x22, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x63, 0x74, 0x40, 0x61, 0x6C, 0x67, 0x6F, 0x72, - 0x61, 0x6E, 0x64, 0x2E, 0x63, 0x6F, 0x6D, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x22, 0x68, - 0x6F, 0x73, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x68, 0x6F, 0x73, 0x74, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x22, 0x62, 0x61, 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x3A, - 0x20, 0x22, 0x2F, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x74, 0x75, - 0x72, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6E, 0x74, 0x69, 0x72, 0x65, 0x20, 0x67, - 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x69, 0x6E, 0x20, 0x6A, - 0x73, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, - 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, - 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, - 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, - 0x22, 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x4A, 0x53, 0x4F, 0x4E, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, - 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, - 0x20, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, - 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x68, 0x65, 0x61, 0x6C, 0x74, 0x68, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, - 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, - 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, - 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, - 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, - 0x3A, 0x20, 0x22, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x4F, 0x4B, 0x20, 0x69, 0x66, - 0x20, 0x68, 0x65, 0x61, 0x6C, 0x74, 0x68, 0x79, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, - 0x22, 0x3A, 0x20, 0x22, 0x48, 0x65, 0x61, 0x6C, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6B, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4F, 0x4B, 0x2E, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, - 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x6D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, - 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2F, 0x70, 0x6C, 0x61, 0x69, 0x6E, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x52, - 0x65, 0x74, 0x75, 0x72, 0x6E, 0x20, 0x6D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x20, 0x61, 0x62, - 0x6F, 0x75, 0x74, 0x20, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x20, 0x66, 0x75, 0x6E, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x69, 0x6E, 0x67, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, - 0x22, 0x4D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x74, 0x65, 0x78, 0x74, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x5C, 0x5C, 0x23, 0x2D, 0x63, - 0x6F, 0x6D, 0x6D, 0x65, 0x6E, 0x74, 0x73, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x6B, 0x65, 0x79, 0x3A, - 0x76, 0x61, 0x6C, 0x75, 0x65, 0x20, 0x6C, 0x69, 0x6E, 0x65, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x34, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x6D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, - 0x20, 0x77, 0x65, 0x72, 0x65, 0x20, 0x63, 0x6F, 0x6D, 0x70, 0x69, 0x6C, 0x65, 0x64, 0x20, 0x6F, - 0x75, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x73, - 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x2E, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x65, 0x6E, 0x74, 0x69, 0x72, 0x65, 0x20, 0x73, 0x77, 0x61, 0x67, 0x67, 0x65, - 0x72, 0x20, 0x73, 0x70, 0x65, 0x63, 0x20, 0x69, 0x6E, 0x20, 0x6A, 0x73, 0x6F, 0x6E, 0x2E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, - 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, - 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, - 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, - 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x75, - 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x73, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x20, 0x73, 0x70, - 0x65, 0x63, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x77, - 0x61, 0x67, 0x67, 0x65, 0x72, 0x4A, 0x53, 0x4F, 0x4E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, - 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x73, 0x77, - 0x61, 0x67, 0x67, 0x65, 0x72, 0x20, 0x73, 0x70, 0x65, 0x63, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, - 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x61, - 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x2F, 0x7B, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x7D, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x69, 0x76, 0x65, - 0x6E, 0x20, 0x61, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x20, 0x61, 0x63, 0x63, - 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x2C, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x63, 0x61, 0x6C, 0x6C, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, - 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x73, 0x20, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2C, 0x20, 0x62, 0x61, 0x6C, 0x61, 0x6E, 0x63, 0x65, 0x20, - 0x61, 0x6E, 0x64, 0x20, 0x73, 0x70, 0x65, 0x6E, 0x64, 0x61, 0x62, 0x6C, 0x65, 0x20, 0x61, 0x6D, - 0x6F, 0x75, 0x6E, 0x74, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, - 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x61, - 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x63, - 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, - 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x5B, - 0x41, 0x2D, 0x5A, 0x30, 0x2D, 0x39, 0x5D, 0x7B, 0x35, 0x38, 0x7D, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6E, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, - 0x6E, 0x74, 0x20, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, - 0x20, 0x74, 0x72, 0x75, 0x65, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, - 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x41, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x49, - 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, - 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x30, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x42, 0x61, 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, - 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, - 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, - 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, 0x6E, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, - 0x74, 0x65, 0x72, 0x6E, 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, - 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, - 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, - 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, - 0x2F, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x2F, 0x7B, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x7D, 0x2F, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x7B, - 0x74, 0x78, 0x69, 0x64, 0x7D, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x47, 0x69, 0x76, 0x65, 0x6E, 0x20, 0x61, 0x20, 0x77, 0x61, 0x6C, 0x6C, 0x65, 0x74, 0x20, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x61, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x64, 0x2C, 0x20, 0x69, 0x74, - 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6F, 0x6E, - 0x66, 0x69, 0x72, 0x6D, 0x65, 0x64, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x20, - 0x54, 0x68, 0x69, 0x73, 0x20, 0x63, 0x61, 0x6C, 0x6C, 0x20, 0x73, 0x63, 0x61, 0x6E, 0x73, 0x20, - 0x75, 0x70, 0x20, 0x74, 0x6F, 0x20, 0x5C, 0x75, 0x30, 0x30, 0x33, 0x63, 0x43, 0x75, 0x72, 0x72, - 0x65, 0x6E, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x5C, 0x75, 0x30, 0x30, 0x33, - 0x65, 0x2E, 0x4D, 0x61, 0x78, 0x54, 0x78, 0x6E, 0x4C, 0x69, 0x66, 0x65, 0x20, 0x62, 0x6C, 0x6F, - 0x63, 0x6B, 0x73, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x73, 0x74, 0x2E, - 0x5C, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, - 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, - 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, - 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, - 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x61, 0x20, 0x73, 0x70, - 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x20, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x72, 0x6D, 0x65, 0x64, - 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, - 0x74, 0x65, 0x72, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x5B, 0x41, 0x2D, - 0x5A, 0x30, 0x2D, 0x39, 0x5D, 0x7B, 0x35, 0x38, 0x7D, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6E, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, - 0x20, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x70, 0x61, 0x74, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x74, - 0x72, 0x75, 0x65, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x5B, 0x41, 0x2D, 0x5A, 0x30, 0x2D, 0x39, 0x5D, 0x2B, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x74, 0x78, 0x69, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x70, 0x61, 0x74, - 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x74, 0x72, 0x75, 0x65, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, - 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, - 0x65, 0x73, 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x52, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x34, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x42, 0x61, 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, - 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, - 0x30, 0x31, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, - 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, - 0x34, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x4E, 0x6F, - 0x74, 0x20, 0x46, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, - 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x61, 0x63, 0x63, 0x6F, - 0x75, 0x6E, 0x74, 0x2F, 0x7B, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x7D, 0x2F, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x72, - 0x6D, 0x65, 0x64, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x20, 0x62, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6E, 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6E, 0x20, - 0x61, 0x20, 0x64, 0x61, 0x74, 0x65, 0x20, 0x72, 0x61, 0x6E, 0x67, 0x65, 0x2E, 0x20, 0x57, 0x68, - 0x65, 0x6E, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x63, 0x61, 0x6C, 0x6C, - 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x52, - 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x6C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, - 0x6E, 0x64, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x61, - 0x6E, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x69, 0x66, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, - 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x69, 0x73, 0x20, 0x6E, 0x6F, 0x74, 0x20, 0x61, 0x76, 0x61, - 0x69, 0x6C, 0x61, 0x62, 0x6C, 0x65, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x6F, - 0x64, 0x65, 0x2E, 0x20, 0x54, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x20, 0x72, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x73, 0x20, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x20, 0x66, 0x72, 0x6F, 0x6D, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6F, 0x6C, 0x64, 0x65, 0x73, - 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, - 0x74, 0x20, 0x61, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x63, 0x6F, 0x6E, 0x66, - 0x69, 0x72, 0x6D, 0x65, 0x64, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, - 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, - 0x74, 0x74, 0x65, 0x72, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x5B, 0x41, 0x2D, 0x5A, 0x30, 0x2D, 0x39, - 0x5D, 0x7B, 0x35, 0x38, 0x7D, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, - 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x41, 0x6E, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x70, 0x75, 0x62, - 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x74, 0x72, 0x75, 0x65, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6D, 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x22, 0x3A, 0x20, - 0x30, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x44, 0x6F, 0x20, - 0x6E, 0x6F, 0x74, 0x20, 0x66, 0x65, 0x74, 0x63, 0x68, 0x20, 0x61, 0x6E, 0x79, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x62, 0x65, 0x66, 0x6F, 0x72, - 0x65, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x2E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x66, 0x69, 0x72, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x6D, 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x22, 0x3A, 0x20, 0x30, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, - 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x44, 0x6F, 0x20, 0x6E, 0x6F, 0x74, 0x20, - 0x66, 0x65, 0x74, 0x63, 0x68, 0x20, 0x61, 0x6E, 0x79, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x6C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x22, 0x3A, 0x20, 0x22, 0x64, 0x61, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x44, 0x6F, 0x20, 0x6E, 0x6F, 0x74, 0x20, 0x66, 0x65, - 0x74, 0x63, 0x68, 0x20, 0x61, 0x6E, 0x79, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x62, 0x65, 0x66, 0x6F, 0x72, 0x65, 0x20, 0x74, 0x68, 0x69, 0x73, - 0x20, 0x64, 0x61, 0x74, 0x65, 0x2E, 0x20, 0x28, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x20, - 0x6F, 0x6E, 0x6C, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x65, - 0x72, 0x29, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x66, 0x72, 0x6F, 0x6D, 0x44, 0x61, - 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, - 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x64, 0x61, 0x74, - 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x44, 0x6F, 0x20, 0x6E, 0x6F, 0x74, 0x20, 0x66, 0x65, 0x74, 0x63, 0x68, 0x20, 0x61, 0x6E, 0x79, - 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x61, 0x66, - 0x74, 0x65, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x64, 0x61, 0x74, 0x65, 0x2E, 0x20, 0x28, - 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x20, 0x6F, 0x6E, 0x6C, 0x79, 0x20, 0x77, 0x69, 0x74, - 0x68, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x65, 0x72, 0x29, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x74, 0x6F, 0x44, 0x61, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, - 0x65, 0x72, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x6D, 0x61, 0x78, 0x69, 0x6D, 0x75, 0x6D, 0x20, - 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x74, 0x6F, 0x20, - 0x73, 0x68, 0x6F, 0x77, 0x20, 0x28, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x20, 0x74, 0x6F, - 0x20, 0x31, 0x30, 0x30, 0x29, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6D, 0x61, 0x78, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, - 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x54, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x30, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x42, 0x61, 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, - 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, - 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, - 0x6E, 0x74, 0x65, 0x72, 0x6E, 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, - 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, - 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, - 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, - 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, - 0x31, 0x2F, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x2F, 0x7B, 0x61, 0x64, 0x64, 0x72, 0x7D, - 0x2F, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x70, 0x65, - 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x47, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, - 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x2C, 0x20, 0x73, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x70, 0x72, 0x69, 0x6F, - 0x72, 0x69, 0x74, 0x79, 0x2C, 0x20, 0x69, 0x6E, 0x20, 0x64, 0x65, 0x63, 0x72, 0x65, 0x61, 0x73, - 0x69, 0x6E, 0x67, 0x20, 0x6F, 0x72, 0x64, 0x65, 0x72, 0x2C, 0x20, 0x74, 0x72, 0x75, 0x6E, 0x63, - 0x61, 0x74, 0x65, 0x64, 0x20, 0x61, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6E, 0x64, 0x20, - 0x61, 0x74, 0x20, 0x4D, 0x41, 0x58, 0x2E, 0x20, 0x49, 0x66, 0x20, 0x4D, 0x41, 0x58, 0x20, 0x3D, - 0x20, 0x30, 0x2C, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x61, 0x6C, 0x6C, 0x20, - 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x73, 0x2E, 0x5C, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, - 0x20, 0x61, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x75, 0x6E, 0x63, 0x6F, 0x6E, - 0x66, 0x69, 0x72, 0x6D, 0x65, 0x64, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x73, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x6C, 0x79, 0x20, 0x69, 0x6E, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x20, 0x70, 0x6F, 0x6F, 0x6C, 0x20, 0x62, 0x79, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x50, - 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x42, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, - 0x61, 0x74, 0x74, 0x65, 0x72, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x5B, 0x41, 0x2D, 0x5A, 0x30, 0x2D, - 0x39, 0x5D, 0x7B, 0x35, 0x38, 0x7D, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x41, 0x6E, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x70, 0x75, - 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x61, 0x64, 0x64, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x74, 0x72, 0x75, 0x65, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6D, 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x22, 0x3A, 0x20, 0x30, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, - 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x75, 0x6E, 0x63, - 0x61, 0x74, 0x65, 0x64, 0x20, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x74, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x74, 0x6F, 0x20, 0x64, - 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x2E, 0x20, 0x49, 0x66, 0x20, 0x6D, 0x61, 0x78, 0x3D, 0x30, - 0x2C, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x61, 0x6C, 0x6C, 0x20, 0x70, 0x65, - 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x78, 0x6E, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x6D, 0x61, 0x78, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, 0x65, 0x72, - 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x54, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, - 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x74, - 0x65, 0x72, 0x6E, 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, - 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, - 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x33, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x55, 0x6E, 0x61, 0x76, 0x61, 0x69, 0x6C, 0x61, 0x62, - 0x6C, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, - 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x61, 0x73, 0x73, 0x65, 0x74, 0x2F, 0x7B, 0x69, 0x6E, - 0x64, 0x65, 0x78, 0x7D, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x47, 0x69, 0x76, 0x65, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x27, - 0x73, 0x20, 0x75, 0x6E, 0x69, 0x71, 0x75, 0x65, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x2C, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x20, 0x63, 0x61, 0x6C, 0x6C, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x27, 0x73, 0x20, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x6F, 0x72, 0x2C, 0x20, 0x6D, 0x61, 0x6E, 0x61, 0x67, 0x65, 0x72, 0x2C, 0x20, - 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x2C, 0x20, 0x66, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x2C, - 0x20, 0x61, 0x6E, 0x64, 0x20, 0x63, 0x6C, 0x61, 0x77, 0x62, 0x61, 0x63, 0x6B, 0x20, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x5C, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, - 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, - 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, - 0x65, 0x74, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, - 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, - 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, - 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x70, - 0x61, 0x74, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x74, 0x72, - 0x75, 0x65, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x42, 0x61, 0x64, 0x20, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, - 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x74, 0x65, 0x72, 0x6E, 0x61, - 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, - 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, - 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, - 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x75, 0x70, - 0x20, 0x74, 0x6F, 0x20, 0x60, 0x6D, 0x61, 0x78, 0x60, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, - 0x2C, 0x20, 0x77, 0x68, 0x65, 0x72, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6D, 0x61, 0x78, 0x69, - 0x6D, 0x75, 0x6D, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x78, 0x20, 0x69, 0x73, 0x20, - 0x5C, 0x75, 0x30, 0x30, 0x33, 0x63, 0x3D, 0x20, 0x60, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, - 0x78, 0x60, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, - 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, - 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, - 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, - 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x4C, 0x69, 0x73, 0x74, 0x20, 0x61, 0x73, 0x73, - 0x65, 0x74, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, - 0x73, 0x65, 0x74, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6D, 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x22, - 0x3A, 0x20, 0x30, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, - 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x46, - 0x65, 0x74, 0x63, 0x68, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x20, 0x77, 0x69, 0x74, 0x68, - 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x20, 0x5C, 0x75, 0x30, - 0x30, 0x33, 0x63, 0x3D, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x78, 0x2E, 0x20, 0x49, - 0x66, 0x20, 0x7A, 0x65, 0x72, 0x6F, 0x2C, 0x20, 0x66, 0x65, 0x74, 0x63, 0x68, 0x20, 0x6D, 0x6F, - 0x73, 0x74, 0x20, 0x72, 0x65, 0x63, 0x65, 0x6E, 0x74, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, - 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, - 0x78, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6D, 0x61, 0x78, 0x69, 0x6D, 0x75, 0x6D, 0x22, 0x3A, 0x20, 0x31, 0x30, - 0x30, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x6D, 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x22, 0x3A, 0x20, 0x30, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, - 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x65, 0x74, 0x63, 0x68, 0x20, 0x6E, 0x6F, 0x20, - 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x6E, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x6D, - 0x61, 0x6E, 0x79, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x6D, 0x61, 0x78, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, 0x65, 0x72, 0x79, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, - 0x73, 0x65, 0x73, 0x2F, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, - 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x30, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x42, 0x61, 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, - 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, - 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, - 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, 0x6E, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, - 0x74, 0x65, 0x72, 0x6E, 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, - 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, - 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, - 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, - 0x2F, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x2F, 0x7B, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x7D, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, - 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, - 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, - 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, - 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x6C, 0x6F, - 0x63, 0x6B, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x67, 0x69, 0x76, 0x65, 0x6E, - 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, - 0x20, 0x22, 0x47, 0x65, 0x74, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, - 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6D, 0x69, - 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x22, 0x3A, 0x20, 0x30, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x66, - 0x72, 0x6F, 0x6D, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x74, 0x6F, 0x20, 0x66, 0x65, 0x74, - 0x63, 0x68, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x72, 0x6F, 0x75, - 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, - 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x74, 0x72, 0x75, 0x65, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, - 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, - 0x65, 0x74, 0x75, 0x72, 0x6E, 0x20, 0x72, 0x61, 0x77, 0x20, 0x6D, 0x73, 0x67, 0x70, 0x61, 0x63, - 0x6B, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x62, 0x79, 0x74, 0x65, 0x73, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x72, 0x61, 0x77, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, - 0x65, 0x72, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, - 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x52, 0x65, 0x73, 0x70, - 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, - 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x42, 0x61, 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, - 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, - 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, - 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x49, 0x6E, 0x74, 0x65, 0x72, 0x6E, 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, - 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, - 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, - 0x76, 0x31, 0x2F, 0x6C, 0x65, 0x64, 0x67, 0x65, 0x72, 0x2F, 0x73, 0x75, 0x70, 0x70, 0x6C, 0x79, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, - 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, - 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, - 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, - 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, - 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6C, 0x79, 0x20, 0x72, 0x65, - 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x65, - 0x64, 0x67, 0x65, 0x72, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, - 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, 0x6C, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, - 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x53, 0x75, 0x70, 0x70, 0x6C, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, - 0x30, 0x31, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, - 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, - 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, - 0x76, 0x31, 0x2F, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, - 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, - 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, - 0x47, 0x65, 0x74, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, - 0x20, 0x6E, 0x6F, 0x64, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, - 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, - 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x74, 0x65, 0x72, 0x6E, - 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x2F, 0x77, 0x61, 0x69, 0x74, 0x2D, 0x66, 0x6F, 0x72, 0x2D, 0x62, 0x6C, 0x6F, - 0x63, 0x6B, 0x2D, 0x61, 0x66, 0x74, 0x65, 0x72, 0x2F, 0x7B, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x7D, - 0x2F, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x57, 0x61, 0x69, - 0x74, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x74, - 0x6F, 0x20, 0x61, 0x70, 0x70, 0x65, 0x61, 0x72, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x72, - 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x7B, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x7D, 0x20, 0x61, 0x6E, 0x64, - 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x6F, 0x64, - 0x65, 0x27, 0x73, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x61, 0x74, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x74, 0x69, 0x6D, 0x65, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x6F, 0x64, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x77, 0x61, 0x69, 0x74, 0x69, 0x6E, 0x67, 0x20, - 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x67, 0x69, 0x76, 0x65, 0x6E, 0x20, 0x72, 0x6F, - 0x75, 0x6E, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x57, - 0x61, 0x69, 0x74, 0x46, 0x6F, 0x72, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6D, - 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x22, 0x3A, 0x20, 0x30, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, - 0x74, 0x6F, 0x20, 0x77, 0x61, 0x69, 0x74, 0x20, 0x75, 0x6E, 0x74, 0x69, 0x6C, 0x20, 0x72, 0x65, - 0x74, 0x75, 0x72, 0x6E, 0x69, 0x6E, 0x67, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, - 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, - 0x74, 0x72, 0x75, 0x65, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, - 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, - 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x42, 0x61, 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, - 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, - 0x31, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, - 0x6B, 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x49, 0x6E, 0x74, 0x65, 0x72, 0x6E, 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, - 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, - 0x30, 0x33, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, - 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x74, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x7B, 0x74, 0x78, 0x69, 0x64, - 0x7D, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x74, - 0x75, 0x72, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, - 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x67, 0x69, 0x76, 0x65, 0x6E, 0x20, 0x74, 0x78, - 0x69, 0x64, 0x2E, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x73, 0x20, 0x6F, 0x6E, 0x6C, 0x79, 0x20, 0x69, - 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x65, 0x72, 0x20, 0x69, 0x73, - 0x20, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, - 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, - 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, - 0x65, 0x74, 0x20, 0x61, 0x6E, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, - 0x6E, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6E, 0x67, 0x6C, 0x65, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, - 0x64, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, - 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x5B, - 0x41, 0x2D, 0x5A, 0x30, 0x2D, 0x39, 0x5D, 0x2B, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x20, 0x69, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x74, 0x78, 0x69, - 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, - 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x74, 0x72, 0x75, 0x65, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, - 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x54, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x30, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x42, 0x61, - 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, - 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x76, - 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x34, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x4E, 0x6F, 0x74, 0x20, 0x46, 0x6F, 0x75, 0x6E, - 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, - 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x6F, - 0x73, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x63, 0x6F, 0x6E, 0x73, 0x75, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6F, 0x6E, 0x2F, 0x78, 0x2D, 0x62, 0x69, 0x6E, 0x61, 0x72, 0x79, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x42, 0x72, 0x6F, 0x61, - 0x64, 0x63, 0x61, 0x73, 0x74, 0x73, 0x20, 0x61, 0x20, 0x72, 0x61, 0x77, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x6E, 0x65, 0x74, 0x77, 0x6F, 0x72, 0x6B, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, - 0x3A, 0x20, 0x22, 0x52, 0x61, 0x77, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, - 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x62, 0x79, 0x74, 0x65, 0x20, 0x65, 0x6E, 0x63, - 0x6F, 0x64, 0x65, 0x64, 0x20, 0x73, 0x69, 0x67, 0x6E, 0x65, 0x64, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x74, 0x6F, 0x20, 0x62, 0x72, 0x6F, 0x61, 0x64, - 0x63, 0x61, 0x73, 0x74, 0x20, 0x74, 0x6F, 0x20, 0x6E, 0x65, 0x74, 0x77, 0x6F, 0x72, 0x6B, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x72, 0x61, 0x77, 0x74, 0x78, 0x6E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x62, 0x6F, 0x64, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, - 0x3A, 0x20, 0x74, 0x72, 0x75, 0x65, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x69, 0x6E, 0x61, 0x72, 0x79, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, - 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, - 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, - 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x44, 0x52, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x34, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x42, 0x61, 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, - 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, - 0x30, 0x31, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, - 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, - 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x49, 0x6E, 0x74, 0x65, 0x72, 0x6E, 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x35, 0x30, 0x33, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x55, 0x6E, 0x61, 0x76, - 0x61, 0x69, 0x6C, 0x61, 0x62, 0x6C, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, - 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x66, 0x65, 0x65, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, - 0x64, 0x20, 0x46, 0x65, 0x65, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x65, - 0x64, 0x20, 0x69, 0x6E, 0x20, 0x75, 0x6E, 0x69, 0x74, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x6D, 0x69, - 0x63, 0x72, 0x6F, 0x2D, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, 0x70, 0x65, 0x72, 0x20, 0x62, 0x79, - 0x74, 0x65, 0x2E, 0x20, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x46, 0x65, - 0x65, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x66, 0x61, 0x6C, 0x6C, 0x20, 0x74, 0x6F, 0x20, 0x7A, 0x65, - 0x72, 0x6F, 0x20, 0x62, 0x75, 0x74, 0x20, 0x73, 0x75, 0x62, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, - 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x6D, 0x75, - 0x73, 0x74, 0x20, 0x73, 0x74, 0x69, 0x6C, 0x6C, 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, 0x61, 0x20, - 0x66, 0x65, 0x65, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x74, 0x20, 0x6C, 0x65, 0x61, 0x73, 0x74, 0x20, - 0x4D, 0x69, 0x6E, 0x54, 0x78, 0x6E, 0x46, 0x65, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x6E, 0x65, 0x74, 0x77, 0x6F, 0x72, - 0x6B, 0x20, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x2E, 0x5C, 0x6E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, - 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, - 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, - 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, - 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, - 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x75, 0x67, 0x67, 0x65, - 0x73, 0x74, 0x65, 0x64, 0x20, 0x66, 0x65, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, - 0x3A, 0x20, 0x22, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x46, 0x65, 0x65, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, - 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x54, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, - 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x33, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x55, 0x6E, 0x61, 0x76, 0x61, 0x69, 0x6C, 0x61, - 0x62, 0x6C, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, - 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, - 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, - 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, - 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, - 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, - 0x20, 0x66, 0x6F, 0x72, 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6E, - 0x67, 0x20, 0x61, 0x20, 0x6E, 0x65, 0x77, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, - 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, 0x54, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x34, 0x30, 0x31, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, - 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, - 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x2F, 0x76, 0x31, 0x2F, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x2F, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x69, 0x73, 0x74, - 0x20, 0x6F, 0x66, 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2C, 0x20, 0x73, 0x6F, 0x72, 0x74, 0x65, 0x64, - 0x20, 0x62, 0x79, 0x20, 0x70, 0x72, 0x69, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x2C, 0x20, 0x69, 0x6E, - 0x20, 0x64, 0x65, 0x63, 0x72, 0x65, 0x61, 0x73, 0x69, 0x6E, 0x67, 0x20, 0x6F, 0x72, 0x64, 0x65, - 0x72, 0x2C, 0x20, 0x74, 0x72, 0x75, 0x6E, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x61, 0x74, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x65, 0x6E, 0x64, 0x20, 0x61, 0x74, 0x20, 0x4D, 0x41, 0x58, 0x2E, 0x20, - 0x49, 0x66, 0x20, 0x4D, 0x41, 0x58, 0x20, 0x3D, 0x20, 0x30, 0x2C, 0x20, 0x72, 0x65, 0x74, 0x75, - 0x72, 0x6E, 0x73, 0x20, 0x61, 0x6C, 0x6C, 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, - 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2E, 0x5C, 0x6E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, - 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, - 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, - 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, 0x6D, 0x61, 0x72, - 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x61, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, - 0x6F, 0x66, 0x20, 0x75, 0x6E, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x72, 0x6D, 0x65, 0x64, 0x20, 0x74, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x63, 0x75, 0x72, 0x72, - 0x65, 0x6E, 0x74, 0x6C, 0x79, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x70, 0x6F, 0x6F, 0x6C, 0x2E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x50, 0x65, 0x6E, 0x64, - 0x69, 0x6E, 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, 0x6D, - 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x6D, 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x22, 0x3A, 0x20, 0x30, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x75, 0x6E, 0x63, 0x61, - 0x74, 0x65, 0x64, 0x20, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x74, 0x6F, 0x20, 0x64, 0x69, - 0x73, 0x70, 0x6C, 0x61, 0x79, 0x2E, 0x20, 0x49, 0x66, 0x20, 0x6D, 0x61, 0x78, 0x3D, 0x30, 0x2C, - 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x73, 0x20, 0x61, 0x6C, 0x6C, 0x20, 0x70, 0x65, 0x6E, - 0x64, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x78, 0x6E, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x6D, 0x61, 0x78, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x71, 0x75, 0x65, 0x72, 0x79, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, - 0x73, 0x65, 0x73, 0x2F, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x76, - 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, 0x6E, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x49, 0x6E, 0x74, 0x65, - 0x72, 0x6E, 0x61, 0x6C, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x33, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x55, 0x6E, 0x61, 0x76, 0x61, 0x69, 0x6C, 0x61, 0x62, 0x6C, - 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, - 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x2F, 0x76, 0x31, 0x2F, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x2F, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x2F, 0x7B, 0x74, 0x78, 0x69, 0x64, - 0x7D, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x74, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x69, 0x76, - 0x65, 0x6E, 0x20, 0x61, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x20, 0x69, 0x64, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x20, 0x72, 0x65, 0x63, 0x65, 0x6E, 0x74, 0x6C, - 0x79, 0x20, 0x73, 0x75, 0x62, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2C, 0x20, 0x69, 0x74, 0x20, 0x72, 0x65, 0x74, 0x75, - 0x72, 0x6E, 0x73, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, - 0x61, 0x62, 0x6F, 0x75, 0x74, 0x20, 0x69, 0x74, 0x2E, 0x20, 0x20, 0x54, 0x68, 0x65, 0x72, 0x65, - 0x20, 0x61, 0x72, 0x65, 0x20, 0x73, 0x65, 0x76, 0x65, 0x72, 0x61, 0x6C, 0x20, 0x63, 0x61, 0x73, - 0x65, 0x73, 0x20, 0x77, 0x68, 0x65, 0x6E, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x6D, 0x69, 0x67, - 0x68, 0x74, 0x20, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x3A, 0x5C, 0x6E, 0x2D, 0x20, 0x74, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x63, 0x6F, 0x6D, 0x6D, 0x69, - 0x74, 0x74, 0x65, 0x64, 0x20, 0x28, 0x63, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, 0x20, - 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x5C, 0x75, 0x30, 0x30, 0x33, 0x65, 0x20, 0x30, 0x29, 0x20, - 0x2D, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x73, 0x74, - 0x69, 0x6C, 0x6C, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x6F, 0x6F, 0x6C, 0x20, - 0x28, 0x63, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, - 0x20, 0x3D, 0x20, 0x30, 0x2C, 0x20, 0x70, 0x6F, 0x6F, 0x6C, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, - 0x20, 0x3D, 0x20, 0x5C, 0x22, 0x5C, 0x22, 0x29, 0x20, 0x2D, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x72, 0x65, 0x6D, 0x6F, 0x76, 0x65, 0x64, 0x20, 0x66, - 0x72, 0x6F, 0x6D, 0x20, 0x70, 0x6F, 0x6F, 0x6C, 0x20, 0x64, 0x75, 0x65, 0x20, 0x74, 0x6F, 0x20, - 0x65, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x28, 0x63, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, - 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x3D, 0x20, 0x30, 0x2C, 0x20, 0x70, 0x6F, 0x6F, 0x6C, - 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x21, 0x3D, 0x20, 0x5C, 0x22, 0x5C, 0x22, 0x29, 0x5C, - 0x6E, 0x4F, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, 0x68, 0x61, 0x70, - 0x70, 0x65, 0x6E, 0x65, 0x64, 0x20, 0x73, 0x75, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6E, 0x74, - 0x6C, 0x79, 0x20, 0x6C, 0x6F, 0x6E, 0x67, 0x20, 0x61, 0x67, 0x6F, 0x20, 0x74, 0x68, 0x61, 0x74, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x6F, 0x64, 0x65, 0x20, 0x6E, 0x6F, 0x20, 0x6C, 0x6F, 0x6E, - 0x67, 0x65, 0x72, 0x20, 0x72, 0x65, 0x6D, 0x65, 0x6D, 0x62, 0x65, 0x72, 0x73, 0x20, 0x69, 0x74, - 0x2C, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, - 0x72, 0x65, 0x74, 0x75, 0x72, 0x6E, 0x20, 0x61, 0x6E, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x2E, - 0x5C, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, - 0x6F, 0x64, 0x75, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, - 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, - 0x6D, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6D, - 0x6D, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x20, 0x61, 0x20, 0x73, 0x70, - 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x74, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, - 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x54, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, - 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x5B, 0x41, 0x2D, 0x5A, 0x30, 0x2D, 0x39, 0x5D, 0x2B, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x74, 0x78, 0x69, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x74, 0x72, 0x75, 0x65, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, - 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, - 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x30, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x42, 0x61, 0x64, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, - 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x31, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x41, 0x50, 0x49, 0x20, 0x54, 0x6F, 0x6B, 0x65, - 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x34, 0x30, 0x34, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x4E, 0x6F, 0x74, 0x20, 0x46, - 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x35, 0x30, 0x33, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, - 0x55, 0x6E, 0x61, 0x76, 0x61, 0x69, 0x6C, 0x61, 0x62, 0x6C, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, - 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, - 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, - 0x6C, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x6B, 0x6E, 0x6F, 0x77, 0x6E, 0x20, 0x45, 0x72, 0x72, 0x6F, 0x72, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x2F, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x67, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, - 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, - 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, - 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x65, 0x73, 0x22, - 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, - 0x74, 0x74, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x32, 0x30, 0x30, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, - 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x73, 0x2F, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x41, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x44, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x72, 0x65, 0x77, - 0x61, 0x72, 0x64, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x77, 0x69, 0x74, 0x68, 0x6F, 0x75, 0x74, 0x70, 0x65, 0x6E, - 0x64, 0x69, 0x6E, 0x67, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x70, 0x75, - 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x6D, 0x6F, 0x75, - 0x6E, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x41, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x20, 0x6E, 0x75, 0x6D, 0x62, - 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, - 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x77, 0x69, 0x74, 0x68, 0x6F, 0x75, 0x74, 0x70, 0x65, - 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6D, 0x6F, 0x75, 0x6E, - 0x74, 0x57, 0x69, 0x74, 0x68, 0x6F, 0x75, 0x74, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x52, - 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x4D, - 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, 0x69, 0x6E, 0x5C, 0x6E, 0x74, 0x68, - 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x2C, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6F, - 0x75, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x72, - 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, - 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, - 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6D, 0x6F, - 0x75, 0x6E, 0x74, 0x57, 0x69, 0x74, 0x68, 0x6F, 0x75, 0x74, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, - 0x67, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, - 0x70, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x4C, 0x6F, - 0x63, 0x61, 0x6C, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x6D, - 0x61, 0x70, 0x20, 0x6F, 0x66, 0x20, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x20, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x69, 0x73, 0x5C, 0x6E, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, - 0x74, 0x20, 0x68, 0x61, 0x73, 0x20, 0x6F, 0x70, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x74, - 0x6F, 0x2C, 0x20, 0x61, 0x73, 0x20, 0x77, 0x65, 0x6C, 0x6C, 0x20, 0x61, 0x73, 0x20, 0x61, 0x20, - 0x63, 0x6F, 0x70, 0x79, 0x20, 0x6F, 0x66, 0x20, 0x65, 0x61, 0x63, 0x68, 0x20, 0x61, 0x70, 0x70, - 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x27, 0x73, 0x5C, 0x6E, 0x4C, 0x6F, 0x63, 0x61, - 0x6C, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x4C, 0x6F, 0x63, 0x61, 0x6C, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x70, 0x61, - 0x72, 0x61, 0x6D, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x20, 0x69, 0x73, 0x20, - 0x61, 0x20, 0x6D, 0x61, 0x70, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x20, - 0x66, 0x6F, 0x72, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x20, 0x74, 0x68, 0x61, 0x74, 0x5C, 0x6E, 0x77, 0x65, 0x72, 0x65, 0x20, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x63, 0x63, 0x6F, - 0x75, 0x6E, 0x74, 0x2E, 0x20, 0x54, 0x68, 0x65, 0x73, 0x65, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6D, - 0x65, 0x74, 0x65, 0x72, 0x73, 0x20, 0x69, 0x6E, 0x63, 0x6C, 0x75, 0x64, 0x65, 0x20, 0x74, 0x68, - 0x65, 0x5C, 0x6E, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x27, 0x73, - 0x20, 0x67, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x6D, 0x61, - 0x70, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, - 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x50, - 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x73, 0x73, 0x65, 0x74, - 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x6F, 0x6C, 0x64, 0x69, 0x6E, 0x67, 0x73, 0x20, 0x6F, 0x66, - 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, - 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x2C, 0x5C, 0x6E, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x65, - 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x49, - 0x44, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, - 0x70, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, - 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x50, 0x61, 0x72, 0x74, - 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, - 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x65, 0x6E, 0x64, - 0x69, 0x6E, 0x67, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, - 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x20, - 0x6F, 0x66, 0x20, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, 0x6F, 0x66, - 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x5C, 0x6E, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, - 0x73, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, - 0x74, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, - 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x52, - 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x77, 0x61, - 0x72, 0x64, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x20, 0x72, 0x65, - 0x77, 0x61, 0x72, 0x64, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, - 0x67, 0x6F, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, - 0x68, 0x61, 0x73, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x2C, 0x20, 0x69, 0x6E, - 0x63, 0x6C, 0x75, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, - 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, - 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, - 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, - 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x6F, 0x75, 0x6E, 0x64, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, - 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x77, 0x68, 0x69, - 0x63, 0x68, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x6C, 0x65, 0x76, 0x61, 0x6E, 0x74, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x69, 0x6E, 0x64, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x6C, 0x65, 0x67, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x27, 0x73, 0x20, 0x4D, 0x69, 0x63, - 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x5C, 0x6E, 0x4F, 0x66, 0x66, 0x6C, 0x69, 0x6E, 0x65, - 0x20, 0x2D, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x61, - 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x6F, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, - 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x69, 0x73, 0x20, 0x64, 0x65, 0x6C, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x64, 0x2E, 0x5C, 0x6E, 0x4F, 0x6E, 0x6C, 0x69, 0x6E, 0x65, 0x20, 0x20, - 0x2D, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x6F, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x20, - 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x75, 0x73, 0x65, 0x64, 0x20, 0x61, 0x73, 0x20, - 0x70, 0x61, 0x72, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x6C, 0x65, - 0x67, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x70, 0x6F, 0x6F, 0x6C, 0x2E, 0x5C, 0x6E, 0x4E, 0x6F, - 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6E, 0x67, 0x20, 0x2D, - 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x6F, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x20, 0x61, - 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x69, 0x73, 0x20, 0x6E, 0x65, 0x69, 0x74, 0x68, 0x65, - 0x72, 0x20, 0x61, 0x20, 0x64, 0x65, 0x6C, 0x65, 0x67, 0x61, 0x74, 0x6F, 0x72, 0x20, 0x6E, 0x6F, - 0x72, 0x20, 0x61, 0x20, 0x64, 0x65, 0x6C, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x68, 0x69, 0x73, 0x61, 0x73, 0x73, 0x65, 0x74, 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, - 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x20, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x63, 0x63, 0x6F, - 0x75, 0x6E, 0x74, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, - 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, - 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, - 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, - 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, 0x70, 0x70, 0x6C, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x43, 0x61, 0x6C, 0x6C, 0x54, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, - 0x43, 0x61, 0x6C, 0x6C, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, - 0x79, 0x70, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x66, 0x69, 0x65, 0x6C, - 0x64, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x6E, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x43, 0x61, 0x6C, 0x6C, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, - 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x64, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x63, 0x63, 0x6F, 0x75, - 0x6E, 0x74, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x65, 0x69, 0x67, 0x6E, 0x61, 0x70, 0x70, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x65, 0x69, 0x67, 0x6E, 0x61, 0x73, 0x73, - 0x65, 0x74, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, - 0x70, 0x70, 0x61, 0x72, 0x67, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x61, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x70, 0x72, 0x6F, 0x67, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6C, 0x65, 0x61, 0x72, 0x70, 0x72, 0x6F, - 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x63, 0x63, 0x6F, 0x75, - 0x6E, 0x74, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x41, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x73, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x73, 0x20, 0x28, 0x69, - 0x6E, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x73, 0x65, 0x6E, 0x64, 0x65, 0x72, 0x29, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6D, - 0x61, 0x79, 0x20, 0x62, 0x65, 0x5C, 0x6E, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x20, - 0x66, 0x72, 0x6F, 0x6D, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x27, 0x73, 0x20, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x61, 0x6C, 0x50, - 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x43, 0x6C, 0x65, 0x61, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x2E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 0x6D, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x41, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x61, 0x70, 0x70, 0x61, 0x72, 0x67, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, - 0x6E, 0x41, 0x72, 0x67, 0x73, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x73, 0x20, 0x73, 0x6F, 0x6D, 0x65, - 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2D, 0x73, 0x70, 0x65, - 0x63, 0x69, 0x66, 0x69, 0x63, 0x20, 0x61, 0x72, 0x67, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x73, 0x20, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x69, 0x62, 0x6C, 0x65, 0x5C, 0x6E, 0x66, 0x72, 0x6F, 0x6D, - 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x6C, 0x6F, 0x67, - 0x69, 0x63, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 0x6D, 0x73, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x41, 0x72, 0x67, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x72, - 0x6F, 0x76, 0x70, 0x72, 0x6F, 0x67, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x61, 0x6C, 0x50, 0x72, 0x6F, - 0x67, 0x72, 0x61, 0x6D, 0x20, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6D, 0x69, 0x6E, 0x65, 0x73, 0x20, - 0x77, 0x68, 0x65, 0x74, 0x68, 0x65, 0x72, 0x20, 0x6F, 0x72, 0x20, 0x6E, 0x6F, 0x74, 0x20, 0x74, - 0x68, 0x69, 0x73, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x43, - 0x61, 0x6C, 0x6C, 0x5C, 0x6E, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, 0x62, 0x65, 0x20, 0x61, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x65, - 0x64, 0x20, 0x6F, 0x72, 0x20, 0x6E, 0x6F, 0x74, 0x2E, 0x20, 0x49, 0x74, 0x20, 0x64, 0x6F, 0x65, - 0x73, 0x20, 0x6E, 0x6F, 0x74, 0x20, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x20, 0x77, 0x68, - 0x65, 0x6E, 0x5C, 0x6E, 0x4F, 0x6E, 0x43, 0x6F, 0x6D, 0x70, 0x6C, 0x65, 0x74, 0x69, 0x6F, 0x6E, - 0x20, 0x3D, 0x3D, 0x20, 0x43, 0x6C, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4F, 0x43, - 0x2C, 0x20, 0x62, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x63, 0x6C, 0x65, 0x61, 0x72, 0x69, - 0x6E, 0x67, 0x20, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x69, - 0x73, 0x20, 0x61, 0x6C, 0x77, 0x61, 0x79, 0x73, 0x5C, 0x6E, 0x61, 0x6C, 0x6C, 0x6F, 0x77, 0x65, - 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x61, - 0x6C, 0x50, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6C, - 0x65, 0x61, 0x72, 0x70, 0x72, 0x6F, 0x67, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x50, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x73, - 0x20, 0x77, 0x68, 0x65, 0x6E, 0x20, 0x61, 0x6E, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x43, 0x61, 0x6C, 0x6C, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x5C, 0x6E, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x73, 0x20, 0x77, - 0x69, 0x74, 0x68, 0x20, 0x4F, 0x6E, 0x43, 0x6F, 0x6D, 0x70, 0x6C, 0x65, 0x74, 0x69, 0x6F, 0x6E, - 0x20, 0x3D, 0x3D, 0x20, 0x43, 0x6C, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4F, 0x43, - 0x2E, 0x20, 0x48, 0x6F, 0x77, 0x65, 0x76, 0x65, 0x72, 0x2C, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, - 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x5C, 0x6E, 0x6D, 0x61, 0x79, 0x20, 0x6E, 0x6F, 0x74, - 0x20, 0x72, 0x65, 0x6A, 0x65, 0x63, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x6F, 0x6E, 0x6C, 0x79, 0x20, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x29, 0x2E, 0x20, 0x49, 0x66, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6F, - 0x67, 0x72, 0x61, 0x6D, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x65, 0x69, 0x67, - 0x6E, 0x61, 0x70, 0x70, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x46, 0x6F, 0x72, 0x65, 0x69, 0x67, 0x6E, 0x41, 0x70, 0x70, 0x73, 0x20, - 0x6C, 0x69, 0x73, 0x74, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x28, 0x69, 0x6E, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, - 0x69, 0x6F, 0x6E, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x78, 0x6E, 0x2E, 0x41, 0x70, 0x70, 0x6C, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x44, 0x29, 0x5C, 0x6E, 0x77, 0x68, 0x6F, 0x73, 0x65, - 0x20, 0x67, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x20, 0x6D, - 0x61, 0x79, 0x20, 0x62, 0x65, 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x20, 0x62, - 0x79, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x27, 0x73, 0x5C, 0x6E, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x61, 0x6C, 0x50, 0x72, - 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x43, 0x6C, 0x65, 0x61, 0x72, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x2E, 0x20, 0x54, 0x68, 0x65, - 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x61, 0x64, 0x2D, - 0x6F, 0x6E, 0x6C, 0x79, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, - 0x65, 0x6D, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, - 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, - 0x6E, 0x74, 0x36, 0x34, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x6F, 0x72, 0x65, 0x69, - 0x67, 0x6E, 0x41, 0x70, 0x70, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x65, - 0x69, 0x67, 0x6E, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x6F, 0x72, 0x65, 0x69, 0x67, 0x6E, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x73, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x20, 0x77, 0x68, 0x6F, 0x73, 0x65, 0x20, 0x70, 0x61, 0x72, - 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x62, 0x65, 0x20, 0x61, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x20, 0x62, 0x79, 0x5C, 0x6E, 0x74, 0x68, 0x69, 0x73, - 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x27, 0x73, 0x20, 0x41, - 0x70, 0x70, 0x72, 0x6F, 0x76, 0x61, 0x6C, 0x50, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x61, - 0x6E, 0x64, 0x20, 0x43, 0x6C, 0x65, 0x61, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6F, - 0x67, 0x72, 0x61, 0x6D, 0x2E, 0x20, 0x54, 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x5C, 0x6E, 0x69, 0x73, 0x20, 0x72, 0x65, 0x61, 0x64, 0x2D, 0x6F, 0x6E, 0x6C, 0x79, 0x2E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 0x6D, 0x73, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, - 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x6F, 0x72, 0x65, 0x69, 0x67, 0x6E, 0x41, 0x73, 0x73, 0x65, - 0x74, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x73, 0x63, - 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, - 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, - 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x64, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, 0x70, 0x6C, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x44, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x62, 0x65, 0x69, - 0x6E, 0x67, 0x20, 0x69, 0x6E, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, - 0x74, 0x68, 0x2C, 0x20, 0x6F, 0x72, 0x20, 0x30, 0x20, 0x69, 0x66, 0x5C, 0x6E, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x69, 0x6E, 0x67, 0x20, 0x61, 0x20, 0x6E, 0x65, 0x77, 0x20, 0x61, 0x70, 0x70, 0x6C, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, - 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, - 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x70, - 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x44, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, - 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, - 0x73, 0x2F, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x6F, 0x6E, 0x63, 0x6F, 0x6D, 0x70, 0x6C, 0x65, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4F, 0x6E, - 0x43, 0x6F, 0x6D, 0x70, 0x6C, 0x65, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, - 0x66, 0x69, 0x65, 0x73, 0x20, 0x77, 0x68, 0x61, 0x74, 0x20, 0x73, 0x69, 0x64, 0x65, 0x20, 0x65, - 0x66, 0x66, 0x65, 0x63, 0x74, 0x73, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, 0x68, 0x61, 0x76, - 0x65, 0x5C, 0x6E, 0x69, 0x66, 0x20, 0x69, 0x74, 0x20, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x66, 0x75, 0x6C, 0x6C, 0x79, 0x20, 0x6D, 0x61, 0x6B, 0x65, 0x73, 0x20, 0x69, 0x74, 0x20, 0x69, - 0x6E, 0x74, 0x6F, 0x20, 0x61, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x2E, 0x5C, 0x6E, 0x5C, 0x6E, - 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x3A, 0x20, 0x74, 0x72, 0x75, 0x65, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x4F, 0x6E, 0x43, 0x6F, 0x6D, 0x70, 0x6C, 0x65, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, - 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, - 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, - 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, - 0x73, 0x20, 0x62, 0x6F, 0x74, 0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x75, 0x6E, 0x69, 0x71, 0x75, - 0x65, 0x20, 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x20, 0x61, 0x6E, 0x64, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x20, - 0x66, 0x6F, 0x72, 0x20, 0x61, 0x6E, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, - 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x6E, 0x64, 0x65, 0x78, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, - 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, - 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x49, 0x6E, 0x64, 0x65, 0x78, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x6E, 0x64, - 0x65, 0x78, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x75, 0x6E, 0x69, 0x71, 0x75, 0x65, - 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, - 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x2F, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, - 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, - 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, - 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, - 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x6F, 0x6E, 0x66, 0x69, 0x67, - 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x20, - 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, - 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, 0x66, - 0x6F, 0x72, 0x20, 0x61, 0x6E, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x63, 0x6F, 0x6E, 0x66, - 0x69, 0x67, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x44, - 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x62, 0x65, - 0x69, 0x6E, 0x67, 0x20, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, 0x20, 0x28, - 0x6F, 0x72, 0x20, 0x65, 0x6D, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x69, 0x6E, 0x67, 0x29, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, - 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, - 0x49, 0x44, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, - 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, - 0x6F, 0x6E, 0x73, 0x2F, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, - 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, - 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, - 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x46, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x54, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x46, 0x72, 0x65, 0x65, 0x7A, - 0x65, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, - 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, - 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, - 0x66, 0x6F, 0x72, 0x20, 0x61, 0x6E, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x66, 0x72, 0x65, - 0x65, 0x7A, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x63, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, - 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x63, 0x63, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, - 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x63, - 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x77, 0x68, 0x65, 0x72, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x73, 0x20, 0x62, 0x65, 0x69, 0x6E, 0x67, 0x20, 0x66, - 0x72, 0x6F, 0x7A, 0x65, 0x6E, 0x20, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x61, 0x77, 0x65, 0x64, 0x2E, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x66, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x77, 0x46, 0x72, 0x65, 0x65, - 0x7A, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, - 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x65, 0x77, 0x20, 0x66, 0x72, 0x65, 0x65, 0x7A, - 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x62, - 0x6F, 0x6F, 0x6C, 0x65, 0x61, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x4E, 0x65, 0x77, 0x46, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x44, 0x20, 0x69, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x62, 0x65, 0x69, 0x6E, - 0x67, 0x20, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, 0x20, 0x28, 0x6F, 0x72, - 0x20, 0x65, 0x6D, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, - 0x6E, 0x67, 0x29, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, - 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x44, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, - 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, - 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, - 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4C, 0x69, 0x73, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4C, 0x69, 0x73, 0x74, 0x20, - 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x61, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, - 0x6F, 0x66, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, - 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, - 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, - 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x20, 0x69, 0x73, 0x20, - 0x61, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 0x6D, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, - 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x41, 0x73, 0x73, 0x65, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, - 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, - 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, - 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, - 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x74, 0x6C, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x20, 0x73, 0x70, - 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x72, 0x61, - 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x6E, 0x20, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x63, 0x69, 0x6D, 0x61, 0x6C, 0x73, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x73, 0x73, 0x65, 0x74, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4E, 0x61, 0x6D, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, - 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x20, 0x6F, - 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x2C, 0x5C, 0x6E, 0x61, - 0x73, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6C, 0x69, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6F, 0x72, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4E, 0x61, 0x6D, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x63, 0x6C, 0x61, 0x77, 0x62, 0x61, 0x63, 0x6B, 0x61, 0x64, 0x64, 0x72, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x61, 0x77, 0x62, - 0x61, 0x63, 0x6B, 0x41, 0x64, 0x64, 0x72, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x75, 0x73, - 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x63, 0x6C, 0x61, 0x77, 0x62, 0x61, 0x63, 0x6B, 0x20, 0x68, - 0x6F, 0x6C, 0x64, 0x69, 0x6E, 0x67, 0x73, 0x20, 0x6F, 0x66, 0x5C, 0x6E, 0x74, 0x68, 0x69, 0x73, - 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x2E, 0x20, 0x20, 0x49, 0x66, 0x20, 0x65, 0x6D, 0x70, 0x74, - 0x79, 0x2C, 0x20, 0x63, 0x6C, 0x61, 0x77, 0x62, 0x61, 0x63, 0x6B, 0x20, 0x69, 0x73, 0x20, 0x6E, - 0x6F, 0x74, 0x20, 0x70, 0x65, 0x72, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, 0x2E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x61, 0x77, 0x62, 0x61, 0x63, 0x6B, 0x41, 0x64, 0x64, 0x72, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6F, 0x72, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x6F, 0x72, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x2E, 0x5C, 0x6E, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x77, 0x68, 0x65, 0x72, 0x65, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x20, 0x66, 0x6F, 0x72, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5C, 0x6E, 0x63, 0x61, 0x6E, - 0x20, 0x62, 0x65, 0x20, 0x66, 0x6F, 0x75, 0x6E, 0x64, 0x2C, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x61, - 0x6C, 0x73, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, - 0x77, 0x68, 0x65, 0x72, 0x65, 0x20, 0x75, 0x6E, 0x77, 0x61, 0x6E, 0x74, 0x65, 0x64, 0x20, 0x61, - 0x73, 0x73, 0x65, 0x74, 0x5C, 0x6E, 0x75, 0x6E, 0x69, 0x74, 0x73, 0x20, 0x63, 0x61, 0x6E, 0x20, - 0x62, 0x65, 0x20, 0x73, 0x65, 0x6E, 0x74, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x77, - 0x6F, 0x72, 0x73, 0x74, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x63, 0x69, 0x6D, 0x61, 0x6C, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x44, 0x65, 0x63, 0x69, 0x6D, 0x61, 0x6C, 0x73, 0x20, 0x73, 0x70, - 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x75, 0x6D, 0x62, - 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x20, 0x74, 0x6F, 0x20, - 0x75, 0x73, 0x65, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, - 0x63, 0x69, 0x6D, 0x61, 0x6C, 0x5C, 0x6E, 0x70, 0x6F, 0x69, 0x6E, 0x74, 0x20, 0x77, 0x68, 0x65, - 0x6E, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x2E, 0x20, 0x49, 0x66, 0x20, 0x30, 0x2C, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x73, 0x20, 0x6E, 0x6F, 0x74, 0x20, - 0x64, 0x69, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6C, 0x65, 0x2E, 0x5C, 0x6E, 0x49, 0x66, 0x20, 0x31, - 0x2C, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x61, 0x73, 0x65, 0x20, 0x75, 0x6E, 0x69, 0x74, 0x20, - 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x73, 0x20, - 0x69, 0x6E, 0x20, 0x74, 0x65, 0x6E, 0x74, 0x68, 0x73, 0x2E, 0x20, 0x49, 0x66, 0x20, 0x32, 0x2C, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x61, 0x73, 0x65, 0x20, 0x75, 0x6E, 0x69, 0x74, 0x5C, 0x6E, - 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x73, 0x20, - 0x69, 0x6E, 0x20, 0x68, 0x75, 0x6E, 0x64, 0x72, 0x65, 0x64, 0x74, 0x68, 0x73, 0x2C, 0x20, 0x61, - 0x6E, 0x64, 0x20, 0x73, 0x6F, 0x20, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, - 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, - 0x6E, 0x74, 0x33, 0x32, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x44, - 0x65, 0x63, 0x69, 0x6D, 0x61, 0x6C, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, - 0x61, 0x75, 0x6C, 0x74, 0x66, 0x72, 0x6F, 0x7A, 0x65, 0x6E, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, - 0x46, 0x72, 0x6F, 0x7A, 0x65, 0x6E, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, - 0x20, 0x77, 0x68, 0x65, 0x74, 0x68, 0x65, 0x72, 0x20, 0x68, 0x6F, 0x6C, 0x64, 0x69, 0x6E, 0x67, - 0x73, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5C, - 0x6E, 0x61, 0x72, 0x65, 0x20, 0x66, 0x72, 0x6F, 0x7A, 0x65, 0x6E, 0x20, 0x62, 0x79, 0x20, 0x64, - 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x6F, 0x6F, - 0x6C, 0x65, 0x61, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x44, - 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x46, 0x72, 0x6F, 0x7A, 0x65, 0x6E, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x66, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x61, 0x64, 0x64, 0x72, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x72, 0x65, 0x65, 0x7A, - 0x65, 0x41, 0x64, 0x64, 0x72, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x75, 0x73, 0x65, 0x64, - 0x20, 0x74, 0x6F, 0x20, 0x66, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x20, 0x68, 0x6F, 0x6C, 0x64, 0x69, - 0x6E, 0x67, 0x73, 0x20, 0x6F, 0x66, 0x5C, 0x6E, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x73, 0x73, - 0x65, 0x74, 0x2E, 0x20, 0x20, 0x49, 0x66, 0x20, 0x65, 0x6D, 0x70, 0x74, 0x79, 0x2C, 0x20, 0x66, - 0x72, 0x65, 0x65, 0x7A, 0x69, 0x6E, 0x67, 0x20, 0x69, 0x73, 0x20, 0x6E, 0x6F, 0x74, 0x20, 0x70, - 0x65, 0x72, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, - 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x46, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x41, 0x64, 0x64, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x6D, 0x61, 0x6E, 0x61, 0x67, 0x65, 0x72, 0x6B, 0x65, 0x79, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4D, 0x61, 0x6E, 0x61, 0x67, 0x65, 0x72, - 0x41, 0x64, 0x64, 0x72, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x75, 0x73, 0x65, 0x64, 0x20, - 0x74, 0x6F, 0x20, 0x6D, 0x61, 0x6E, 0x61, 0x67, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6B, 0x65, - 0x79, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x5C, 0x6E, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x64, 0x65, 0x73, 0x74, 0x72, 0x6F, 0x79, - 0x20, 0x69, 0x74, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4D, 0x61, 0x6E, 0x61, 0x67, - 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6D, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x68, 0x61, 0x73, 0x68, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x48, - 0x61, 0x73, 0x68, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x61, 0x20, - 0x63, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x6D, 0x65, 0x6E, 0x74, 0x20, 0x74, 0x6F, 0x20, 0x73, 0x6F, - 0x6D, 0x65, 0x20, 0x75, 0x6E, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x61, - 0x73, 0x73, 0x65, 0x74, 0x5C, 0x6E, 0x6D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2E, 0x20, - 0x54, 0x68, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, - 0x69, 0x73, 0x20, 0x6D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x69, 0x73, 0x20, 0x75, - 0x70, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, - 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x48, 0x61, 0x73, 0x68, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x61, 0x64, 0x64, 0x72, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x20, - 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x68, 0x6F, 0x6C, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x72, 0x65, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x20, 0x28, 0x6E, 0x6F, 0x6E, 0x2D, 0x6D, 0x69, 0x6E, 0x74, 0x65, - 0x64, 0x29, 0x5C, 0x6E, 0x75, 0x6E, 0x69, 0x74, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x52, - 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, - 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x20, 0x6E, 0x75, - 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x75, 0x6E, 0x69, 0x74, 0x73, 0x20, 0x6F, 0x66, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x2E, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x6E, 0x69, - 0x74, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x69, 0x74, 0x4E, 0x61, 0x6D, 0x65, 0x20, 0x73, 0x70, 0x65, - 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x20, - 0x6F, 0x66, 0x20, 0x61, 0x20, 0x75, 0x6E, 0x69, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x2C, 0x5C, 0x6E, 0x61, 0x73, 0x20, 0x73, 0x75, 0x70, - 0x70, 0x6C, 0x69, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x6F, 0x72, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, - 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, - 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x6E, 0x69, 0x74, - 0x4E, 0x61, 0x6D, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x72, 0x6C, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x52, 0x4C, 0x20, - 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, 0x20, 0x61, 0x20, 0x55, 0x52, 0x4C, 0x20, - 0x77, 0x68, 0x65, 0x72, 0x65, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, - 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x61, 0x62, 0x6F, 0x75, 0x74, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x63, 0x61, 0x6E, 0x20, 0x62, 0x65, 0x5C, 0x6E, 0x72, - 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x55, - 0x52, 0x4C, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, - 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, - 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, - 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, - 0x72, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, - 0x54, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, - 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x6E, 0x20, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x69, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x6D, - 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x63, 0x76, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x6D, 0x74, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6D, 0x6F, 0x75, - 0x6E, 0x74, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, - 0x20, 0x62, 0x65, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x72, - 0x65, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, - 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6C, 0x6F, 0x73, 0x65, 0x74, 0x6F, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x6F, 0x73, 0x65, - 0x54, 0x6F, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6E, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x72, 0x65, 0x6D, 0x61, 0x69, 0x6E, - 0x69, 0x6E, 0x67, 0x20, 0x66, 0x75, 0x6E, 0x64, 0x73, 0x20, 0x28, 0x69, 0x66, 0x20, 0x63, 0x6C, - 0x6F, 0x73, 0x69, 0x6E, 0x67, 0x29, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, - 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, - 0x6F, 0x73, 0x65, 0x54, 0x6F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6C, 0x6F, 0x73, 0x65, - 0x74, 0x6F, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x54, 0x6F, 0x41, 0x6D, - 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x6F, - 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x6D, 0x61, 0x69, 0x6E, 0x69, 0x6E, 0x67, 0x20, - 0x66, 0x75, 0x6E, 0x64, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x77, 0x65, 0x72, 0x65, 0x20, - 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x63, 0x6C, 0x6F, 0x73, 0x65, 0x20, 0x74, 0x6F, 0x20, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x20, 0x28, 0x69, 0x66, 0x20, 0x63, 0x6C, 0x6F, 0x73, 0x69, 0x6E, 0x67, 0x29, - 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x54, 0x6F, 0x41, 0x6D, - 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x64, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, - 0x49, 0x44, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, - 0x62, 0x65, 0x69, 0x6E, 0x67, 0x20, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, - 0x20, 0x28, 0x6F, 0x72, 0x20, 0x65, 0x6D, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x69, 0x6E, 0x67, 0x29, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, - 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, - 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x49, 0x44, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x63, 0x76, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x63, 0x65, - 0x69, 0x76, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x63, 0x69, - 0x70, 0x69, 0x65, 0x6E, 0x74, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x2E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x73, 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x65, 0x6E, 0x64, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x73, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, - 0x74, 0x20, 0x28, 0x69, 0x66, 0x20, 0x75, 0x73, 0x69, 0x6E, 0x67, 0x20, 0x63, 0x6C, 0x61, 0x77, - 0x62, 0x61, 0x63, 0x6B, 0x29, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x65, 0x6E, - 0x64, 0x65, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, - 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, - 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, - 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, - 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, - 0x69, 0x6E, 0x73, 0x20, 0x61, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x69, 0x6E, 0x66, 0x6F, - 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, - 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x50, 0x72, - 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x41, 0x70, - 0x70, 0x72, 0x6F, 0x76, 0x61, 0x6C, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x56, - 0x6F, 0x74, 0x65, 0x42, 0x65, 0x66, 0x6F, 0x72, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, - 0x6C, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x4F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x50, 0x72, 0x6F, 0x70, - 0x6F, 0x73, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, - 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x65, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x61, 0x73, 0x68, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6F, 0x75, - 0x73, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x48, 0x61, 0x73, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x65, 0x65, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x6F, 0x73, 0x65, 0x72, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x65, 0x72, 0x69, 0x6F, - 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x78, 0x6E, - 0x52, 0x6F, 0x6F, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x69, 0x6D, 0x65, 0x73, 0x74, 0x61, 0x6D, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, - 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, - 0x6F, 0x6C, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, - 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x68, 0x61, - 0x74, 0x20, 0x72, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6E, 0x74, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, - 0x6C, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x50, - 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x72, 0x61, - 0x63, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x54, 0x68, 0x65, 0x20, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x6C, 0x65, - 0x66, 0x74, 0x6F, 0x76, 0x65, 0x72, 0x20, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, - 0x73, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x69, 0x73, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x6F, 0x66, 0x20, 0x52, 0x65, 0x77, 0x61, - 0x72, 0x64, 0x73, 0x52, 0x61, 0x74, 0x65, 0x2F, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x55, 0x6E, - 0x69, 0x74, 0x73, 0x5C, 0x6E, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, - 0x66, 0x6F, 0x72, 0x20, 0x65, 0x76, 0x65, 0x72, 0x79, 0x20, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, - 0x20, 0x75, 0x6E, 0x69, 0x74, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x65, 0x78, - 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, - 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, - 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, - 0x77, 0x61, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x69, 0x64, 0x75, 0x65, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x68, 0x61, 0x73, 0x68, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x48, 0x61, 0x73, 0x68, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x68, - 0x61, 0x73, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, - 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x48, 0x61, 0x73, 0x68, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, - 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x69, 0x73, 0x20, 0x61, - 0x20, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x72, 0x65, 0x70, - 0x72, 0x65, 0x73, 0x65, 0x6E, 0x74, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x65, 0x78, 0x74, - 0x20, 0x70, 0x72, 0x6F, 0x70, 0x6F, 0x73, 0x65, 0x64, 0x20, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, - 0x6F, 0x6C, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, - 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x50, - 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x61, 0x6C, 0x73, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, - 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x41, 0x70, 0x70, 0x72, 0x6F, - 0x76, 0x61, 0x6C, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x75, 0x6D, 0x62, - 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x73, 0x20, 0x77, 0x68, 0x69, - 0x63, 0x68, 0x20, 0x61, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, - 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, - 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, - 0x6F, 0x6C, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x61, 0x6C, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x6E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x53, 0x77, 0x69, - 0x74, 0x63, 0x68, 0x4F, 0x6E, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, - 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x4F, 0x6E, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x6F, 0x6E, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x75, 0x70, 0x67, 0x72, - 0x61, 0x64, 0x65, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, 0x74, 0x61, 0x6B, 0x65, 0x20, 0x65, 0x66, - 0x66, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, - 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, - 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x50, 0x72, - 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x4F, 0x6E, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, - 0x56, 0x6F, 0x74, 0x65, 0x42, 0x65, 0x66, 0x6F, 0x72, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, - 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x56, 0x6F, 0x74, 0x65, 0x42, 0x65, 0x66, 0x6F, 0x72, 0x65, 0x20, - 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x61, 0x64, 0x6C, 0x69, 0x6E, 0x65, 0x20, - 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x70, - 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x20, - 0x28, 0x4E, 0x6F, 0x20, 0x76, 0x6F, 0x74, 0x65, 0x73, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, 0x62, - 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x69, 0x64, 0x65, 0x72, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x29, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x56, 0x6F, - 0x74, 0x65, 0x42, 0x65, 0x66, 0x6F, 0x72, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x65, - 0x72, 0x69, 0x6F, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x50, 0x65, 0x72, 0x69, 0x6F, 0x64, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x70, 0x65, 0x72, 0x69, 0x6F, 0x64, 0x20, 0x6F, 0x6E, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x77, 0x61, 0x73, 0x20, 0x63, - 0x6F, 0x6E, 0x66, 0x69, 0x72, 0x6D, 0x65, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, - 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, - 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x65, - 0x72, 0x69, 0x6F, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6F, - 0x75, 0x73, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x48, 0x61, 0x73, 0x68, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6F, - 0x75, 0x73, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x48, 0x61, 0x73, 0x68, 0x20, 0x69, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6F, 0x75, 0x73, 0x20, 0x62, 0x6C, 0x6F, 0x63, - 0x6B, 0x20, 0x68, 0x61, 0x73, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x72, 0x65, - 0x76, 0x69, 0x6F, 0x75, 0x73, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x48, 0x61, 0x73, 0x68, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x6F, 0x73, 0x65, 0x72, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x72, 0x6F, 0x70, 0x6F, - 0x73, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, - 0x20, 0x70, 0x72, 0x6F, 0x70, 0x6F, 0x73, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, - 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x50, 0x72, 0x6F, 0x70, 0x6F, 0x73, 0x65, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x61, - 0x74, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x54, 0x68, 0x65, 0x20, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x6E, - 0x65, 0x77, 0x20, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, 0x61, 0x64, - 0x64, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, - 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x73, 0x74, 0x61, 0x6B, 0x65, 0x20, 0x66, - 0x72, 0x6F, 0x6D, 0x20, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x20, 0x61, 0x74, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x6E, 0x65, 0x78, 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x2E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, - 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x52, 0x61, 0x74, 0x65, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, - 0x73, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x73, - 0x20, 0x68, 0x6F, 0x77, 0x20, 0x6D, 0x61, 0x6E, 0x79, 0x20, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, - 0x73, 0x2C, 0x20, 0x69, 0x6E, 0x20, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, - 0x2C, 0x5C, 0x6E, 0x68, 0x61, 0x76, 0x65, 0x20, 0x62, 0x65, 0x65, 0x6E, 0x20, 0x64, 0x69, 0x73, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x65, 0x61, 0x63, 0x68, - 0x20, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x2E, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, - 0x2E, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x55, 0x6E, 0x69, 0x74, 0x5C, 0x6E, 0x6F, 0x66, 0x20, - 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, 0x73, 0x69, 0x6E, 0x63, 0x65, - 0x20, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, - 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, - 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x52, - 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, - 0x6F, 0x6E, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x62, 0x6C, - 0x6F, 0x63, 0x6B, 0x20, 0x77, 0x61, 0x73, 0x20, 0x61, 0x70, 0x70, 0x65, 0x6E, 0x64, 0x65, 0x64, - 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x68, 0x61, 0x69, 0x6E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, - 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x65, - 0x65, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x53, 0x65, 0x65, 0x64, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x6F, 0x72, - 0x74, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x73, 0x65, 0x65, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x53, 0x65, 0x65, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6D, 0x65, - 0x73, 0x74, 0x61, 0x6D, 0x70, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x54, 0x69, 0x6D, 0x65, 0x53, 0x74, 0x61, 0x6D, 0x70, 0x20, 0x69, 0x6E, - 0x20, 0x73, 0x65, 0x63, 0x6F, 0x6E, 0x64, 0x73, 0x20, 0x73, 0x69, 0x6E, 0x63, 0x65, 0x20, 0x65, - 0x70, 0x6F, 0x63, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, - 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x69, 0x6D, 0x65, 0x73, 0x74, 0x61, - 0x6D, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x78, 0x6E, 0x52, 0x6F, 0x6F, 0x74, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x61, - 0x75, 0x74, 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x73, 0x65, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x61, 0x70, 0x70, 0x65, 0x61, 0x72, 0x69, 0x6E, 0x67, 0x20, 0x69, - 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x2E, 0x5C, 0x6E, 0x4D, 0x6F, - 0x72, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x6C, 0x6C, 0x79, 0x2C, - 0x20, 0x69, 0x74, 0x27, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x6F, 0x6F, 0x74, 0x20, 0x6F, - 0x66, 0x20, 0x61, 0x20, 0x6D, 0x65, 0x72, 0x6B, 0x6C, 0x65, 0x20, 0x74, 0x72, 0x65, 0x65, 0x20, - 0x77, 0x68, 0x6F, 0x73, 0x65, 0x20, 0x6C, 0x65, 0x61, 0x76, 0x65, 0x73, 0x20, 0x61, 0x72, 0x65, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x27, 0x73, 0x20, 0x54, 0x78, 0x69, - 0x64, 0x73, 0x2C, 0x20, 0x69, 0x6E, 0x20, 0x6C, 0x65, 0x78, 0x69, 0x63, 0x6F, 0x67, 0x72, 0x61, - 0x70, 0x68, 0x69, 0x63, 0x20, 0x6F, 0x72, 0x64, 0x65, 0x72, 0x2E, 0x5C, 0x6E, 0x46, 0x6F, 0x72, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6D, 0x70, 0x74, 0x79, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, - 0x2C, 0x20, 0x69, 0x74, 0x27, 0x73, 0x20, 0x30, 0x2E, 0x5C, 0x6E, 0x4E, 0x6F, 0x74, 0x65, 0x20, - 0x74, 0x68, 0x61, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x54, 0x78, 0x6E, 0x43, 0x6F, 0x6D, 0x6D, - 0x69, 0x74, 0x6D, 0x65, 0x6E, 0x74, 0x73, 0x20, 0x64, 0x6F, 0x65, 0x73, 0x20, 0x6E, 0x6F, 0x74, - 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x73, 0x69, 0x67, 0x6E, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x20, 0x6F, 0x6E, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x2C, 0x20, 0x6F, 0x6E, 0x6C, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x6D, 0x73, 0x65, 0x6C, 0x76, - 0x65, 0x73, 0x2E, 0x5C, 0x6E, 0x54, 0x77, 0x6F, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x73, 0x20, - 0x77, 0x69, 0x74, 0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x61, 0x6D, 0x65, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x69, - 0x6E, 0x20, 0x61, 0x20, 0x64, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x6F, 0x72, - 0x64, 0x65, 0x72, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x64, 0x69, 0x66, - 0x66, 0x65, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x73, 0x69, 0x67, 0x6E, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x73, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x73, 0x61, 0x6D, 0x65, 0x20, 0x54, 0x78, 0x6E, 0x43, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x6D, 0x65, - 0x6E, 0x74, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x6F, 0x6F, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x78, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, - 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x4C, 0x69, 0x73, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x65, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x70, 0x67, - 0x72, 0x61, 0x64, 0x65, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x65, 0x20, 0x69, 0x6E, 0x64, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x79, 0x65, 0x73, 0x20, 0x76, 0x6F, 0x74, 0x65, - 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, - 0x20, 0x70, 0x72, 0x6F, 0x70, 0x6F, 0x73, 0x61, 0x6C, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x62, - 0x6F, 0x6F, 0x6C, 0x65, 0x61, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x41, 0x70, 0x70, 0x72, 0x6F, 0x76, 0x65, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x50, 0x72, 0x6F, 0x70, - 0x6F, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x50, 0x72, 0x6F, 0x70, 0x6F, 0x73, 0x65, - 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x70, 0x72, 0x6F, - 0x70, 0x6F, 0x73, 0x65, 0x64, 0x20, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x50, 0x72, 0x6F, 0x70, 0x6F, - 0x73, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, - 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, - 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, - 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x42, 0x75, 0x69, 0x6C, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x74, 0x6C, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x42, 0x75, 0x69, - 0x6C, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, - 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x61, - 0x6C, 0x67, 0x6F, 0x64, 0x20, 0x62, 0x75, 0x69, 0x6C, 0x64, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6D, - 0x61, 0x6A, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x6D, 0x69, 0x6E, 0x6F, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x62, 0x75, 0x69, 0x6C, 0x64, 0x5F, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x5F, - 0x68, 0x61, 0x73, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x62, 0x72, 0x61, 0x6E, 0x63, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x63, 0x68, 0x61, 0x6E, 0x6E, 0x65, 0x6C, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, - 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x62, 0x72, 0x61, 0x6E, 0x63, 0x68, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x42, 0x72, 0x61, 0x6E, 0x63, 0x68, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x62, 0x75, - 0x69, 0x6C, 0x64, 0x5F, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x42, 0x75, 0x69, 0x6C, 0x64, 0x4E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x63, 0x68, 0x61, 0x6E, 0x6E, 0x65, 0x6C, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x43, 0x68, 0x61, 0x6E, 0x6E, 0x65, 0x6C, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, - 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x5F, 0x68, 0x61, 0x73, 0x68, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x43, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x48, 0x61, 0x73, 0x68, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x6D, 0x61, 0x6A, 0x6F, 0x72, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, - 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4D, - 0x61, 0x6A, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6D, 0x69, 0x6E, 0x6F, 0x72, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, - 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4D, 0x69, 0x6E, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, - 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, - 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, - 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, - 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x63, 0x6F, 0x6D, - 0x6D, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x4B, 0x65, 0x79, 0x72, 0x65, 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x4B, 0x65, 0x79, 0x72, 0x65, 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x66, - 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x20, 0x6B, 0x65, 0x79, 0x72, - 0x65, 0x67, 0x20, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x65, 0x6C, 0x6B, 0x65, 0x79, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x65, 0x6C, - 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x50, 0x4B, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x56, 0x52, 0x46, 0x20, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x75, - 0x73, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, - 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x65, 0x6C, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x50, 0x4B, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x6F, 0x74, 0x65, 0x66, 0x73, 0x74, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, - 0x74, 0x65, 0x46, 0x69, 0x72, 0x73, 0x74, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, - 0x69, 0x72, 0x73, 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, - 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x6B, 0x65, - 0x79, 0x20, 0x69, 0x73, 0x20, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, - 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x56, 0x6F, 0x74, 0x65, 0x46, 0x69, 0x72, 0x73, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, - 0x6F, 0x74, 0x65, 0x6B, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x4B, 0x65, 0x79, 0x44, 0x69, 0x6C, 0x75, 0x74, - 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x69, 0x6C, 0x75, 0x74, - 0x69, 0x6F, 0x6E, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x32, 0x2D, 0x6C, 0x65, - 0x76, 0x65, 0x6C, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6F, - 0x6E, 0x20, 0x6B, 0x65, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, - 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x4B, - 0x65, 0x79, 0x44, 0x69, 0x6C, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x76, 0x6F, 0x74, 0x65, 0x6B, 0x65, 0x79, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x50, 0x4B, 0x20, 0x69, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6F, - 0x6E, 0x20, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x75, 0x73, 0x65, - 0x64, 0x20, 0x69, 0x6E, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x50, 0x4B, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x76, 0x6F, 0x74, 0x65, 0x6C, 0x73, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x4C, 0x61, 0x73, 0x74, 0x20, - 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x61, 0x73, 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, - 0x64, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x69, 0x73, 0x20, 0x76, 0x61, 0x6C, 0x69, - 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x4C, 0x61, 0x73, 0x74, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, - 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, - 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, - 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x4E, 0x6F, 0x64, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x6F, 0x64, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x69, 0x6E, - 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x61, 0x62, 0x6F, 0x75, 0x74, 0x20, - 0x61, 0x20, 0x6E, 0x6F, 0x64, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, - 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, - 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x6C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6C, 0x61, 0x73, 0x74, 0x43, 0x6F, - 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x43, 0x6F, - 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x43, 0x6F, - 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x52, 0x6F, - 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, - 0x65, 0x78, 0x74, 0x43, 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6F, 0x6E, 0x53, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6D, 0x65, 0x53, 0x69, 0x6E, 0x63, - 0x65, 0x4C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x61, 0x74, 0x63, 0x68, 0x75, 0x70, 0x54, 0x69, 0x6D, - 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x61, 0x73, - 0x53, 0x79, 0x6E, 0x63, 0x65, 0x64, 0x53, 0x69, 0x6E, 0x63, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x75, 0x70, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x74, - 0x6F, 0x70, 0x70, 0x65, 0x64, 0x41, 0x74, 0x55, 0x6E, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, - 0x65, 0x64, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, - 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x63, 0x61, 0x74, 0x63, 0x68, 0x75, 0x70, 0x54, 0x69, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x61, 0x74, 0x63, 0x68, - 0x75, 0x70, 0x54, 0x69, 0x6D, 0x65, 0x20, 0x69, 0x6E, 0x20, 0x6E, 0x61, 0x6E, 0x6F, 0x73, 0x65, - 0x63, 0x6F, 0x6E, 0x64, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, - 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, - 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x61, 0x74, 0x63, 0x68, 0x75, - 0x70, 0x54, 0x69, 0x6D, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x68, 0x61, 0x73, 0x53, 0x79, - 0x6E, 0x63, 0x65, 0x64, 0x53, 0x69, 0x6E, 0x63, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x48, - 0x61, 0x73, 0x53, 0x79, 0x6E, 0x63, 0x65, 0x64, 0x53, 0x69, 0x6E, 0x63, 0x65, 0x53, 0x74, 0x61, - 0x72, 0x74, 0x75, 0x70, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x77, - 0x68, 0x65, 0x74, 0x68, 0x65, 0x72, 0x20, 0x61, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x68, - 0x61, 0x73, 0x20, 0x63, 0x6F, 0x6D, 0x70, 0x6C, 0x65, 0x74, 0x65, 0x64, 0x20, 0x73, 0x69, 0x6E, - 0x63, 0x65, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x62, 0x6F, 0x6F, 0x6C, 0x65, 0x61, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x48, 0x61, 0x73, 0x53, 0x79, 0x6E, 0x63, 0x65, 0x64, 0x53, 0x69, 0x6E, 0x63, 0x65, - 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6C, 0x61, 0x73, - 0x74, 0x43, 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x4C, 0x61, 0x73, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x64, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x61, 0x73, 0x74, 0x20, 0x63, - 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, - 0x20, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x4C, 0x61, 0x73, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x6C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4C, 0x61, 0x73, 0x74, 0x52, 0x6F, - 0x75, 0x6E, 0x64, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x6C, 0x61, 0x73, 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x73, 0x65, 0x65, - 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, - 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x43, 0x6F, 0x6E, 0x73, 0x65, 0x6E, - 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x6F, 0x66, 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, - 0x73, 0x20, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x74, 0x6F, 0x20, 0x75, 0x73, - 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x43, 0x6F, 0x6E, - 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x52, 0x6F, 0x75, - 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x4E, 0x65, 0x78, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x52, 0x6F, 0x75, 0x6E, - 0x64, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x61, - 0x74, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x65, 0x78, 0x74, - 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6F, 0x6E, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x79, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, - 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x52, 0x6F, - 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x65, 0x78, 0x74, 0x43, 0x6F, 0x6E, - 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x53, 0x75, 0x70, - 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, - 0x53, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x73, 0x20, 0x77, 0x68, 0x65, 0x74, 0x68, 0x65, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x6E, 0x65, 0x78, 0x74, 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x20, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x73, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, - 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x6E, 0x6F, 0x64, 0x65, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x6F, 0x6F, 0x6C, 0x65, 0x61, 0x6E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x65, 0x78, 0x74, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6F, 0x6E, 0x53, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x73, 0x74, 0x6F, 0x70, 0x70, 0x65, 0x64, 0x41, 0x74, 0x55, 0x6E, 0x73, 0x75, 0x70, - 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x74, 0x6F, 0x70, 0x70, 0x65, - 0x64, 0x41, 0x74, 0x55, 0x6E, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x52, 0x6F, - 0x75, 0x6E, 0x64, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, - 0x61, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6E, 0x6F, 0x64, 0x65, 0x20, 0x64, 0x6F, 0x65, 0x73, - 0x20, 0x6E, 0x6F, 0x74, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x6E, 0x65, 0x77, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x73, 0x20, 0x61, 0x6E, 0x64, 0x20, - 0x68, 0x61, 0x73, 0x20, 0x73, 0x74, 0x6F, 0x70, 0x70, 0x65, 0x64, 0x20, 0x6D, 0x61, 0x6B, 0x69, - 0x6E, 0x67, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x65, 0x73, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x62, 0x6F, 0x6F, 0x6C, 0x65, 0x61, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x53, 0x74, 0x6F, 0x70, 0x70, 0x65, 0x64, 0x41, 0x74, 0x55, 0x6E, 0x73, 0x75, - 0x70, 0x70, 0x6F, 0x72, 0x74, 0x65, 0x64, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x69, 0x6D, 0x65, 0x53, 0x69, 0x6E, 0x63, 0x65, 0x4C, 0x61, 0x73, 0x74, 0x52, - 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x54, 0x69, 0x6D, 0x65, 0x53, 0x69, 0x6E, 0x63, 0x65, 0x4C, 0x61, 0x73, 0x74, - 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x6E, 0x61, 0x6E, 0x6F, 0x73, 0x65, 0x63, - 0x6F, 0x6E, 0x64, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, - 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x69, 0x6D, 0x65, 0x53, 0x69, 0x6E, - 0x63, 0x65, 0x4C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, - 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, - 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, - 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, - 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x61, 0x72, - 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x20, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x74, 0x70, 0x6B, 0x62, 0x36, 0x34, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x72, 0x66, 0x70, 0x6B, - 0x62, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, - 0x6F, 0x74, 0x65, 0x66, 0x73, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x76, 0x6F, 0x74, 0x65, 0x6C, 0x73, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x6F, 0x74, 0x65, 0x6B, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, - 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x72, 0x74, 0x70, 0x6B, 0x62, 0x36, 0x34, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x61, 0x72, - 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x50, 0x4B, 0x20, 0x69, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x72, 0x6F, 0x6F, 0x74, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, - 0x70, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, - 0x79, 0x20, 0x28, 0x69, 0x66, 0x20, 0x61, 0x6E, 0x79, 0x29, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, - 0x6E, 0x74, 0x6C, 0x79, 0x20, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, - 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, - 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x50, 0x4B, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x6F, 0x74, 0x65, 0x66, 0x73, 0x74, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, - 0x46, 0x69, 0x72, 0x73, 0x74, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, - 0x73, 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x77, 0x68, 0x69, - 0x63, 0x68, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x73, 0x20, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x2E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x46, 0x69, 0x72, 0x73, 0x74, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x76, 0x6F, 0x74, 0x65, 0x6B, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x4B, 0x65, 0x79, - 0x44, 0x69, 0x6C, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x73, 0x75, 0x62, 0x6B, 0x65, 0x79, - 0x73, 0x20, 0x69, 0x6E, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x65, 0x61, 0x63, 0x68, 0x20, 0x62, 0x61, - 0x74, 0x63, 0x68, 0x20, 0x6F, 0x66, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, - 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x6B, 0x65, 0x79, 0x73, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, - 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x56, 0x6F, 0x74, 0x65, 0x4B, 0x65, 0x79, 0x44, 0x69, 0x6C, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x6F, 0x74, 0x65, 0x6C, 0x73, 0x74, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x4C, - 0x61, 0x73, 0x74, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x61, 0x73, 0x74, 0x20, - 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x20, 0x69, 0x73, 0x20, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x56, 0x6F, 0x74, 0x65, 0x4C, 0x61, 0x73, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x76, 0x72, 0x66, 0x70, 0x6B, 0x62, 0x36, 0x34, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x52, 0x46, 0x50, 0x4B, 0x20, 0x69, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x6C, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x70, 0x75, - 0x62, 0x6C, 0x69, 0x63, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x28, 0x69, 0x66, 0x20, 0x61, 0x6E, 0x79, - 0x29, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x6C, 0x79, 0x20, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, - 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, - 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x52, 0x46, 0x50, 0x4B, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, - 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, - 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, - 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, - 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, - 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, - 0x61, 0x79, 0x6D, 0x65, 0x6E, 0x74, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x50, 0x61, 0x79, 0x6D, 0x65, 0x6E, 0x74, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x66, - 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x20, 0x70, 0x61, 0x79, 0x6D, - 0x65, 0x6E, 0x74, 0x20, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x6F, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, - 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6D, 0x6F, 0x75, 0x6E, 0x74, - 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x6F, - 0x66, 0x20, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, 0x69, 0x6E, 0x74, - 0x65, 0x6E, 0x64, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x62, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, - 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, - 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x41, 0x6D, 0x6F, - 0x75, 0x6E, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6C, 0x6F, 0x73, 0x65, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x6F, - 0x73, 0x65, 0x52, 0x65, 0x6D, 0x61, 0x69, 0x6E, 0x64, 0x65, 0x72, 0x54, 0x6F, 0x20, 0x69, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x73, 0x65, 0x6E, 0x64, 0x65, 0x72, 0x20, 0x63, 0x6C, 0x6F, 0x73, 0x65, 0x64, 0x20, 0x74, - 0x6F, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x52, 0x65, 0x6D, - 0x61, 0x69, 0x6E, 0x64, 0x65, 0x72, 0x54, 0x6F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6C, - 0x6F, 0x73, 0x65, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x41, 0x6D, 0x6F, - 0x75, 0x6E, 0x74, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, - 0x74, 0x20, 0x73, 0x65, 0x6E, 0x74, 0x20, 0x74, 0x6F, 0x20, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x52, - 0x65, 0x6D, 0x61, 0x69, 0x6E, 0x64, 0x65, 0x72, 0x54, 0x6F, 0x2C, 0x20, 0x66, 0x6F, 0x72, 0x20, - 0x63, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, - 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x6F, 0x73, 0x65, - 0x41, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6C, 0x6F, 0x73, - 0x65, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x52, 0x65, 0x77, 0x61, - 0x72, 0x64, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, - 0x74, 0x20, 0x6F, 0x66, 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x72, 0x65, 0x77, - 0x61, 0x72, 0x64, 0x73, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x52, 0x65, 0x6D, 0x61, 0x69, 0x6E, 0x64, - 0x65, 0x72, 0x54, 0x6F, 0x5C, 0x6E, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x61, 0x73, - 0x20, 0x70, 0x61, 0x72, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, - 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x6F, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x54, 0x6F, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x63, - 0x65, 0x69, 0x76, 0x65, 0x72, 0x27, 0x73, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, - 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x6F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x6F, - 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x6F, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x20, - 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x6F, 0x66, - 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, - 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x54, 0x6F, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x5C, 0x6E, 0x61, 0x73, 0x20, 0x70, - 0x61, 0x72, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, - 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, - 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x6F, - 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, - 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, - 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, - 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, - 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x54, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x72, 0x65, 0x70, 0x72, 0x65, - 0x73, 0x65, 0x6E, 0x74, 0x73, 0x20, 0x61, 0x20, 0x70, 0x6F, 0x74, 0x65, 0x6E, 0x74, 0x69, 0x61, - 0x6C, 0x6C, 0x79, 0x20, 0x74, 0x72, 0x75, 0x6E, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x6C, 0x69, - 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x6C, 0x79, 0x20, 0x69, 0x6E, 0x20, - 0x74, 0x68, 0x65, 0x5C, 0x6E, 0x6E, 0x6F, 0x64, 0x65, 0x27, 0x73, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x70, 0x6F, 0x6F, 0x6C, 0x2E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, - 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, - 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x72, 0x75, 0x6E, 0x63, 0x61, 0x74, 0x65, 0x64, 0x54, 0x78, - 0x6E, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x6F, - 0x74, 0x61, 0x6C, 0x54, 0x78, 0x6E, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, - 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x54, 0x78, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x54, 0x78, - 0x6E, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, - 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, - 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x54, 0x78, 0x6E, - 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x72, 0x75, 0x6E, 0x63, 0x61, 0x74, 0x65, 0x64, - 0x54, 0x78, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, - 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x4C, 0x69, 0x73, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, - 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, - 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, - 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x50, 0x72, 0x6F, 0x6F, 0x66, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x54, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6F, 0x6F, 0x66, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, - 0x20, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x20, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x20, 0x70, 0x72, 0x6F, 0x6F, 0x66, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, - 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x70, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x70, 0x6D, 0x73, 0x67, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x70, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x50, 0x72, 0x6F, 0x6F, 0x66, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6D, 0x73, 0x67, - 0x70, 0x61, 0x63, 0x6B, 0x20, 0x65, 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x6F, 0x66, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x70, 0x72, 0x6F, 0x6F, 0x66, - 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, - 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, - 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6F, 0x6F, 0x66, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x73, 0x70, 0x6D, 0x73, 0x67, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6F, - 0x6F, 0x66, 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x6D, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6B, 0x20, 0x65, 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, - 0x67, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x70, - 0x72, 0x6F, 0x6F, 0x66, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6F, 0x6F, 0x66, 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, - 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, - 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, - 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, - 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, - 0x3A, 0x20, 0x6D, 0x6F, 0x64, 0x65, 0x6C, 0x20, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, - 0x65, 0x6D, 0x61, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x74, 0x6C, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x20, 0x72, 0x65, 0x70, 0x72, 0x65, 0x73, - 0x65, 0x6E, 0x74, 0x73, 0x20, 0x61, 0x20, 0x4C, 0x6F, 0x63, 0x61, 0x6C, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x53, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x20, 0x6F, 0x72, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, - 0x6C, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x2E, 0x20, 0x54, 0x68, - 0x65, 0x73, 0x65, 0x5C, 0x6E, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x73, 0x20, 0x64, 0x65, 0x74, - 0x65, 0x72, 0x6D, 0x69, 0x6E, 0x65, 0x20, 0x68, 0x6F, 0x77, 0x20, 0x6D, 0x75, 0x63, 0x68, 0x20, - 0x73, 0x74, 0x6F, 0x72, 0x61, 0x67, 0x65, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x62, 0x65, 0x20, 0x75, - 0x73, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x61, 0x20, 0x4C, 0x6F, 0x63, 0x61, 0x6C, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x20, 0x6F, 0x72, 0x5C, 0x6E, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x6E, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x20, 0x54, 0x68, 0x65, 0x20, 0x6D, 0x6F, 0x72, 0x65, - 0x20, 0x73, 0x70, 0x61, 0x63, 0x65, 0x20, 0x75, 0x73, 0x65, 0x64, 0x2C, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x6C, 0x61, 0x72, 0x67, 0x65, 0x72, 0x20, 0x6D, 0x69, 0x6E, 0x69, 0x6D, 0x75, 0x6D, 0x5C, - 0x6E, 0x62, 0x61, 0x6C, 0x61, 0x6E, 0x63, 0x65, 0x20, 0x6D, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, - 0x20, 0x6D, 0x61, 0x69, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x68, 0x6F, 0x6C, 0x64, 0x69, - 0x6E, 0x67, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x61, 0x74, 0x61, 0x2E, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, - 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, - 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x62, 0x79, 0x74, - 0x65, 0x73, 0x6C, 0x69, 0x63, 0x65, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, - 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x62, 0x79, 0x74, 0x65, 0x73, 0x6C, 0x69, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x75, 0x6D, 0x42, 0x79, 0x74, - 0x65, 0x53, 0x6C, 0x69, 0x63, 0x65, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6D, 0x61, - 0x78, 0x69, 0x6D, 0x75, 0x6D, 0x20, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, - 0x54, 0x45, 0x41, 0x4C, 0x20, 0x62, 0x79, 0x74, 0x65, 0x20, 0x73, 0x6C, 0x69, 0x63, 0x65, 0x73, - 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x62, 0x65, 0x5C, 0x6E, 0x73, 0x74, - 0x6F, 0x72, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6B, 0x65, 0x79, 0x2F, - 0x76, 0x61, 0x6C, 0x75, 0x65, 0x20, 0x73, 0x74, 0x6F, 0x72, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, - 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x4E, 0x75, 0x6D, 0x42, 0x79, 0x74, 0x65, 0x53, 0x6C, 0x69, 0x63, 0x65, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x75, 0x6D, 0x55, 0x69, 0x6E, 0x74, 0x20, 0x69, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6D, 0x61, 0x78, 0x69, 0x6D, 0x75, 0x6D, 0x20, 0x6E, 0x75, - 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x54, 0x45, 0x41, 0x4C, 0x20, 0x75, 0x69, 0x6E, - 0x74, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x62, 0x65, 0x20, 0x73, - 0x74, 0x6F, 0x72, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x5C, 0x6E, 0x74, 0x68, 0x65, 0x20, 0x6B, 0x65, - 0x79, 0x2F, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x20, 0x73, 0x74, 0x6F, 0x72, 0x65, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, - 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x4E, 0x75, 0x6D, 0x55, 0x69, 0x6E, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, - 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, - 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, - 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, - 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x53, 0x75, 0x70, 0x70, - 0x6C, 0x79, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x75, 0x70, - 0x70, 0x6C, 0x79, 0x20, 0x72, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6E, 0x74, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6C, - 0x79, 0x20, 0x6F, 0x66, 0x20, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, - 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, - 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, - 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x4D, 0x6F, 0x6E, 0x65, 0x79, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6F, 0x6E, 0x6C, 0x69, - 0x6E, 0x65, 0x4D, 0x6F, 0x6E, 0x65, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, - 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x6F, 0x6E, 0x6C, 0x69, 0x6E, 0x65, 0x4D, 0x6F, 0x6E, 0x65, 0x79, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4F, 0x6E, 0x6C, 0x69, 0x6E, - 0x65, 0x4D, 0x6F, 0x6E, 0x65, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, - 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, - 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, - 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4F, 0x6E, 0x6C, 0x69, - 0x6E, 0x65, 0x4D, 0x6F, 0x6E, 0x65, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x6F, 0x75, - 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, - 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, - 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, - 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x52, 0x6F, 0x75, 0x6E, - 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x6F, 0x74, 0x61, 0x6C, 0x4D, 0x6F, 0x6E, 0x65, - 0x79, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x4D, 0x6F, 0x6E, 0x65, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, - 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x4D, 0x6F, 0x6E, 0x65, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, - 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, - 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, - 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, - 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x63, 0x6F, - 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x61, 0x6C, 0x6C, 0x20, 0x66, 0x69, 0x65, 0x6C, 0x64, - 0x73, 0x20, 0x63, 0x6F, 0x6D, 0x6D, 0x6F, 0x6E, 0x20, 0x74, 0x6F, 0x20, 0x61, 0x6C, 0x6C, 0x20, - 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x61, 0x6E, 0x64, - 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x73, 0x20, 0x61, 0x73, 0x20, 0x61, 0x6E, 0x20, 0x65, 0x6E, - 0x76, 0x65, 0x6C, 0x6F, 0x70, 0x65, 0x20, 0x74, 0x6F, 0x20, 0x61, 0x6C, 0x6C, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x5C, 0x6E, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x78, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x72, 0x6F, 0x6D, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x65, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x69, 0x72, 0x73, 0x74, 0x2D, 0x72, 0x6F, 0x75, 0x6E, 0x64, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6C, 0x61, 0x73, 0x74, - 0x2D, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x61, 0x70, 0x70, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x73, 0x70, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, - 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x49, 0x44, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x68, 0x61, 0x73, 0x68, 0x62, - 0x36, 0x34, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x70, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, - 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, - 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, - 0x43, 0x61, 0x6C, 0x6C, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, - 0x79, 0x70, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x75, 0x72, 0x63, 0x66, 0x67, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, - 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, - 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x6F, 0x6E, 0x66, 0x69, 0x67, - 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x75, 0x72, 0x66, 0x72, 0x7A, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, - 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x2F, 0x41, 0x73, 0x73, 0x65, 0x74, 0x46, 0x72, 0x65, 0x65, 0x7A, 0x65, 0x54, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x63, 0x75, 0x72, 0x78, 0x66, 0x65, 0x72, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, - 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x41, 0x73, - 0x73, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x54, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x66, 0x65, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x46, 0x65, 0x65, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x66, 0x65, 0x65, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x46, 0x65, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x69, 0x72, 0x73, 0x74, - 0x2D, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x69, 0x72, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x20, - 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, - 0x72, 0x73, 0x74, 0x20, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, - 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, - 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, - 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, - 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x69, 0x72, 0x73, 0x74, 0x52, - 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x72, 0x6F, 0x6D, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x72, 0x6F, - 0x6D, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x6E, 0x64, 0x65, 0x72, 0x27, - 0x73, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, - 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x46, 0x72, 0x6F, 0x6D, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x72, 0x6F, 0x6D, 0x72, 0x65, - 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, - 0x22, 0x3A, 0x20, 0x22, 0x46, 0x72, 0x6F, 0x6D, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x20, - 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x6D, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x6F, 0x66, - 0x20, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, - 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x46, 0x72, 0x6F, 0x6D, 0x5C, 0x6E, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, 0x61, 0x73, - 0x20, 0x70, 0x61, 0x72, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, - 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x46, 0x72, 0x6F, 0x6D, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x49, 0x44, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, - 0x20, 0x49, 0x44, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, - 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, - 0x73, 0x49, 0x44, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, - 0x68, 0x61, 0x73, 0x68, 0x62, 0x36, 0x34, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x20, 0x68, 0x61, - 0x73, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x48, 0x61, 0x73, 0x68, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x72, 0x6F, 0x75, 0x70, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x72, 0x6F, 0x75, 0x70, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, - 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x47, 0x72, 0x6F, 0x75, 0x70, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6B, 0x65, 0x79, - 0x72, 0x65, 0x67, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, - 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x4B, 0x65, 0x79, 0x72, 0x65, 0x67, 0x54, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, 0x70, 0x65, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6C, 0x61, 0x73, 0x74, 0x2D, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4C, 0x61, 0x73, - 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x61, 0x73, 0x74, 0x20, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, - 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, - 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, - 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x4C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6C, - 0x65, 0x61, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x4C, 0x65, 0x61, 0x73, 0x65, 0x20, 0x65, 0x6E, 0x66, 0x6F, 0x72, 0x63, 0x65, - 0x73, 0x20, 0x6D, 0x75, 0x74, 0x75, 0x61, 0x6C, 0x20, 0x65, 0x78, 0x63, 0x6C, 0x75, 0x73, 0x69, - 0x6F, 0x6E, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x2E, 0x20, 0x20, 0x49, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x66, 0x69, 0x65, - 0x6C, 0x64, 0x20, 0x69, 0x73, 0x5C, 0x6E, 0x6E, 0x6F, 0x6E, 0x7A, 0x65, 0x72, 0x6F, 0x2C, 0x20, - 0x74, 0x68, 0x65, 0x6E, 0x20, 0x6F, 0x6E, 0x63, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x73, 0x20, 0x63, 0x6F, 0x6E, - 0x66, 0x69, 0x72, 0x6D, 0x65, 0x64, 0x2C, 0x20, 0x69, 0x74, 0x20, 0x61, 0x63, 0x71, 0x75, 0x69, - 0x72, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x5C, 0x6E, 0x6C, 0x65, 0x61, 0x73, 0x65, 0x20, 0x69, - 0x64, 0x65, 0x6E, 0x74, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x28, 0x53, 0x65, 0x6E, 0x64, 0x65, 0x72, 0x2C, 0x20, 0x4C, 0x65, 0x61, 0x73, 0x65, 0x29, - 0x20, 0x70, 0x61, 0x69, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x75, 0x6E, 0x74, 0x69, 0x6C, 0x5C, 0x6E, - 0x74, 0x68, 0x65, 0x20, 0x4C, 0x61, 0x73, 0x74, 0x56, 0x61, 0x6C, 0x69, 0x64, 0x20, 0x72, 0x6F, - 0x75, 0x6E, 0x64, 0x20, 0x70, 0x61, 0x73, 0x73, 0x65, 0x73, 0x2E, 0x20, 0x20, 0x57, 0x68, 0x69, - 0x6C, 0x65, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x20, 0x70, 0x6F, 0x73, 0x73, 0x65, 0x73, 0x73, 0x65, 0x73, 0x20, 0x74, 0x68, - 0x65, 0x5C, 0x6E, 0x6C, 0x65, 0x61, 0x73, 0x65, 0x2C, 0x20, 0x6E, 0x6F, 0x20, 0x6F, 0x74, 0x68, - 0x65, 0x72, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x73, - 0x70, 0x65, 0x63, 0x69, 0x66, 0x79, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x6C, - 0x65, 0x61, 0x73, 0x65, 0x20, 0x63, 0x61, 0x6E, 0x20, 0x62, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x66, - 0x69, 0x72, 0x6D, 0x65, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4C, 0x65, 0x61, 0x73, 0x65, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6E, 0x6F, 0x74, 0x65, 0x62, 0x36, 0x34, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x6F, 0x74, 0x65, 0x20, 0x69, - 0x73, 0x20, 0x61, 0x20, 0x66, 0x72, 0x65, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x20, 0x64, 0x61, - 0x74, 0x61, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, - 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x6F, 0x74, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, - 0x61, 0x79, 0x6D, 0x65, 0x6E, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, - 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x50, 0x61, 0x79, 0x6D, - 0x65, 0x6E, 0x74, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x54, 0x79, - 0x70, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x6F, 0x6F, 0x6C, 0x65, 0x72, 0x72, 0x6F, - 0x72, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x50, 0x6F, 0x6F, 0x6C, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x20, 0x77, 0x61, 0x73, 0x20, 0x65, 0x76, 0x69, 0x63, 0x74, 0x65, 0x64, 0x20, - 0x66, 0x72, 0x6F, 0x6D, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x6E, 0x6F, 0x64, 0x65, 0x27, 0x73, - 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x5C, 0x6E, 0x70, 0x6F, - 0x6F, 0x6C, 0x20, 0x28, 0x69, 0x66, 0x20, 0x6E, 0x6F, 0x6E, 0x2D, 0x65, 0x6D, 0x70, 0x74, 0x79, - 0x29, 0x2E, 0x20, 0x20, 0x41, 0x20, 0x6E, 0x6F, 0x6E, 0x2D, 0x65, 0x6D, 0x70, 0x74, 0x79, 0x20, - 0x50, 0x6F, 0x6F, 0x6C, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x64, 0x6F, 0x65, 0x73, 0x20, 0x6E, - 0x6F, 0x74, 0x20, 0x67, 0x75, 0x61, 0x72, 0x61, 0x6E, 0x74, 0x65, 0x65, 0x20, 0x74, 0x68, 0x61, - 0x74, 0x20, 0x74, 0x68, 0x65, 0x5C, 0x6E, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x20, 0x6E, 0x65, 0x76, 0x65, 0x72, 0x20, 0x62, 0x65, - 0x20, 0x63, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x74, 0x65, 0x64, 0x3B, 0x20, 0x6F, 0x74, 0x68, 0x65, - 0x72, 0x20, 0x6E, 0x6F, 0x64, 0x65, 0x73, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x6E, 0x6F, 0x74, 0x20, - 0x68, 0x61, 0x76, 0x65, 0x20, 0x65, 0x76, 0x69, 0x63, 0x74, 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, - 0x5C, 0x6E, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x61, 0x6E, - 0x64, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x61, 0x74, 0x74, 0x65, 0x6D, 0x70, 0x74, 0x20, 0x74, 0x6F, - 0x20, 0x63, 0x6F, 0x6D, 0x6D, 0x69, 0x74, 0x20, 0x69, 0x74, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x66, 0x75, 0x74, 0x75, 0x72, 0x65, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, - 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x50, 0x6F, 0x6F, 0x6C, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, - 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x43, 0x6F, 0x6E, 0x66, 0x69, 0x72, 0x6D, 0x65, 0x64, 0x52, 0x6F, 0x75, 0x6E, - 0x64, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x61, 0x70, - 0x70, 0x65, 0x61, 0x72, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, - 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, - 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x43, - 0x6F, 0x6E, 0x66, 0x69, 0x72, 0x6D, 0x65, 0x64, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x73, 0x70, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, - 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x50, 0x72, 0x6F, 0x6F, 0x66, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x54, 0x79, 0x70, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x78, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x78, 0x49, 0x44, 0x20, - 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x20, 0x49, 0x44, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, - 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, - 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x78, 0x49, 0x44, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x78, 0x72, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x73, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, - 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, - 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x52, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x54, 0x79, 0x70, 0x65, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x54, 0x79, 0x70, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, - 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, - 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, - 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x46, 0x65, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x46, 0x65, 0x65, - 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x75, - 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x66, 0x65, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, - 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, - 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x65, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, - 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x66, 0x65, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x46, 0x65, 0x65, 0x20, 0x69, 0x73, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x66, 0x65, 0x65, 0x5C, 0x6E, 0x46, 0x65, 0x65, 0x20, 0x69, 0x73, - 0x20, 0x69, 0x6E, 0x20, 0x75, 0x6E, 0x69, 0x74, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x6D, 0x69, 0x63, - 0x72, 0x6F, 0x2D, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, 0x70, 0x65, 0x72, 0x20, 0x62, 0x79, 0x74, - 0x65, 0x2E, 0x5C, 0x6E, 0x46, 0x65, 0x65, 0x20, 0x6D, 0x61, 0x79, 0x20, 0x66, 0x61, 0x6C, 0x6C, - 0x20, 0x74, 0x6F, 0x20, 0x7A, 0x65, 0x72, 0x6F, 0x20, 0x62, 0x75, 0x74, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x6D, 0x75, 0x73, 0x74, 0x20, 0x73, - 0x74, 0x69, 0x6C, 0x6C, 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, 0x61, 0x20, 0x66, 0x65, 0x65, 0x20, - 0x6F, 0x66, 0x5C, 0x6E, 0x61, 0x74, 0x20, 0x6C, 0x65, 0x61, 0x73, 0x74, 0x20, 0x4D, 0x69, 0x6E, - 0x54, 0x78, 0x6E, 0x46, 0x65, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, - 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x6E, 0x65, 0x74, 0x77, 0x6F, 0x72, 0x6B, 0x20, 0x70, - 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, - 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, - 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x65, - 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, - 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, - 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, 0x65, - 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, 0x70, - 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x4C, 0x69, - 0x73, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x4C, 0x69, 0x73, 0x74, 0x20, 0x63, 0x6F, 0x6E, - 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x61, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, - 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, - 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x4C, 0x69, 0x73, 0x74, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, - 0x66, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 0x6D, 0x73, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, - 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, - 0x6F, 0x6E, 0x73, 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, - 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, - 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, 0x61, - 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, 0x73, - 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x50, - 0x61, 0x72, 0x61, 0x6D, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x50, 0x61, 0x72, 0x61, 0x6D, - 0x73, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, - 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x68, - 0x65, 0x6C, 0x70, 0x20, 0x61, 0x20, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x20, 0x63, 0x6F, 0x6E, - 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x5C, 0x6E, 0x61, 0x20, 0x6E, 0x65, 0x77, 0x20, 0x74, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, - 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, - 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x66, 0x65, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x49, 0x44, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x68, 0x61, 0x73, - 0x68, 0x62, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x6C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, - 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, - 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x6F, - 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x69, - 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6F, 0x6E, - 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x5C, 0x6E, 0x61, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x4C, - 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, - 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x43, 0x6F, 0x6E, 0x73, 0x65, 0x6E, 0x73, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x65, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x46, 0x65, 0x65, 0x20, 0x69, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x66, 0x65, 0x65, 0x5C, 0x6E, 0x46, 0x65, - 0x65, 0x20, 0x69, 0x73, 0x20, 0x69, 0x6E, 0x20, 0x75, 0x6E, 0x69, 0x74, 0x73, 0x20, 0x6F, 0x66, - 0x20, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x2D, 0x41, 0x6C, 0x67, 0x6F, 0x73, 0x20, 0x70, 0x65, 0x72, - 0x20, 0x62, 0x79, 0x74, 0x65, 0x2E, 0x5C, 0x6E, 0x46, 0x65, 0x65, 0x20, 0x6D, 0x61, 0x79, 0x20, - 0x66, 0x61, 0x6C, 0x6C, 0x20, 0x74, 0x6F, 0x20, 0x7A, 0x65, 0x72, 0x6F, 0x20, 0x62, 0x75, 0x74, - 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x6D, 0x75, - 0x73, 0x74, 0x20, 0x73, 0x74, 0x69, 0x6C, 0x6C, 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, 0x61, 0x20, - 0x66, 0x65, 0x65, 0x20, 0x6F, 0x66, 0x5C, 0x6E, 0x61, 0x74, 0x20, 0x6C, 0x65, 0x61, 0x73, 0x74, - 0x20, 0x4D, 0x69, 0x6E, 0x54, 0x78, 0x6E, 0x46, 0x65, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x6E, 0x65, 0x74, 0x77, 0x6F, - 0x72, 0x6B, 0x20, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x2E, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x46, 0x65, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, - 0x69, 0x73, 0x49, 0x44, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x20, 0x49, 0x44, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, - 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x49, 0x44, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x68, 0x61, 0x73, 0x68, 0x62, 0x36, - 0x34, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x20, 0x68, 0x61, 0x73, 0x68, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, - 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x47, - 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x48, 0x61, 0x73, 0x68, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x6C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, - 0x64, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x6C, 0x61, 0x73, 0x74, 0x20, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x73, 0x65, 0x65, 0x6E, 0x22, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, - 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4C, 0x61, 0x73, 0x74, 0x52, 0x6F, 0x75, 0x6E, 0x64, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x6D, 0x69, 0x6E, 0x46, 0x65, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x6D, 0x69, 0x6E, - 0x69, 0x6D, 0x75, 0x6D, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x20, 0x66, 0x65, 0x65, 0x20, 0x28, 0x6E, 0x6F, 0x74, 0x20, 0x70, 0x65, 0x72, 0x20, 0x62, 0x79, - 0x74, 0x65, 0x29, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x66, 0x6F, 0x72, - 0x20, 0x74, 0x68, 0x65, 0x5C, 0x6E, 0x74, 0x78, 0x6E, 0x20, 0x74, 0x6F, 0x20, 0x76, 0x61, 0x6C, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x75, - 0x72, 0x72, 0x65, 0x6E, 0x74, 0x20, 0x6E, 0x65, 0x74, 0x77, 0x6F, 0x72, 0x6B, 0x20, 0x70, 0x72, - 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, - 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, - 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x4D, 0x69, 0x6E, - 0x54, 0x78, 0x6E, 0x46, 0x65, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, - 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, - 0x64, 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, - 0x70, 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x52, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x73, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, - 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x61, 0x62, 0x6F, - 0x75, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x69, 0x64, 0x65, 0x20, 0x65, 0x66, 0x66, 0x65, - 0x63, 0x74, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, - 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x64, 0x61, 0x70, 0x70, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x70, - 0x70, 0x49, 0x6E, 0x64, 0x65, 0x78, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x70, 0x70, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x20, 0x6F, - 0x66, 0x20, 0x61, 0x6E, 0x20, 0x61, 0x70, 0x70, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x78, 0x6E, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, - 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, - 0x20, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x70, 0x70, 0x49, 0x6E, 0x64, 0x65, - 0x78, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, - 0x20, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x6E, - 0x64, 0x65, 0x78, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x20, 0x6F, 0x66, - 0x20, 0x61, 0x6E, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x78, 0x6E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, - 0x20, 0x22, 0x75, 0x69, 0x6E, 0x74, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, - 0x6E, 0x64, 0x65, 0x78, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, - 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, - 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, - 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x4E, 0x6F, 0x74, 0x65, 0x20, 0x74, 0x68, 0x61, - 0x74, 0x20, 0x77, 0x65, 0x20, 0x61, 0x6E, 0x6E, 0x6F, 0x74, 0x61, 0x74, 0x65, 0x20, 0x74, 0x68, - 0x69, 0x73, 0x20, 0x61, 0x73, 0x20, 0x61, 0x20, 0x6D, 0x6F, 0x64, 0x65, 0x6C, 0x20, 0x73, 0x6F, - 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6C, 0x65, 0x67, 0x61, 0x63, 0x79, 0x20, 0x63, 0x6C, 0x69, - 0x65, 0x6E, 0x74, 0x73, 0x5C, 0x6E, 0x63, 0x61, 0x6E, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x6C, 0x79, 0x20, 0x69, 0x6D, 0x70, 0x6F, 0x72, 0x74, 0x20, 0x61, 0x20, 0x73, 0x77, 0x61, 0x67, - 0x67, 0x65, 0x72, 0x20, 0x67, 0x65, 0x6E, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x20, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x6D, 0x6F, 0x64, 0x65, 0x6C, 0x2E, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, - 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, - 0x74, 0x6C, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x63, - 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, - 0x65, 0x6E, 0x74, 0x20, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, - 0x6E, 0x2E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, - 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x5F, 0x69, 0x64, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, - 0x73, 0x69, 0x73, 0x5F, 0x68, 0x61, 0x73, 0x68, 0x5F, 0x62, 0x36, 0x34, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x62, 0x75, 0x69, 0x6C, 0x64, 0x22, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, - 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x62, 0x75, 0x69, 0x6C, 0x64, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, - 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, - 0x73, 0x2F, 0x42, 0x75, 0x69, 0x6C, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, 0x5F, 0x68, 0x61, 0x73, 0x68, - 0x5F, 0x62, 0x36, 0x34, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x22, 0x3A, 0x20, 0x22, 0x62, 0x79, 0x74, 0x65, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, 0x6F, - 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x6E, 0x65, 0x73, 0x69, 0x73, - 0x48, 0x61, 0x73, 0x68, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 0x65, 0x6E, 0x65, 0x73, 0x69, - 0x73, 0x5F, 0x69, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, 0x6E, - 0x65, 0x73, 0x69, 0x73, 0x49, 0x44, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, - 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, - 0x74, 0x65, 0x6D, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, - 0x72, 0x69, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, - 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6F, 0x6E, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, - 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, - 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x64, - 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, 0x69, 0x2F, - 0x73, 0x70, 0x65, 0x63, 0x2F, 0x63, 0x6F, 0x6D, 0x6D, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x44, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x44, 0x20, 0x44, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, - 0x63, 0x74, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, - 0x69, 0x72, 0x65, 0x64, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x74, 0x78, 0x49, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5D, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x69, - 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x74, 0x78, 0x49, 0x64, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x54, 0x78, 0x49, 0x64, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, - 0x74, 0x72, 0x69, 0x6E, 0x67, 0x20, 0x65, 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x6F, - 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x20, 0x68, 0x61, 0x73, 0x68, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, - 0x6E, 0x67, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x78, 0x49, - 0x44, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x78, 0x2D, 0x67, - 0x6F, 0x2D, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x44, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x78, 0x2D, 0x67, 0x6F, 0x2D, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x22, 0x3A, 0x20, - 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x61, 0x6C, 0x67, 0x6F, - 0x72, 0x61, 0x6E, 0x64, 0x2F, 0x67, 0x6F, 0x2D, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x61, 0x6E, 0x64, - 0x2F, 0x64, 0x61, 0x65, 0x6D, 0x6F, 0x6E, 0x2F, 0x61, 0x6C, 0x67, 0x6F, 0x64, 0x2F, 0x61, 0x70, - 0x69, 0x2F, 0x73, 0x70, 0x65, 0x63, 0x2F, 0x76, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x22, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, - 0x65, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, 0x63, 0x63, 0x6F, - 0x75, 0x6E, 0x74, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x52, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x41, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, - 0x69, 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, - 0x61, 0x69, 0x6E, 0x73, 0x20, 0x61, 0x6E, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x20, - 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, - 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x41, - 0x63, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, - 0x74, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x70, - 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x49, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x52, - 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, - 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, - 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, - 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, - 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6D, - 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, - 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x61, 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, - 0x66, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, - 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x41, 0x73, 0x73, 0x65, - 0x74, 0x4C, 0x69, 0x73, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x42, 0x6C, 0x6F, 0x63, 0x6B, - 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x42, 0x6C, 0x6F, 0x63, 0x6B, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, - 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, - 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, - 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x42, - 0x6C, 0x6F, 0x63, 0x6B, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, - 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, - 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, - 0x61, 0x69, 0x6E, 0x73, 0x20, 0x61, 0x20, 0x28, 0x70, 0x6F, 0x74, 0x65, 0x6E, 0x74, 0x69, 0x61, - 0x6C, 0x6C, 0x79, 0x20, 0x74, 0x72, 0x75, 0x6E, 0x63, 0x61, 0x74, 0x65, 0x64, 0x29, 0x20, 0x6C, - 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x73, 0x20, 0x61, 0x6E, 0x64, 0x5C, 0x6E, 0x74, 0x68, 0x65, 0x20, 0x74, 0x6F, 0x74, - 0x61, 0x6C, 0x20, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6E, - 0x74, 0x6C, 0x79, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x6F, 0x6F, 0x6C, 0x2E, - 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, - 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, - 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, - 0x6F, 0x6E, 0x73, 0x2F, 0x50, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x54, 0x72, 0x61, 0x6E, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x6E, 0x6F, 0x64, 0x65, 0x27, 0x73, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, - 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, - 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x4E, - 0x6F, 0x64, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x53, - 0x75, 0x70, 0x70, 0x6C, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x53, 0x75, 0x70, 0x70, 0x6C, 0x79, 0x52, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x6C, 0x65, 0x64, 0x67, 0x65, 0x72, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6C, - 0x79, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, - 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x2F, 0x53, 0x75, 0x70, 0x70, 0x6C, 0x79, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, - 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6F, - 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, - 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x61, 0x20, - 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x66, 0x65, 0x65, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, - 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x46, 0x65, 0x65, 0x22, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, - 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, - 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, - 0x69, 0x6E, 0x73, 0x20, 0x61, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, - 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x2F, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x49, 0x44, 0x22, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x50, - 0x61, 0x72, 0x61, 0x6D, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6F, 0x6E, 0x50, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, - 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, - 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x5C, 0x6E, 0x63, - 0x6F, 0x6E, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6E, 0x67, 0x20, 0x61, 0x20, 0x6E, 0x65, - 0x77, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x22, 0x2C, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, - 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, - 0x73, 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x50, 0x61, 0x72, - 0x61, 0x6D, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6F, 0x6E, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, - 0x69, 0x6E, 0x73, 0x20, 0x61, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, - 0x6E, 0x20, 0x69, 0x6E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x2C, 0x0A, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, - 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, - 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, - 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, - 0x22, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x61, - 0x20, 0x6C, 0x69, 0x73, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, - 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, - 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, 0x54, 0x72, 0x61, 0x6E, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6F, 0x6E, 0x4C, 0x69, 0x73, 0x74, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x7D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x22, 0x3A, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x74, 0x6F, 0x20, 0x27, 0x47, 0x45, - 0x54, 0x20, 0x2F, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x73, 0x27, 0x22, 0x2C, 0x0A, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x22, 0x3A, 0x20, 0x7B, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3A, - 0x20, 0x22, 0x23, 0x2F, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x2F, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7D, - 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x22, 0x73, - 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x44, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, - 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x69, 0x5F, - 0x6B, 0x65, 0x79, 0x22, 0x3A, 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x47, 0x65, - 0x6E, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x20, 0x70, - 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x2E, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x74, - 0x6F, 0x6B, 0x65, 0x6E, 0x20, 0x63, 0x61, 0x6E, 0x20, 0x62, 0x65, 0x20, 0x67, 0x65, 0x6E, 0x65, - 0x72, 0x61, 0x74, 0x65, 0x64, 0x20, 0x75, 0x73, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x47, 0x6F, 0x61, 0x6C, 0x20, 0x63, 0x6F, 0x6D, 0x6D, 0x61, 0x6E, 0x64, 0x20, 0x6C, 0x69, 0x6E, - 0x65, 0x20, 0x74, 0x6F, 0x6F, 0x6C, 0x2E, 0x20, 0x45, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, - 0x76, 0x61, 0x6C, 0x75, 0x65, 0x20, 0x3D, 0x27, 0x62, 0x37, 0x65, 0x33, 0x38, 0x34, 0x64, 0x30, - 0x33, 0x31, 0x37, 0x62, 0x38, 0x30, 0x35, 0x30, 0x63, 0x65, 0x34, 0x35, 0x39, 0x30, 0x30, 0x61, - 0x39, 0x34, 0x61, 0x31, 0x39, 0x33, 0x31, 0x65, 0x32, 0x38, 0x35, 0x34, 0x30, 0x65, 0x31, 0x66, - 0x36, 0x39, 0x62, 0x32, 0x64, 0x32, 0x34, 0x32, 0x62, 0x34, 0x32, 0x34, 0x36, 0x35, 0x39, 0x63, - 0x33, 0x34, 0x31, 0x62, 0x34, 0x36, 0x39, 0x37, 0x27, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x61, 0x70, 0x69, 0x4B, 0x65, - 0x79, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, - 0x3A, 0x20, 0x22, 0x58, 0x2D, 0x41, 0x6C, 0x67, 0x6F, 0x2D, 0x41, 0x50, 0x49, 0x2D, 0x54, 0x6F, - 0x6B, 0x65, 0x6E, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6E, 0x22, - 0x3A, 0x20, 0x22, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x22, 0x78, 0x2D, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x22, 0x3A, 0x20, 0x22, - 0x62, 0x37, 0x65, 0x33, 0x38, 0x34, 0x64, 0x30, 0x33, 0x31, 0x37, 0x62, 0x38, 0x30, 0x35, 0x30, - 0x63, 0x65, 0x34, 0x35, 0x39, 0x30, 0x30, 0x61, 0x39, 0x34, 0x61, 0x31, 0x39, 0x33, 0x31, 0x65, - 0x32, 0x38, 0x35, 0x34, 0x30, 0x65, 0x31, 0x66, 0x36, 0x39, 0x62, 0x32, 0x64, 0x32, 0x34, 0x32, - 0x62, 0x34, 0x32, 0x34, 0x36, 0x35, 0x39, 0x63, 0x33, 0x34, 0x31, 0x62, 0x34, 0x36, 0x39, 0x37, - 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x7D, 0x2C, 0x0A, 0x20, 0x20, 0x22, - 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x22, 0x3A, 0x20, 0x5B, 0x0A, 0x20, 0x20, 0x20, - 0x20, 0x7B, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x70, 0x69, 0x5F, 0x6B, 0x65, - 0x79, 0x22, 0x3A, 0x20, 0x5B, 0x5D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7D, 0x0A, 0x20, 0x20, 0x5D, - 0x0A, 0x7D, - }) -} diff --git a/daemon/algod/api/server/lib/common.go b/daemon/algod/api/server/lib/common.go index 8e002d26af..0bc7394847 100644 --- a/daemon/algod/api/server/lib/common.go +++ b/daemon/algod/api/server/lib/common.go @@ -25,9 +25,6 @@ import ( "github.com/algorand/go-algorand/node" ) -// SwaggerSpecJSON is autogenerated from swagger.json, and bundled in with a script on build. -var SwaggerSpecJSON string - // GenesisJSONText is initialized when the node starts. var GenesisJSONText string diff --git a/daemon/algod/api/server/router.go b/daemon/algod/api/server/router.go index de76f6fb02..9f6c4277bd 100644 --- a/daemon/algod/api/server/router.go +++ b/daemon/algod/api/server/router.go @@ -15,48 +15,6 @@ // along with go-algorand. If not, see . // Package server Algod REST API. -// -// API Endpoint for AlgoD Operations. -// -// -// Schemes: http -// Host: localhost -// BasePath: / -// Version: 0.0.1 -// License: -// Contact: contact@algorand.com -// -// Consumes: -// - application/json -// -// Produces: -// - application/json -// -// Security: -// - api_key: -// -// SecurityDefinitions: -// api_key: -// type: apiKey -// name: X-Algo-API-Token -// in: header -// description: >- -// Generated header parameter. This token can be generated using the Goal command line tool. Example value -// ='b7e384d0317b8050ce45900a94a1931e28540e1f69b2d242b424659c341b4697' -// required: true -// x-example: b7e384d0317b8050ce45900a94a1931e28540e1f69b2d242b424659c341b4697 -// -// swagger:meta -//--- -// Currently, server implementation annotations serve as the API ground truth. From that, -// we use go-swagger to generate a swagger spec. -// -// Autogenerate the swagger json - automatically run by the 'make build' step. -// Base path must be a fully specified package name (else, it seems that swagger feeds a relative path to -// loader.Config.Import(), and that breaks the vendor directory if the source is symlinked from elsewhere) -//go:generate swagger generate spec -o="../swagger.json" -//go:generate swagger validate ../swagger.json --stop-on-error -//go:generate sh ./lib/bundle_swagger_json.sh package server import ( diff --git a/daemon/algod/api/server/router_test.go b/daemon/algod/api/server/router_test.go index edc5933d7b..0e481e5fc1 100644 --- a/daemon/algod/api/server/router_test.go +++ b/daemon/algod/api/server/router_test.go @@ -17,6 +17,7 @@ package server import ( "net/http" + "net/http/httptest" "testing" "github.com/labstack/echo/v4" @@ -25,181 +26,61 @@ import ( "github.com/algorand/go-algorand/daemon/algod/api/server/lib" "github.com/algorand/go-algorand/daemon/algod/api/server/v1/routes" + + "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/test/partitiontest" ) type TestSuite struct { suite.Suite - calls int - e *echo.Echo + 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. + // Make a deep copy of the routes array with handlers. 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, + HandlerFunc: route.HandlerFunc, }) } + // Make a ReqContext with an initialized logger to prevent nil dereferencing. + reqCtx := lib.ReqContext{Log: logging.NewLogger()} // Registering v1 routes - registerHandlers(s.e, apiV1Tag, v1RoutesCopy, lib.ReqContext{}) -} -func (s *TestSuite) SetupTest() { - s.calls = 0 -} -func (s *TestSuite) TestBaselineRoute() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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) + registerHandlers(s.e, apiV1Tag, v1RoutesCopy, reqCtx) } -func (s *TestSuite) TestTransactionInformation() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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")) +func (s *TestSuite) TestGetTransactionV1Sunset() { + testCases := []struct { + path string + route string + }{ + {"/v1/account/address-param/transactions/pending", "/v1/account/:addr/transactions/pending"}, + {"/v1/status/wait-for-block-after/123456", "/v1/status/wait-for-block-after/:round"}, + {"/v1/block/123456", "/v1/block/:round"}, + {"/v1/transactions/pending/ASPB5E72OT2UWSOCQGD5OPT3W4KV4LZZDL7L5MBCC3EBAIJCDHAA", "/v1/transactions/pending/:txid"}, + {"/v1/asset/123456", "/v1/asset/: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) TestBlock() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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")) + rec := httptest.NewRecorder() + ctx := s.e.NewContext(nil, rec) - // 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() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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")) + for _, testCase := range testCases { + s.e.Router().Find(http.MethodGet, testCase.path, ctx) + assert.Equal(s.T(), testCase.route, ctx.Path()) - // 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() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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")) + // Check that router correctly routes to the v1Sunset handler. + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.NotNil(s.T(), rec.Body) + assert.Equal(s.T(), http.StatusGone, rec.Code) + } - // 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() { - // partitiontest.PartitionTest(s.T()) - // Partitioning in TestTestSuite() - 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) { partitiontest.PartitionTest(t) suite.Run(t, new(TestSuite)) diff --git a/daemon/algod/api/server/v1/handlers/errors.go b/daemon/algod/api/server/v1/handlers/errors.go index ae70a52e96..0506eea6e8 100644 --- a/daemon/algod/api/server/v1/handlers/errors.go +++ b/daemon/algod/api/server/v1/handlers/errors.go @@ -17,38 +17,5 @@ package handlers var ( - errBlockHashBeenDeletedArchival = "this is a non-archival node and the requested block has been already deleted" - errFailedGettingInformationFromIndexer = "failed retrieving information from the indexer" - errFailedLookingUpLedger = "failed to retrieve information from the ledger" - errFailedLookingUpTransactionPool = "failed to retrieve information from the transaction pool" - errFailedRetrievingNodeStatus = "failed retrieving node status" - errFailedRetrievingAsset = "failed to retrieve asset information" - 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" - errInvalidTransactionTypePending = "a transaction with invalid type field was found in transaction pool - type %s, transaction #%s" - errNoAccountSpecified = "no address was specified" - errNoRoundsSpecified = "Indexer is not enabled, firstRound and lastRound must be specified" - errNoTxnSpecified = "no transaction ID was specified" - errTransactionNotFound = "couldn't find the required transaction in the required range" - errServiceShuttingDown = "operation aborted as server is shutting down" - errUnknownTransactionType = "found a transaction with an unknown type" - errRequestedRoundInUnsupportedRound = "requested round would reach only after the protocol upgrade which isn't supported" - errOperationNotAvailableDuringCatchup = "operation not available during catchup" - errCertificateIsMissingFromBlock = "certificate is missing from block" + errV1Sunset = "v1 algod APIs are no longer available, please use the v2 algod API" ) diff --git a/daemon/algod/api/server/v1/handlers/handlers.go b/daemon/algod/api/server/v1/handlers/handlers.go index d01034fecd..e1209cf1d0 100644 --- a/daemon/algod/api/server/v1/handlers/handlers.go +++ b/daemon/algod/api/server/v1/handlers/handlers.go @@ -17,1902 +17,17 @@ package handlers import ( - "database/sql" - "encoding/base64" "errors" - "fmt" - "io" "net/http" - "strconv" - "time" - "unicode" - "unicode/utf8" "github.com/labstack/echo/v4" - "github.com/algorand/go-algorand/agreement" - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod/api/server/lib" - 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" - "github.com/algorand/go-algorand/data/transactions" - "github.com/algorand/go-algorand/ledger/ledgercore" - "github.com/algorand/go-algorand/node" - "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-algorand/rpcs" ) -func getNodeStatus(node *node.AlgorandFullNode) (res v1.NodeStatus, err error) { - stat, err := node.Status() - if err != nil { - return v1.NodeStatus{}, err - } - - return v1.NodeStatus{ - LastRound: uint64(stat.LastRound), - LastVersion: string(stat.LastVersion), - NextVersion: string(stat.NextVersion), - NextVersionRound: uint64(stat.NextVersionRound), - NextVersionSupported: stat.NextVersionSupported, - TimeSinceLastRound: stat.TimeSinceLastRound().Nanoseconds(), - CatchupTime: stat.CatchupTime.Nanoseconds(), - StoppedAtUnsupportedRound: stat.StoppedAtUnsupportedRound, - }, nil -} - -// decorateUnknownTransactionTypeError takes an error of type errUnknownTransactionType and converts it into -// either errInvalidTransactionTypeLedger or errInvalidTransactionTypePending as needed. -func decorateUnknownTransactionTypeError(err error, txs node.TxnWithStatus) error { - if err.Error() != errUnknownTransactionType { - return err - } - if txs.ConfirmedRound != basics.Round(0) { - return fmt.Errorf(errInvalidTransactionTypeLedger, txs.Txn.Txn.Type, txs.Txn.Txn.ID().String(), txs.ConfirmedRound) - } - return fmt.Errorf(errInvalidTransactionTypePending, txs.Txn.Txn.Type, txs.Txn.Txn.ID().String()) -} - -// txEncode copies the data fields of the internal transaction object and populate the v1.Transaction accordingly. -// if unexpected transaction type is encountered, an error is returned. The caller is expected to ignore the returned -// transaction when error is non-nil. -func txEncode(tx transactions.Transaction, ad transactions.ApplyData) (v1.Transaction, error) { - var res v1.Transaction - switch tx.Type { - case protocol.PaymentTx: - res = paymentTxEncode(tx, ad) - case protocol.KeyRegistrationTx: - res = keyregTxEncode(tx, ad) - case protocol.AssetConfigTx: - res = assetConfigTxEncode(tx, ad) - case protocol.AssetTransferTx: - res = assetTransferTxEncode(tx, ad) - case protocol.AssetFreezeTx: - res = assetFreezeTxEncode(tx, ad) - case protocol.ApplicationCallTx: - res = applicationCallTxEncode(tx, ad) - case protocol.StateProofTx: - res = stateProofTxEncode(tx) - default: - return res, errors.New(errUnknownTransactionType) - } - - res.Type = string(tx.Type) - res.TxID = tx.ID().String() - res.From = tx.Src().String() - res.Fee = tx.TxFee().Raw - res.FirstRound = uint64(tx.First()) - res.LastRound = uint64(tx.Last()) - res.Note = tx.Aux() - res.FromRewards = ad.SenderRewards.Raw - res.GenesisID = tx.GenesisID - res.GenesisHash = tx.GenesisHash[:] - - if tx.Group != (crypto.Digest{}) { - res.Group = tx.Group[:] - } - - if tx.Lease != ([32]byte{}) { - res.Lease = tx.Lease[:] - } - - return res, nil -} - -func paymentTxEncode(tx transactions.Transaction, ad transactions.ApplyData) v1.Transaction { - payment := v1.PaymentTransactionType{ - To: tx.Receiver.String(), - Amount: tx.TxAmount().Raw, - ToRewards: ad.ReceiverRewards.Raw, - CloseRewards: ad.CloseRewards.Raw, - } - - if tx.CloseRemainderTo != (basics.Address{}) { - payment.CloseRemainderTo = tx.CloseRemainderTo.String() - payment.CloseAmount = ad.ClosingAmount.Raw - } - - return v1.Transaction{ - Payment: &payment, - } -} - -func keyregTxEncode(tx transactions.Transaction, ad transactions.ApplyData) v1.Transaction { - keyreg := v1.KeyregTransactionType{ - VotePK: tx.KeyregTxnFields.VotePK[:], - SelectionPK: tx.KeyregTxnFields.SelectionPK[:], - VoteFirst: uint64(tx.KeyregTxnFields.VoteFirst), - VoteLast: uint64(tx.KeyregTxnFields.VoteLast), - VoteKeyDilution: tx.KeyregTxnFields.VoteKeyDilution, - } - - return v1.Transaction{ - Keyreg: &keyreg, - } -} - -func participationKeysEncode(r basics.AccountData) *v1.Participation { - var apiParticipation v1.Participation - apiParticipation.ParticipationPK = r.VoteID[:] - apiParticipation.VRFPK = r.SelectionID[:] - apiParticipation.VoteFirst = uint64(r.VoteFirstValid) - apiParticipation.VoteLast = uint64(r.VoteLastValid) - apiParticipation.VoteKeyDilution = r.VoteKeyDilution - - return &apiParticipation -} - -// printableUTF8OrEmpty checks to see if the entire string is a UTF8 printable string. -// If this is the case, the string is returned as is. Otherwise, the empty string is returned. -func printableUTF8OrEmpty(in string) string { - // iterate throughout all the characters in the string to see if they are all printable. - // when range iterating on go strings, go decode each element as a utf8 rune. - for _, c := range in { - // is this a printable character, or invalid rune ? - if c == utf8.RuneError || !unicode.IsPrint(c) { - return "" - } - } - return in -} - -func modelAssetParams(creator basics.Address, params basics.AssetParams) v1.AssetParams { - paramsModel := v1.AssetParams{ - Total: params.Total, - DefaultFrozen: params.DefaultFrozen, - Decimals: params.Decimals, - } - - paramsModel.UnitName = printableUTF8OrEmpty(params.UnitName) - paramsModel.AssetName = printableUTF8OrEmpty(params.AssetName) - paramsModel.URL = printableUTF8OrEmpty(params.URL) - if params.MetadataHash != [32]byte{} { - paramsModel.MetadataHash = params.MetadataHash[:] - } - - if !creator.IsZero() { - paramsModel.Creator = creator.String() - } - - if !params.Manager.IsZero() { - paramsModel.ManagerAddr = params.Manager.String() - } - - if !params.Reserve.IsZero() { - paramsModel.ReserveAddr = params.Reserve.String() - } - - if !params.Freeze.IsZero() { - paramsModel.FreezeAddr = params.Freeze.String() - } - - if !params.Clawback.IsZero() { - paramsModel.ClawbackAddr = params.Clawback.String() - } - - 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 := modelAssetParams(basics.Address{}, tx.AssetConfigTxnFields.AssetParams) - - config := v1.AssetConfigTransactionType{ - AssetID: uint64(tx.AssetConfigTxnFields.ConfigAsset), - Params: params, - } - - return v1.Transaction{ - AssetConfig: &config, - } -} - -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)) - } - - 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))) - } - - app.Accounts = encodedAccounts - app.ApplicationArgs = encodedArgs - app.ForeignApps = encodedForeignApps - app.ForeignAssets = encodedForeignAssets - return v1.Transaction{ - ApplicationCall: &app, - } -} - -func assetTransferTxEncode(tx transactions.Transaction, ad transactions.ApplyData) v1.Transaction { - xfer := v1.AssetTransferTransactionType{ - AssetID: uint64(tx.AssetTransferTxnFields.XferAsset), - Amount: tx.AssetTransferTxnFields.AssetAmount, - Receiver: tx.AssetTransferTxnFields.AssetReceiver.String(), - } - - if !tx.AssetTransferTxnFields.AssetSender.IsZero() { - xfer.Sender = tx.AssetTransferTxnFields.AssetSender.String() - } - - if !tx.AssetTransferTxnFields.AssetCloseTo.IsZero() { - xfer.CloseTo = tx.AssetTransferTxnFields.AssetCloseTo.String() - } - - if ad.AssetClosingAmount != 0 { - xfer.CloseToAmount = ad.AssetClosingAmount - } - - return v1.Transaction{ - AssetTransfer: &xfer, - } -} - -func assetFreezeTxEncode(tx transactions.Transaction, ad transactions.ApplyData) v1.Transaction { - freeze := v1.AssetFreezeTransactionType{ - AssetID: uint64(tx.AssetFreezeTxnFields.FreezeAsset), - Account: tx.AssetFreezeTxnFields.FreezeAccount.String(), - NewFreezeStatus: tx.AssetFreezeTxnFields.AssetFrozen, - } - - return v1.Transaction{ - AssetFreeze: &freeze, - } -} - -func stateProofTxEncode(tx transactions.Transaction) v1.Transaction { - sp := v1.StateProofTransactionType{ - StateProof: protocol.Encode(&tx.StateProofTxnFields.StateProof), - StateProofMessage: protocol.Encode(&tx.Message), - } - - return v1.Transaction{ - StateProof: &sp, - } -} - -func txWithStatusEncode(tr node.TxnWithStatus) (v1.Transaction, error) { - s, err := txEncode(tr.Txn.Txn, tr.ApplyData) - if err != nil { - err = decorateUnknownTransactionTypeError(err, tr) - return v1.Transaction{}, err - } - s.ConfirmedRound = uint64(tr.ConfirmedRound) - s.PoolError = tr.PoolError - return s, nil -} - -func computeCreatableIndexInPayset(tx node.TxnWithStatus, txnCounter uint64, payset []transactions.SignedTxnWithAD) (aidx uint64) { - // Compute transaction index in block - offset := -1 - for idx, stxnib := range payset { - if tx.Txn.Txn.ID() == stxnib.Txn.ID() { - offset = idx - break - } - } - - // Sanity check that txn was in fetched block - if offset < 0 { - return 0 - } - - // Count into block to get created asset index - return txnCounter - uint64(len(payset)) + uint64(offset) + 1 -} - -// 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 computeAssetIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) uint64 { - // Must have ledger - if l == nil { - return 0 - } - // Transaction must be confirmed - if tx.ConfirmedRound == 0 { - return 0 - } - // Transaction must be AssetConfig transaction - if tx.Txn.Txn.AssetConfigTxnFields == (transactions.AssetConfigTxnFields{}) { - return 0 - } - // Transaction must be creating an asset - if tx.Txn.Txn.AssetConfigTxnFields.ConfigAsset != 0 { - return 0 - } - - aidx := uint64(tx.ApplyData.ConfigAsset) - if aidx > 0 { - return aidx - } - // If there is no ConfigAsset in the ApplyData, it must be a - // transaction before inner transactions were activated. Therefore - // the computeCreatableIndexInPayset function will work properly - // to deduce the aid. Proceed. - - // 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) -} - -// 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) 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 - } - - aidx := uint64(tx.ApplyData.ApplicationID) - if aidx > 0 { - return aidx - } - // If there is no ApplicationID in the ApplyData, it must be a - // transaction before inner transactions were activated. Therefore - // the computeCreatableIndexInPayset function will work properly - // to deduce the aidx. Proceed. - - // 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) { - block := v1.Block{ - Hash: crypto.Digest(b.Hash()).String(), - PreviousBlockHash: crypto.Digest(b.Branch).String(), - Seed: crypto.Digest(b.Seed()).String(), - Proposer: c.Proposal.OriginalProposer.String(), - Round: uint64(b.Round()), - TransactionsRoot: b.TxnCommitments.NativeSha512_256Commitment.String(), // No need to support SHA256 in API V1 - RewardsRate: b.RewardsRate, - RewardsLevel: b.RewardsLevel, - RewardsResidue: b.RewardsResidue, - Timestamp: b.TimeStamp, - - UpgradeState: v1.UpgradeState{ - CurrentProtocol: string(b.CurrentProtocol), - NextProtocol: string(b.NextProtocol), - NextProtocolApprovals: b.NextProtocolApprovals, - NextProtocolVoteBefore: uint64(b.NextProtocolVoteBefore), - NextProtocolSwitchOn: uint64(b.NextProtocolSwitchOn), - }, - UpgradeVote: v1.UpgradeVote{ - UpgradePropose: string(b.UpgradePropose), - UpgradeApprove: b.UpgradeApprove, - }, - } - - // Transactions - var txns []v1.Transaction - payset, err := b.DecodePaysetFlat() - if err != nil { - return v1.Block{}, err - } - - for _, txn := range payset { - tx := node.TxnWithStatus{ - Txn: txn.SignedTxn, - ConfirmedRound: b.Round(), - ApplyData: txn.ApplyData, - } - - encTx, err := txWithStatusEncode(tx) - if err != nil { - return v1.Block{}, err - } - - txns = append(txns, encTx) - } - - block.Transactions = v1.TransactionList{Transactions: txns} - - return block, nil -} - -// Status is an httpHandler for route GET /v1/status -func Status(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/status GetStatus - //--- - // Summary: Gets the current node status. - // Produces: - // - application/json - // Schemes: - // - http - // Responses: - // 200: - // "$ref": '#/responses/StatusResponse' - // 500: - // description: Internal Error - // schema: {type: string} - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - - nodeStatus, err := getNodeStatus(ctx.Node) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - return - } - - response := StatusResponse{&nodeStatus} - SendJSON(response, w, ctx.Log) -} - -// WaitForBlock is an httpHandler for route GET /v1/status/wait-for-block-after/{round:[0-9]+} -func WaitForBlock(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/status/wait-for-block-after/{round}/ WaitForBlock - // --- - // Summary: Gets the node status after waiting for the given round. - // Description: Waits for a block to appear after round {round} and returns the node's status at the time. - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: round - // in: path - // type: integer - // format: int64 - // minimum: 0 - // required: true - // description: The round to wait until returning status - // Responses: - // 200: - // "$ref": '#/responses/StatusResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 401: { description: Invalid API Token } - // 500: - // description: Internal Error - // schema: {type: string} - // 503: - // description: Service - // schema: {type: string} - // default: { description: Unknown Error } - - w := context.Response().Writer - - queryRound, err := strconv.ParseUint(context.Param("round"), 10, 64) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedParsingRoundNumber, ctx.Log) - return - } - - ledger := ctx.Node.Ledger() - latestBlkHdr, err := ledger.BlockHdr(ledger.Latest()) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - return - } - if latestBlkHdr.NextProtocol != "" { - if _, nextProtocolSupported := config.Consensus[latestBlkHdr.NextProtocol]; !nextProtocolSupported { - // see if the desired protocol switch is expect to happen before or after the above point. - if latestBlkHdr.NextProtocolSwitchOn <= basics.Round(queryRound+1) { - // we would never reach to this round, since this round would happen after the (unsupported) protocol upgrade. - lib.ErrorResponse(w, http.StatusBadRequest, err, errRequestedRoundInUnsupportedRound, ctx.Log) - return - } - } - } - - internalNodeStatus, err := ctx.Node.Status() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - } - - if internalNodeStatus.Catchpoint != "" { - // node is currently catching up to the requested catchpoint. - lib.ErrorResponse(w, http.StatusServiceUnavailable, fmt.Errorf("WaitForBlock failed as the node was catchpoint catchuping"), errOperationNotAvailableDuringCatchup, ctx.Log) - return - } - - select { - case <-ctx.Shutdown: - lib.ErrorResponse(w, http.StatusInternalServerError, err, errServiceShuttingDown, ctx.Log) - return - case <-time.After(1 * time.Minute): - case <-ledger.Wait(basics.Round(queryRound + 1)): - } - - nodeStatus, err := getNodeStatus(ctx.Node) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - return - } - - response := StatusResponse{&nodeStatus} - SendJSON(response, w, ctx.Log) -} - -// RawTransaction is an httpHandler for route POST /v1/transactions -func RawTransaction(ctx lib.ReqContext, context echo.Context) { - // swagger:operation POST /v1/transactions RawTransaction - // --- - // Summary: Broadcasts a raw transaction to the network. - // Produces: - // - application/json - // Consumes: - // - application/x-binary - // Schemes: - // - http - // Parameters: - // - name: rawtxn - // in: body - // schema: - // type: string - // format: binary - // required: true - // description: The byte encoded signed transaction to broadcast to network - // Responses: - // 200: - // "$ref": "#/responses/TransactionIDResponse" - // 400: - // description: Bad Request - // schema: {type: string} - // 401: { description: Invalid API Token } - // 500: - // description: Internal Error - // schema: {type: string} - // 503: - // description: Service Unavailable - // schema: {type: string} - // default: { description: Unknown Error } - - w := context.Response().Writer - r := context.Request() - - stat, err := ctx.Node.Status() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - return - } - if stat.Catchpoint != "" { - // node is currently catching up to the requested catchpoint. - lib.ErrorResponse(w, http.StatusServiceUnavailable, fmt.Errorf("RawTransaction failed as the node was catchpoint catchuping"), errOperationNotAvailableDuringCatchup, ctx.Log) - return - } - proto := config.Consensus[stat.LastVersion] - - var txgroup []transactions.SignedTxn - dec := protocol.NewDecoder(r.Body) - for { - var st transactions.SignedTxn - err := dec.Decode(&st) - if err == io.EOF { - break - } - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, err.Error(), ctx.Log) - return - } - txgroup = append(txgroup, st) - - if len(txgroup) > proto.MaxTxGroupSize { - err := fmt.Errorf("max group size is %d", proto.MaxTxGroupSize) - lib.ErrorResponse(w, http.StatusBadRequest, err, err.Error(), ctx.Log) - return - } - } - - if len(txgroup) == 0 { - err := errors.New("empty txgroup") - lib.ErrorResponse(w, http.StatusBadRequest, err, err.Error(), ctx.Log) - return - } - - err = ctx.Node.BroadcastSignedTxGroup(txgroup) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, err.Error(), ctx.Log) - return - } - - // For backwards compatibility, return txid of first tx in group - txid := txgroup[0].ID() - SendJSON(TransactionIDResponse{&v1.TransactionID{TxID: txid.String()}}, w, ctx.Log) -} - -// AccountInformation is an httpHandler for route GET /v1/account/{addr:[A-Z0-9]{KeyLength}} -func AccountInformation(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/account/{address} AccountInformation - // --- - // Summary: Get account information. - // Description: Given a specific account public key, this call returns the accounts status, balance and spendable amounts - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: address - // in: path - // type: string - // pattern: "[A-Z0-9]{58}" - // required: true - // description: An account public key - // Responses: - // 200: - // "$ref": '#/responses/AccountInformationResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 500: - // description: Internal Error - // schema: {type: string} - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - - queryAddr := context.Param("addr") - - if queryAddr == "" { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errNoAccountSpecified), errNoAccountSpecified, ctx.Log) - return - } - - addr, err := basics.UnmarshalChecksumAddress(queryAddr) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedToParseAddress, ctx.Log) - return - } - - ledger := ctx.Node.Ledger() - record, lastRound, amountWithoutPendingRewards, err := ledger.LookupLatest(basics.Address(addr)) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpLedger, ctx.Log) - return - } - - amount := record.MicroAlgos - pendingRewards, overflowed := basics.OSubA(amount, amountWithoutPendingRewards) - if overflowed { - err = fmt.Errorf("overflowed pending rewards: %v - %v", amount, amountWithoutPendingRewards) - lib.ErrorResponse(w, http.StatusInternalServerError, err, errInternalFailure, ctx.Log) - return - } - - var assets map[uint64]v1.AssetHolding - if len(record.Assets) > 0 { - assets = make(map[uint64]v1.AssetHolding) - for curid, holding := range record.Assets { - var creator string - 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 - // longer fetch the creator - creator = "" - } - assets[uint64(curid)] = v1.AssetHolding{ - Creator: creator, - Amount: holding.Amount, - Frozen: holding.Frozen, - } - } - } - - var assetParams map[uint64]v1.AssetParams - if len(record.AssetParams) > 0 { - assetParams = make(map[uint64]v1.AssetParams, len(record.AssetParams)) - for idx, params := range record.AssetParams { - 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) - } - } - - var apiParticipation *v1.Participation - if record.VoteID != (crypto.OneTimeSignatureVerifier{}) { - apiParticipation = participationKeysEncode(record) - } - - accountInfo := v1.Account{ - Round: uint64(lastRound), - Address: addr.String(), - Amount: amount.Raw, - PendingRewards: pendingRewards.Raw, - AmountWithoutPendingRewards: amountWithoutPendingRewards.Raw, - Rewards: record.RewardedMicroAlgos.Raw, - Status: record.Status.String(), - Participation: apiParticipation, - AssetParams: assetParams, - Assets: assets, - AppParams: appParams, - AppLocalStates: apps, - } - - SendJSON(AccountInformationResponse{&accountInfo}, w, ctx.Log) -} - -// TransactionInformation is an httpHandler for route GET /v1/account/{addr:[A-Z0-9]{KeyLength}}/transaction/{txid:[A-Z0-9]+} -func TransactionInformation(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/account/{address}/transaction/{txid} TransactionInformation - // --- - // Summary: Get a specific confirmed transaction. - // Description: > - // Given a wallet address and a transaction id, it returns the confirmed transaction - // information. This call scans up to .MaxTxnLife blocks in the past. - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: address - // in: path - // type: string - // pattern: "[A-Z0-9]{58}" - // required: true - // description: An account public key - // - name: txid - // in: path - // type: string - // pattern: "[A-Z0-9]+" - // required: true - // description: A transaction id - // Responses: - // 200: - // "$ref": '#/responses/TransactionResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 404: - // description: Transaction Not Found - // schema: {type: string} - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - - queryTxID := context.Param("txid") - if queryTxID == "" { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errNoTxnSpecified), errNoTxnSpecified, ctx.Log) - return - } - - txID := transactions.Txid{} - if txID.UnmarshalText([]byte(queryTxID)) != nil { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errNoTxnSpecified), errNoTxnSpecified, ctx.Log) - return - } - - queryAddr := context.Param("addr") - if queryAddr == "" { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errNoAccountSpecified), errNoAccountSpecified, ctx.Log) - return - } - - addr, err := basics.UnmarshalChecksumAddress(queryAddr) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errFailedToParseAddress), errFailedToParseAddress, ctx.Log) - return - } - - ledger := ctx.Node.Ledger() - latestRound := ledger.Latest() - stat, err := ctx.Node.Status() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - return - } - proto := config.Consensus[stat.LastVersion] - // non-Archival nodes keep proto.MaxTxnLife blocks around, - // so without the + 1 in the below calculation, - // Node.GetTransaction will query 1 round more than is kept around - start := latestRound - basics.Round(proto.MaxTxnLife) + 1 - if latestRound < basics.Round(proto.MaxTxnLife) { - start = 0 - } - - if txn, ok := ctx.Node.GetTransaction(addr, txID, start, latestRound); ok { - var responseTxs v1.Transaction - responseTxs, err = txWithStatusEncode(txn) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedToParseTransaction, ctx.Log) - return - } - - response := TransactionResponse{ - Body: &responseTxs, - } - - SendJSON(response, w, ctx.Log) - return - } - - // We didn't find it, return a failure - lib.ErrorResponse(w, http.StatusNotFound, errors.New(errTransactionNotFound), errTransactionNotFound, ctx.Log) - return -} - -// PendingTransactionInformation is an httpHandler for route GET /v1/transactions/pending/{txid:[A-Z0-9]+} -func PendingTransactionInformation(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/transactions/pending/{txid} PendingTransactionInformation - // --- - // Summary: Get a specific pending transaction. - // Description: > - // Given a transaction id of a recently submitted transaction, it returns information - // about it. There are several cases when this might succeed: - // - // - transaction committed (committed round > 0) - // - transaction still in the pool (committed round = 0, pool error = "") - // - transaction removed from pool due to error (committed round = 0, pool error != "") - // - // Or the transaction may have happened sufficiently long ago that the - // node no longer remembers it, and this will return an error. - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: txid - // in: path - // type: string - // pattern: "[A-Z0-9]+" - // required: true - // description: A transaction id - // Responses: - // 200: - // "$ref": '#/responses/TransactionResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 404: - // description: Transaction Not Found - // schema: {type: string} - // 401: { description: Invalid API Token } - // 503: - // description: Service Unavailable - // schema: {type: string} - // default: { description: Unknown Error } - - w := context.Response().Writer - - queryTxID := context.Param("txid") - if queryTxID == "" { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errNoTxnSpecified), errNoTxnSpecified, ctx.Log) - return - } - - txID := transactions.Txid{} - if txID.UnmarshalText([]byte(queryTxID)) != nil { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errNoTxnSpecified), errNoTxnSpecified, ctx.Log) - return - } - - internalNodeStatus, err := ctx.Node.Status() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - } - if internalNodeStatus.Catchpoint != "" { - // node is currently catching up to the requested catchpoint. - lib.ErrorResponse(w, http.StatusServiceUnavailable, fmt.Errorf("PendingTransactionInformation failed as the node was catchpoint catchuping"), errOperationNotAvailableDuringCatchup, ctx.Log) - return - } - - if txn, ok := ctx.Node.GetPendingTransaction(txID); ok { - ledger := ctx.Node.Ledger() - responseTxs, err := txWithStatusEncode(txn) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedToParseTransaction, ctx.Log) - return - } - - responseTxs.TransactionResults = &v1.TransactionResults{ - // This field will be omitted for transactions that did not - // 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{ - Body: &responseTxs, - } - - SendJSON(response, w, ctx.Log) - return - } - - // We didn't find it, return a failure - lib.ErrorResponse(w, http.StatusNotFound, errors.New(errTransactionNotFound), errTransactionNotFound, ctx.Log) - return -} - -// GetPendingTransactions is an httpHandler for route GET /v1/transactions/pending. -func GetPendingTransactions(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/transactions/pending GetPendingTransactions - // --- - // Summary: Get a list of unconfirmed transactions currently in the transaction pool. - // Description: > - // Get the list of pending transactions, sorted by priority, - // in decreasing order, truncated at the end at MAX. If MAX = 0, - // returns all pending transactions. - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: max - // in: query - // type: integer - // format: int64 - // minimum: 0 - // required: false - // description: Truncated number of transactions to display. If max=0, returns all pending txns. - // Responses: - // "200": - // "$ref": '#/responses/PendingTransactionsResponse' - // 401: { description: Invalid API Token } - // 500: - // description: Internal Error - // schema: {type: string} - // 503: - // description: Service Unavailable - // schema: {type: string} - // default: { description: Unknown Error } - - w := context.Response().Writer - r := context.Request() - - max, err := strconv.ParseUint(r.FormValue("max"), 10, 64) - if err != nil { - max = 0 - } - - internalNodeStatus, err := ctx.Node.Status() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - } - if internalNodeStatus.Catchpoint != "" { - // node is currently catching up to the requested catchpoint. - lib.ErrorResponse(w, http.StatusServiceUnavailable, fmt.Errorf("GetPendingTransactions failed as the node was catchpoint catchuping"), errOperationNotAvailableDuringCatchup, ctx.Log) - return - } - - txs, err := ctx.Node.GetPendingTxnsFromPool() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpTransactionPool, ctx.Log) - return - } - - totalTxns := uint64(len(txs)) - if max > 0 && totalTxns > max { - // we expose this truncating mechanism for the client only, for the flexibility - // to avoid dumping the whole pool over REST or in a cli. There is no need to optimize - // fetching a smaller transaction set at a lower level. - txs = txs[:max] - } - - responseTxs := make([]v1.Transaction, len(txs)) - for i, twr := range txs { - responseTxs[i], err = txEncode(twr.Txn, transactions.ApplyData{}) - if err != nil { - // update the error as needed - err = decorateUnknownTransactionTypeError(err, node.TxnWithStatus{Txn: twr}) - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpTransactionPool, ctx.Log) - return - } - } - - response := PendingTransactionsResponse{ - Body: &v1.PendingTransactions{ - TruncatedTxns: v1.TransactionList{ - Transactions: responseTxs, - }, - TotalTxns: totalTxns, - }, - } - - SendJSON(response, w, ctx.Log) -} - -// GetPendingTransactionsByAddress is an httpHandler for route GET /v1/account/addr:[A-Z0-9]{KeyLength}}/transactions/pending. -func GetPendingTransactionsByAddress(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/account/{addr}/transactions/pending GetPendingTransactionsByAddress - // --- - // Summary: Get a list of unconfirmed transactions currently in the transaction pool by address. - // Description: > - // Get the list of pending transactions by address, sorted by priority, - // in decreasing order, truncated at the end at MAX. If MAX = 0, - // returns all pending transactions. - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: addr - // in: path - // type: string - // pattern: "[A-Z0-9]{58}" - // required: true - // description: An account public key - // - name: max - // in: query - // type: integer - // format: int64 - // minimum: 0 - // required: false - // description: Truncated number of transactions to display. If max=0, returns all pending txns. - // Responses: - // "200": - // "$ref": '#/responses/PendingTransactionsResponse' - // 401: { description: Invalid API Token } - // 500: - // description: Internal Error - // schema: {type: string} - // 503: - // description: Service Unavailable - // schema: {type: string} - // default: { description: Unknown Error } - +// V1Sunset is a generic handler for all v1 routes that shows the sunset message. +func V1Sunset(ctx lib.ReqContext, context echo.Context) { w := context.Response().Writer - r := context.Request() - - queryMax := r.FormValue("max") - max, err := strconv.ParseUint(queryMax, 10, 64) - if queryMax != "" && err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errFailedToParseMaxValue), errFailedToParseMaxValue, ctx.Log) - return - } - - queryAddr := context.Param("addr") - if queryAddr == "" { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errNoAccountSpecified), errNoAccountSpecified, ctx.Log) - return - } - - addr, err := basics.UnmarshalChecksumAddress(queryAddr) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedToParseAddress, ctx.Log) - return - } - - internalNodeStatus, err := ctx.Node.Status() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - } - - if internalNodeStatus.Catchpoint != "" { - // node is currently catching up to the requested catchpoint. - lib.ErrorResponse(w, http.StatusServiceUnavailable, fmt.Errorf("GetPendingTransactionsByAddress failed as the node was catchpoint catchuping"), errOperationNotAvailableDuringCatchup, ctx.Log) - return - } - - txs, err := ctx.Node.GetPendingTxnsFromPool() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpTransactionPool, ctx.Log) - return - } - - responseTxs := make([]v1.Transaction, 0) - for i, twr := range txs { - if twr.Txn.Sender == addr || twr.Txn.Receiver == addr { - // truncate in case max was passed - if max > 0 && uint64(i) > max { - break - } - - tx, err := txEncode(twr.Txn, transactions.ApplyData{}) - responseTxs = append(responseTxs, tx) - if err != nil { - // update the error as needed - err = decorateUnknownTransactionTypeError(err, node.TxnWithStatus{Txn: twr}) - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpTransactionPool, ctx.Log) - return - } - } - } - - response := PendingTransactionsResponse{ - Body: &v1.PendingTransactions{ - TruncatedTxns: v1.TransactionList{ - Transactions: responseTxs, - }, - TotalTxns: uint64(len(responseTxs)), - }, - } - - SendJSON(response, w, ctx.Log) -} - -// AssetInformation is an httpHandler for route GET /v1/asset/{index:[0-9]+} -func AssetInformation(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/asset/{index} AssetInformation - // --- - // Summary: Get asset information. - // Description: > - // Given the asset's unique index, this call returns the asset's creator, - // manager, reserve, freeze, and clawback addresses - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: index - // in: path - // type: integer - // format: int64 - // required: true - // description: Asset index - // Responses: - // 200: - // "$ref": '#/responses/AssetInformationResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 500: - // description: Internal Error - // schema: {type: string} - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - - queryIndex, err := strconv.ParseUint(context.Param("index"), 10, 64) - - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedToParseAssetIndex, ctx.Log) - return - } - - ledger := ctx.Node.Ledger() - aidx := basics.AssetIndex(queryIndex) - 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 - } - - lastRound := ledger.Latest() - resource, err := ledger.LookupAsset(lastRound, creator, aidx) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpLedger, ctx.Log) - return - } - - if resource.AssetParams != nil { - thisAssetParams := modelAssetParams(creator, *resource.AssetParams) - SendJSON(AssetInformationResponse{&thisAssetParams}, w, ctx.Log) - } else { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errFailedRetrievingAsset), errFailedRetrievingAsset, ctx.Log) - return - } -} - -// Assets is an httpHandler for route GET /v1/assets -func Assets(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/assets Assets - // --- - // Summary: List assets - // Description: Returns list of up to `max` assets, where the maximum assetIdx is <= `assetIdx` - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: assetIdx - // in: query - // type: integer - // format: int64 - // minimum: 0 - // required: false - // description: Fetch assets with asset index <= assetIdx. If zero, fetch most recent assets. - // - name: max - // in: query - // type: integer - // format: int64 - // minimum: 0 - // maximum: 100 - // required: false - // description: Fetch no more than this many assets - // Responses: - // 200: - // "$ref": '#/responses/AssetsResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 500: - // description: Internal Error - // schema: {type: string} - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - r := context.Request() - - const maxAssetsToList = 100 - - var err error - var max int64 = maxAssetsToList - var assetIdx int64 = 0 - - // Parse max assets to fetch from db - if r.PostFormValue("max") != "" { - max, err = strconv.ParseInt(r.FormValue("max"), 10, 64) - if err != nil || max < 0 || max > maxAssetsToList { - err := fmt.Errorf(errFailedParsingMaxAssetsToList, 0, maxAssetsToList) - lib.ErrorResponse(w, http.StatusBadRequest, err, err.Error(), ctx.Log) - return - } - } - - // Parse maximum asset idx - if r.PostFormValue("assetIdx") != "" { - assetIdx, err = strconv.ParseInt(r.FormValue("assetIdx"), 10, 64) - if err != nil || assetIdx < 0 { - errs := errFailedParsingAssetIdx - lib.ErrorResponse(w, http.StatusBadRequest, errors.New(errs), errs, ctx.Log) - return - } - } - - // If assetIdx is 0, we want the most recent assets, so make it intmax - if assetIdx == 0 { - assetIdx = (1 << 63) - 1 - } - - // Query asset range from the database - ledger := ctx.Node.Ledger() - alocs, err := ledger.ListAssets(basics.AssetIndex(assetIdx), uint64(max)) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingAsset, ctx.Log) - return - } - - // Fill in the asset models - lastRound := ledger.Latest() - var result v1.AssetList - for _, aloc := range alocs { - // Fetch the asset parameters - record, err := ledger.LookupAsset(lastRound, aloc.Creator, basics.AssetIndex(aloc.Index)) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpLedger, ctx.Log) - return - } - - if record.AssetParams == nil { - continue - } - - // Append the result - params := modelAssetParams(aloc.Creator, *record.AssetParams) - result.Assets = append(result.Assets, v1.Asset{ - AssetIndex: uint64(aloc.Index), - AssetParams: params, - }) - } - - SendJSON(AssetsResponse{&result}, w, ctx.Log) -} - -// SuggestedFee is an httpHandler for route GET /v1/transactions/fee -func SuggestedFee(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/transactions/fee SuggestedFee - // --- - // Summary: Get the suggested fee - // Description: > - // Suggested Fee is returned in units of micro-Algos per byte. - // Suggested Fee may fall to zero but submitted transactions - // must still have a fee of at least MinTxnFee for the current - // network protocol. - // Produces: - // - application/json - // Schemes: - // - http - // Responses: - // "200": - // "$ref": '#/responses/TransactionFeeResponse' - // 401: { description: Invalid API Token } - // 503: - // description: Service Unavailable - // schema: {type: string} - // default: { description: Unknown Error } - - w := context.Response().Writer - - internalNodeStatus, err := ctx.Node.Status() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - } - - if internalNodeStatus.Catchpoint != "" { - // node is currently catching up to the requested catchpoint. - lib.ErrorResponse(w, http.StatusServiceUnavailable, fmt.Errorf("SuggestedFee failed as the node was catchpoint catchuping"), errOperationNotAvailableDuringCatchup, ctx.Log) - return - } - - fee := v1.TransactionFee{Fee: ctx.Node.SuggestedFee().Raw} - SendJSON(TransactionFeeResponse{&fee}, w, ctx.Log) -} - -// SuggestedParams is an httpHandler for route GET /v1/transactions/params -func SuggestedParams(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/transactions/params TransactionParams - // --- - // Summary: Get parameters for constructing a new transaction - // Produces: - // - application/json - // Schemes: - // - http - // Responses: - // "200": - // "$ref": '#/responses/TransactionParamsResponse' - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - - stat, err := ctx.Node.Status() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedRetrievingNodeStatus, ctx.Log) - return - } - if stat.Catchpoint != "" { - // node is currently catching up to the requested catchpoint. - lib.ErrorResponse(w, http.StatusServiceUnavailable, fmt.Errorf("SuggestedParams failed as the node was catchpoint catchuping"), errOperationNotAvailableDuringCatchup, ctx.Log) - return - } - - gh := ctx.Node.GenesisHash() - - var params v1.TransactionParams - params.Fee = ctx.Node.SuggestedFee().Raw - params.GenesisID = ctx.Node.GenesisID() - params.GenesisHash = gh[:] - params.LastRound = uint64(stat.LastRound) - params.ConsensusVersion = string(stat.LastVersion) - - proto := config.Consensus[stat.LastVersion] - params.MinTxnFee = proto.MinTxnFee - - SendJSON(TransactionParamsResponse{¶ms}, w, ctx.Log) -} - -// GetBlock is an httpHandler for route GET /v1/block/{round} -func GetBlock(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/block/{round} GetBlock - // --- - // Summary: Get the block for the given round. - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: round - // in: path - // type: integer - // format: int64 - // minimum: 0 - // required: true - // description: The round from which to fetch block information. - // - name: raw - // in: query - // type: integer - // format: int64 - // required: false - // description: Return raw msgpack block bytes - // Responses: - // 200: - // "$ref": '#/responses/BlockResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 500: - // description: Internal Error - // schema: {type: string} - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - r := context.Request() - - queryRound, err := strconv.ParseUint(context.Param("round"), 10, 64) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedParsingRoundNumber, ctx.Log) - return - } - - // raw msgpack option: - rawstr := r.FormValue("raw") - if rawstr != "" { - rawint, err := strconv.ParseUint(rawstr, 10, 64) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedParsingRawOption, ctx.Log) - return - } - if rawint != 0 { - blockbytes, err := rpcs.RawBlockBytes(ctx.Node.Ledger(), basics.Round(queryRound)) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpLedger, ctx.Log) - return - } - w.Header().Set("Content-Type", rpcs.BlockResponseContentType) - w.Header().Set("Content-Length", strconv.Itoa(len(blockbytes))) - w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") - w.WriteHeader(http.StatusOK) - _, err = w.Write(blockbytes) - if err != nil { - ctx.Log.Warnf("algod failed to write an object to the response stream: %v", err) - } - return - } - } - - // decoded json-reencoded default: - ledger := ctx.Node.Ledger() - b, c, err := ledger.BlockCert(basics.Round(queryRound)) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpLedger, ctx.Log) - return - } - - if len(c.Votes) == 0 && c.Round > basics.Round(0) { - lib.ErrorResponse(w, http.StatusNotFound, err, errCertificateIsMissingFromBlock, ctx.Log) - return - } - - block, err := blockEncode(b, c) - - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errInternalFailure, ctx.Log) - return - } - - SendJSON(BlockResponse{&block}, w, ctx.Log) -} - -// GetSupply is an httpHandler for route GET /v1/ledger/supply -func GetSupply(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/ledger/supply GetSupply - //--- - // Summary: Get the current supply reported by the ledger. - // Produces: - // - application/json - // Schemes: - // - http - // Responses: - // 200: - // "$ref": '#/responses/SupplyResponse' - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - - latest, totals, err := ctx.Node.Ledger().LatestTotals() - if err != nil { - err = fmt.Errorf("GetSupply(): round %d failed: %v", latest, err) - lib.ErrorResponse(w, http.StatusInternalServerError, err, errInternalFailure, ctx.Log) - return - } - supply := v1.Supply{ - Round: uint64(latest), - TotalMoney: totals.Participating().Raw, - OnlineMoney: totals.Online.Money.Raw, - } - SendJSON(SupplyResponse{&supply}, w, ctx.Log) -} - -func parseTime(t string) (res time.Time, err error) { - // check for just date - res, err = time.Parse("2006-01-02", t) - if err == nil { - return - } - - // check for date and time - res, err = time.Parse(time.RFC3339, t) - if err == nil { - return - } - - return -} - -// Transactions is an httpHandler for route GET /v1/account/{addr:[A-Z0-9]+}/transactions -func Transactions(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/account/{address}/transactions Transactions - // --- - // Summary: Get a list of confirmed transactions. - // Description: Returns the list of confirmed transactions between within a date range. When indexer is disabled this call requires firstRound and lastRound and returns an error if firstRound is not available to the node. The transaction results start from the oldest round. - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: address - // in: path - // type: string - // pattern: "[A-Z0-9]{58}" - // required: true - // description: An account public key - // - name: firstRound - // in: query - // type: integer - // format: int64 - // minimum: 0 - // required: false - // description: Do not fetch any transactions before this round. - // - name: lastRound - // in: query - // type: integer - // format: int64 - // minimum: 0 - // required: false - // description: Do not fetch any transactions after this round. - // - name: fromDate - // in: query - // type: string - // format: date - // required: false - // description: Do not fetch any transactions before this date. (enabled only with indexer) - // - name: toDate - // in: query - // type: string - // format: date - // required: false - // description: Do not fetch any transactions after this date. (enabled only with indexer) - // - name: max - // in: query - // type: integer - // format: int64 - // required: false - // description: maximum transactions to show (default to 100) - // Responses: - // 200: - // "$ref": '#/responses/TransactionsResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 500: - // description: Internal Error - // schema: {type: string} - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - r := context.Request() - - queryAddr := context.Param("addr") - addr, err := basics.UnmarshalChecksumAddress(queryAddr) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedToParseAddress, ctx.Log) - return - } - - max, err := strconv.ParseUint(r.FormValue("max"), 10, 64) - if err != nil { - max = 100 - } - - // Get different params - firstRound := r.FormValue("firstRound") - lastRound := r.FormValue("lastRound") - fromDate := r.FormValue("fromDate") - toDate := r.FormValue("toDate") - - var rounds []uint64 - var txs []node.TxnWithStatus - // Were rounds provided? - if firstRound != "" && lastRound != "" { - // Are they valid? - fR, err := strconv.ParseUint(firstRound, 10, 64) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedParsingRoundNumber, ctx.Log) - return - } - - lR, err := strconv.ParseUint(lastRound, 10, 64) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errFailedParsingRoundNumber, ctx.Log) - return - } - - txs, err = ctx.Node.ListTxns(addr, basics.Round(fR), basics.Round(lR)) - if err != nil { - switch err.(type) { - case ledgercore.ErrNoEntry: - if !ctx.Node.IsArchival() { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errBlockHashBeenDeletedArchival, ctx.Log) - return - } - } - - lib.ErrorResponse(w, http.StatusInternalServerError, err, err.Error(), ctx.Log) - return - } - - } else { - // is indexer on? - indexer, err := ctx.Node.Indexer() - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, errNoRoundsSpecified, ctx.Log) - return - } - - // Were dates provided? - if fromDate != "" && toDate != "" { - fd, err := parseTime(fromDate) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, err.Error(), ctx.Log) - return - } - - td, err := parseTime(toDate) - if err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, err.Error(), ctx.Log) - return - } - - rounds, err = indexer.GetRoundsByAddressAndDate(addr.String(), max, fd.Unix(), td.Unix()) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, err.Error(), ctx.Log) - return - } - - } else { - // return last [max] transactions - rounds, err = indexer.GetRoundsByAddress(addr.String(), max) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedGettingInformationFromIndexer, ctx.Log) - return - } - } - } - - if len(rounds) > 0 { - for _, rnd := range rounds { - txns, _ := ctx.Node.ListTxns(addr, basics.Round(rnd), basics.Round(rnd)) - txs = append(txs, txns...) - - // They may be more txns in the round than requested, break. - if uint64(len(txs)) > max { - break - } - } - } - - // clip length to [max] - if uint64(len(txs)) > max { - txs = txs[:max] - } - - responseTxs := make([]v1.Transaction, len(txs)) - for i, twr := range txs { - responseTxs[i], err = txWithStatusEncode(twr) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedToParseTransaction, ctx.Log) - return - } - } - - response := TransactionsResponse{ - &v1.TransactionList{ - Transactions: responseTxs, - }, - } - - SendJSON(response, w, ctx.Log) -} - -// GetTransactionByID is an httpHandler for route GET /v1/transaction/{txid} -func GetTransactionByID(ctx lib.ReqContext, context echo.Context) { - // swagger:operation GET /v1/transaction/{txid} Transaction - // --- - // Summary: Get an information of a single transaction. - // Description: Returns the transaction information of the given txid. Works only if the indexer is enabled. - // Produces: - // - application/json - // Schemes: - // - http - // Parameters: - // - name: txid - // in: path - // type: string - // pattern: "[A-Z0-9]+" - // required: true - // description: A transaction id - // Responses: - // 200: - // "$ref": '#/responses/TransactionResponse' - // 400: - // description: Bad Request - // schema: {type: string} - // 404: - // description: Transaction Not Found - // schema: {type: string} - // 401: { description: Invalid API Token } - // default: { description: Unknown Error } - - w := context.Response().Writer - - indexer, err := ctx.Node.Indexer() - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errIndexerNotRunning, ctx.Log) - return - } - - queryTxID := context.Param("txid") - if queryTxID == "" { - lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errNoTxnSpecified), errNoTxnSpecified, ctx.Log) - return - } - - var txID transactions.Txid - if err := txID.UnmarshalText([]byte(queryTxID)); err != nil { - lib.ErrorResponse(w, http.StatusBadRequest, err, err.Error(), ctx.Log) - return - } - - rnd, err := indexer.GetRoundByTXID(queryTxID) - if err == sql.ErrNoRows { - lib.ErrorResponse(w, http.StatusNotFound, err, errTransactionNotFound, ctx.Log) - return - } - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedGettingInformationFromIndexer, ctx.Log) - return - } - - if txn, err := ctx.Node.GetTransactionByID(txID, basics.Round(rnd)); err == nil { - var responseTxs v1.Transaction - responseTxs, err = txWithStatusEncode(txn) - if err != nil { - lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedToParseTransaction, ctx.Log) - return - } - - response := TransactionResponse{ - Body: &responseTxs, - } - - SendJSON(response, w, ctx.Log) - return - } - // We didn't find it, return a failure - lib.ErrorResponse(w, http.StatusNotFound, errors.New(errTransactionNotFound), errTransactionNotFound, ctx.Log) - return + lib.ErrorResponse(w, http.StatusGone, errors.New(errV1Sunset), errV1Sunset, ctx.Log) } diff --git a/daemon/algod/api/server/v1/handlers/handlers_test.go b/daemon/algod/api/server/v1/handlers/handlers_test.go deleted file mode 100644 index 6e54c9118b..0000000000 --- a/daemon/algod/api/server/v1/handlers/handlers_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (C) 2019-2022 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 handlers - -import ( - "errors" - "fmt" - "testing" - - "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/protocol" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" -) - -func TestDecorateUnknownTransactionTypeError(t *testing.T) { - partitiontest.PartitionTest(t) - type TestCase struct { - err error - txn node.TxnWithStatus - expectedOutcome error - } - - paymentTx := transactions.Transaction{Type: protocol.PaymentTx} - keyregTx := transactions.Transaction{Type: protocol.KeyRegistrationTx} - signedPaymentTx := transactions.SignedTxn{Txn: paymentTx} - signedKeyregTx := transactions.SignedTxn{Txn: keyregTx} - - testCases := []TestCase{ - { - err: errors.New(errBlockHashBeenDeletedArchival), - expectedOutcome: errors.New(errBlockHashBeenDeletedArchival), - }, - { - err: errors.New(errUnknownTransactionType), - txn: node.TxnWithStatus{Txn: signedPaymentTx, ConfirmedRound: basics.Round(12345)}, - expectedOutcome: fmt.Errorf(errInvalidTransactionTypeLedger, paymentTx.Type, paymentTx.ID().String(), basics.Round(12345)), - }, - { - err: errors.New(errUnknownTransactionType), - txn: node.TxnWithStatus{Txn: signedKeyregTx, ConfirmedRound: basics.Round(5678)}, - expectedOutcome: fmt.Errorf(errInvalidTransactionTypeLedger, keyregTx.Type, keyregTx.ID().String(), basics.Round(5678)), - }, - { - err: errors.New(errUnknownTransactionType), - txn: node.TxnWithStatus{Txn: signedPaymentTx}, - expectedOutcome: fmt.Errorf(errInvalidTransactionTypePending, paymentTx.Type, paymentTx.ID().String()), - }, - { - err: errors.New(errUnknownTransactionType), - txn: node.TxnWithStatus{Txn: signedKeyregTx}, - expectedOutcome: fmt.Errorf(errInvalidTransactionTypePending, keyregTx.Type, keyregTx.ID().String()), - }, - } - for _, testCase := range testCases { - outcome := decorateUnknownTransactionTypeError(testCase.err, testCase.txn) - require.Equal(t, outcome.Error(), testCase.expectedOutcome.Error()) - } -} diff --git a/daemon/algod/api/server/v1/handlers/responses.go b/daemon/algod/api/server/v1/handlers/responses.go deleted file mode 100644 index 16f8a51b09..0000000000 --- a/daemon/algod/api/server/v1/handlers/responses.go +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (C) 2019-2022 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 handlers handles and helps specify the algod/api -// -// Currently, server implementation annotations serve -// as the API ground truth. From that, we use go-swagger -// to generate a swagger spec. -// -// IF YOU MODIFY THIS PACKAGE: IMPORTANT -// MAKE SURE YOU REGENERATE THE SWAGGER SPEC (using go:generate) -// MAKE SURE IT VALIDATES -package handlers - -import ( - "encoding/json" - "io" - "net/http" - - v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - "github.com/algorand/go-algorand/logging" -) - -// Response is a generic interface wrapping any data returned by the server. -// We wrap every type in a Response type so that we can swagger annotate them. -// -// Each response must have a Body (a payload). We -// write an interface for this because it better mirrors the -// go-swagger annotation style (which requires swagger colon responses -// to have an embedded body struct of the actual data to be sent. of -// course, they can also have headers and the sort.) -// Anything implementing the Response interface will naturally be -// able to be annotated by swagger:response. This also allows package -// functions to naturally unwrap Response types and send the underlying -// Body through another interface (e.g. an http.ResponseWriter) -type Response interface { - getBody() interface{} -} - -func writeJSON(obj interface{}, w io.Writer) error { - enc := json.NewEncoder(w) - return enc.Encode(obj) -} - -// SendJSON is like writeJSON, but it writes to the log instead of returning an error. -// The caller must ensure that no writes to w happen after this function is called. -// Unwraps a Response object and converts it to an HTTP Response. -func SendJSON(obj Response, w http.ResponseWriter, log logging.Logger) { - w.Header().Set("Content-Type", "application/json") - err := writeJSON(obj.getBody(), w) - if err != nil { - log.Warnf("algod failed to write an object to the response stream: %v", err) - } -} - -// StatusResponse contains the node's status information -// -// swagger:response StatusResponse -type StatusResponse struct { - // in: body - Body *v1.NodeStatus -} - -func (sr StatusResponse) getBody() interface{} { - return sr.Body -} - -// TransactionIDResponse contains a transaction information -// -// swagger:response TransactionIDResponse -type TransactionIDResponse struct { - // in: body - Body *v1.TransactionID -} - -func (r TransactionIDResponse) getBody() interface{} { - return r.Body -} - -// AccountInformationResponse contains an account information -// -// swagger:response AccountInformationResponse -type AccountInformationResponse struct { - // in: body - Body *v1.Account -} - -func (r AccountInformationResponse) getBody() interface{} { - return r.Body -} - -// TransactionResponse contains a transaction information -// -// swagger:response TransactionResponse -type TransactionResponse struct { - // in: body - Body *v1.Transaction -} - -func (r TransactionResponse) getBody() interface{} { - return r.Body -} - -// TransactionsResponse contains a list of transactions -// -// swagger:response TransactionsResponse -type TransactionsResponse struct { - // in: body - Body *v1.TransactionList -} - -func (r TransactionsResponse) getBody() interface{} { - return r.Body -} - -// AssetsResponse contains a list of assets -// -// swagger:response AssetsResponse -type AssetsResponse struct { - // in: body - Body *v1.AssetList -} - -func (r AssetsResponse) getBody() interface{} { - return r.Body -} - -// AssetInformationResponse contains asset information -// -// swagger:response AssetInformationResponse -type AssetInformationResponse struct { - // in: body - Body *v1.AssetParams -} - -func (r AssetInformationResponse) getBody() interface{} { - return r.Body -} - -// TransactionFeeResponse contains a suggested fee -// -// swagger:response TransactionFeeResponse -type TransactionFeeResponse struct { - // in: body - Body *v1.TransactionFee -} - -func (r TransactionFeeResponse) getBody() interface{} { - return r.Body -} - -// TransactionParamsResponse contains the parameters for -// constructing a new transaction. -// -// swagger:response TransactionParamsResponse -type TransactionParamsResponse struct { - // in: body - Body *v1.TransactionParams -} - -func (r TransactionParamsResponse) getBody() interface{} { - return r.Body -} - -// RawBlockResponse contains encoded, raw block information -// -// swagger:ignore -type RawBlockResponse struct { - // in: body - Body *v1.RawBlock -} - -func (r RawBlockResponse) getBody() interface{} { - return r.Body -} - -// BlockResponse contains block information -// -// swagger:response BlockResponse -type BlockResponse struct { - // in: body - Body *v1.Block -} - -func (r BlockResponse) getBody() interface{} { - return r.Body -} - -// SupplyResponse contains the ledger supply information -// -// swagger:response SupplyResponse -type SupplyResponse struct { - // in: body - Body *v1.Supply -} - -func (r SupplyResponse) getBody() interface{} { - return r.Body -} - -/* Errors */ - -// PendingTransactionsResponse contains a (potentially truncated) list of transactions and -// the total number of transactions currently in the pool. -// -// swagger:response PendingTransactionsResponse -type PendingTransactionsResponse struct { - // in: body - Body *v1.PendingTransactions -} - -func (r PendingTransactionsResponse) getBody() interface{} { - return r.Body -} diff --git a/daemon/algod/api/server/v1/routes/routes.go b/daemon/algod/api/server/v1/routes/routes.go index 94496e294d..d5f1e3a4b9 100644 --- a/daemon/algod/api/server/v1/routes/routes.go +++ b/daemon/algod/api/server/v1/routes/routes.go @@ -21,114 +21,112 @@ import ( "github.com/algorand/go-algorand/daemon/algod/api/server/v1/handlers" ) -// KeyLength is the an Algorand's public address length -const KeyLength = 58 - // V1Routes contains all routes for v1 +// v1 algod paths will route to the sunset message, resulting in a 410 Gone response. var V1Routes = lib.Routes{ lib.Route{ Name: "status", Method: "GET", Path: "/status", - HandlerFunc: handlers.Status, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "wait-for-block", Method: "GET", Path: "/status/wait-for-block-after/:round", - HandlerFunc: handlers.WaitForBlock, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "raw-transaction", Method: "POST", Path: "/transactions", - HandlerFunc: handlers.RawTransaction, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "account-information", Method: "GET", Path: "/account/:addr", - HandlerFunc: handlers.AccountInformation, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "transaction-information", Method: "GET", Path: "/account/:addr/transaction/:txid", - HandlerFunc: handlers.TransactionInformation, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "suggested-fee", Method: "GET", Path: "/transactions/fee", - HandlerFunc: handlers.SuggestedFee, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "suggested-params", Method: "GET", Path: "/transactions/params", - HandlerFunc: handlers.SuggestedParams, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "transactions", Method: "GET", Path: "/account/:addr/transactions", - HandlerFunc: handlers.Transactions, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "block", Method: "GET", Path: "/block/:round", - HandlerFunc: handlers.GetBlock, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "ledger-supply", Method: "GET", Path: "/ledger/supply", - HandlerFunc: handlers.GetSupply, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "list-pending-transactions", Method: "GET", Path: "/transactions/pending", - HandlerFunc: handlers.GetPendingTransactions, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "pending-transaction-information", Method: "GET", Path: "/transactions/pending/:txid", - HandlerFunc: handlers.PendingTransactionInformation, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "pending-transaction-information-by-address", Method: "GET", Path: "/account/:addr/transactions/pending", - HandlerFunc: handlers.GetPendingTransactionsByAddress, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "asset-information-by-id", Method: "GET", Path: "/asset/:index", - HandlerFunc: handlers.AssetInformation, + HandlerFunc: handlers.V1Sunset, }, lib.Route{ Name: "list-assets", Method: "GET", Path: "/assets", - HandlerFunc: handlers.Assets, + HandlerFunc: handlers.V1Sunset, }, // ----- This can only be active when indexer is live @@ -137,6 +135,6 @@ var V1Routes = lib.Routes{ Name: "get-transaction-by-id", Method: "GET", Path: "/transaction/:txid", - HandlerFunc: handlers.GetTransactionByID, + HandlerFunc: handlers.V1Sunset, }, } diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go index 10971a622a..4c2392c06f 100644 --- a/daemon/algod/api/server/v2/account.go +++ b/daemon/algod/api/server/v2/account.go @@ -28,6 +28,15 @@ import ( "github.com/algorand/go-algorand/data/basics" ) +// AssetHolding converts between basics.AssetHolding and model.AssetHolding +func AssetHolding(ah basics.AssetHolding, ai basics.AssetIndex) model.AssetHolding { + return model.AssetHolding{ + Amount: ah.Amount, + AssetID: uint64(ai), + IsFrozen: ah.Frozen, + } +} + // AccountDataToAccount converts basics.AccountData to v2.model.Account func AccountDataToAccount( address string, record *basics.AccountData, @@ -39,11 +48,7 @@ func AccountDataToAccount( for curid, holding := range record.Assets { // Empty is ok, asset may have been deleted, so we can no // longer fetch the creator - holding := model.AssetHolding{ - Amount: holding.Amount, - AssetID: uint64(curid), - IsFrozen: holding.Frozen, - } + holding := AssetHolding(holding, curid) assets = append(assets, holding) } @@ -86,15 +91,7 @@ func AccountDataToAccount( appsLocalState := make([]model.ApplicationLocalState, 0, len(record.AppLocalStates)) for appIdx, state := range record.AppLocalStates { - localState := convertTKVToGenerated(&state.KeyValue) - appsLocalState = append(appsLocalState, model.ApplicationLocalState{ - Id: uint64(appIdx), - KeyValue: localState, - Schema: model.ApplicationStateSchema{ - NumByteSlice: state.Schema.NumByteSlice, - NumUint: state.Schema.NumUint, - }, - }) + appsLocalState = append(appsLocalState, AppLocalState(state, appIdx)) } sort.Slice(appsLocalState, func(i, j int) bool { return appsLocalState[i].Id < appsLocalState[j].Id @@ -447,6 +444,19 @@ func AppParamsToApplication(creator string, appIdx basics.AppIndex, appParams *b return app } +// AppLocalState converts between basics.AppLocalState and model.ApplicationLocalState +func AppLocalState(state basics.AppLocalState, appIdx basics.AppIndex) model.ApplicationLocalState { + localState := convertTKVToGenerated(&state.KeyValue) + return model.ApplicationLocalState{ + Id: uint64(appIdx), + KeyValue: localState, + Schema: model.ApplicationStateSchema{ + NumByteSlice: state.Schema.NumByteSlice, + NumUint: state.Schema.NumUint, + }, + } +} + // AssetParamsToAsset converts basics.AssetParams to model.Asset func AssetParamsToAsset(creator string, idx basics.AssetIndex, params *basics.AssetParams) model.Asset { frozen := params.DefaultFrozen diff --git a/daemon/algod/api/server/v2/delta.go b/daemon/algod/api/server/v2/delta.go new file mode 100644 index 0000000000..d97cc2d426 --- /dev/null +++ b/daemon/algod/api/server/v2/delta.go @@ -0,0 +1,168 @@ +// Copyright (C) 2019-2022 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" + "fmt" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/ledgercore" +) + +// convertAppResourceRecordToGenerated takes ledgercore.AppResourceRecord and converts it to v2.model.AppResourceRecord +func convertAppResourceRecordToGenerated(app ledgercore.AppResourceRecord) model.AppResourceRecord { + var appLocalState *model.ApplicationLocalState = nil + if app.State.LocalState != nil { + s := AppLocalState(*app.State.LocalState, app.Aidx) + appLocalState = &s + } + var appParams *model.ApplicationParams = nil + if app.Params.Params != nil { + p := AppParamsToApplication(app.Addr.String(), app.Aidx, app.Params.Params).Params + appParams = &p + } + return model.AppResourceRecord{ + Address: app.Addr.String(), + AppIndex: uint64(app.Aidx), + AppDeleted: app.Params.Deleted, + AppParams: appParams, + AppLocalStateDeleted: app.State.Deleted, + AppLocalState: appLocalState, + } +} + +// convertAssetResourceRecordToGenerated takes ledgercore.AppResourceRecord and converts it to v2.model.AppResourceRecord +func convertAssetResourceRecordToGenerated(asset ledgercore.AssetResourceRecord) model.AssetResourceRecord { + var assetHolding *model.AssetHolding = nil + if asset.Holding.Holding != nil { + a := AssetHolding(*asset.Holding.Holding, asset.Aidx) + assetHolding = &a + } + var assetParams *model.AssetParams = nil + if asset.Params.Params != nil { + a := AssetParamsToAsset(asset.Addr.String(), asset.Aidx, asset.Params.Params) + assetParams = &a.Params + } + return model.AssetResourceRecord{ + Address: asset.Addr.String(), + AssetIndex: uint64(asset.Aidx), + AssetHoldingDeleted: asset.Holding.Deleted, + AssetHolding: assetHolding, + AssetParams: assetParams, + AssetDeleted: asset.Params.Deleted, + } +} + +// stateDeltaToLedgerDelta converts ledgercore.StateDelta to v2.model.LedgerStateDelta +func stateDeltaToLedgerDelta(sDelta ledgercore.StateDelta, consensus config.ConsensusParams, rewardsLevel uint64, round uint64) (response model.LedgerStateDelta, err error) { + var accts []model.AccountBalanceRecord + var apps []model.AppResourceRecord + var assets []model.AssetResourceRecord + var keyValues []model.KvDelta + var modifiedApps []model.ModifiedApp + var modifiedAssets []model.ModifiedAsset + var txLeases []model.TxLease + + for key, kvDelta := range sDelta.KvMods { + var keyBytes = []byte(key) + keyValues = append(keyValues, model.KvDelta{ + Key: &keyBytes, + Value: &kvDelta.Data, + }) + } + + for _, record := range sDelta.Accts.Accts { + var ot basics.OverflowTracker + pendingRewards := basics.PendingRewards(&ot, consensus, record.MicroAlgos, record.RewardsBase, rewardsLevel) + + amountWithoutPendingRewards, overflowed := basics.OSubA(record.MicroAlgos, pendingRewards) + if overflowed { + return response, errors.New("overflow on pending reward calculation") + } + + ad := basics.AccountData{} + ledgercore.AssignAccountData(&ad, record.AccountData) + a, err := AccountDataToAccount(record.Addr.String(), &ad, basics.Round(round), &consensus, amountWithoutPendingRewards) + if err != nil { + return response, err + } + + accts = append(accts, model.AccountBalanceRecord{ + AccountData: a, + Address: record.Addr.String(), + }) + } + + for _, app := range sDelta.Accts.GetAllAppResources() { + apps = append(apps, convertAppResourceRecordToGenerated(app)) + } + + for _, asset := range sDelta.Accts.GetAllAssetResources() { + assets = append(assets, convertAssetResourceRecordToGenerated(asset)) + } + + for createIdx, mod := range sDelta.Creatables { + switch mod.Ctype { + case basics.AppCreatable: + modifiedApps = append(modifiedApps, model.ModifiedApp{ + Created: mod.Created, + Creator: mod.Creator.String(), + Id: uint64(createIdx), + }) + case basics.AssetCreatable: + modifiedAssets = append(modifiedAssets, model.ModifiedAsset{ + Created: mod.Created, + Creator: mod.Creator.String(), + Id: uint64(createIdx), + }) + default: + return response, fmt.Errorf("unable to determine type of creatable for modified creatable with index %d", createIdx) + } + } + + for lease, expRnd := range sDelta.Txleases { + txLeases = append(txLeases, model.TxLease{ + Expiration: uint64(expRnd), + Lease: lease.Lease[:], + Sender: lease.Sender.String(), + }) + } + + response = model.LedgerStateDeltaResponse{ + Accts: &model.AccountDeltas{ + Accounts: &accts, + Apps: &apps, + Assets: &assets, + }, + ModifiedApps: &modifiedApps, + ModifiedAssets: &modifiedAssets, + KvMods: &keyValues, + PrevTimestamp: numOrNil(uint64(sDelta.PrevTimestamp)), + StateProofNext: numOrNil(uint64(sDelta.StateProofNext)), + Totals: &model.AccountTotals{ + NotParticipating: sDelta.Totals.NotParticipating.Money.Raw, + Offline: sDelta.Totals.Offline.Money.Raw, + Online: sDelta.Totals.Online.Money.Raw, + RewardsLevel: sDelta.Totals.RewardsLevel, + }, + TxLeases: &txLeases, + } + return +} diff --git a/daemon/algod/api/server/v2/delta_test.go b/daemon/algod/api/server/v2/delta_test.go new file mode 100644 index 0000000000..921798d28c --- /dev/null +++ b/daemon/algod/api/server/v2/delta_test.go @@ -0,0 +1,143 @@ +// Copyright (C) 2019-2022 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/model" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" +) + +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 txLease = [32]byte{} + +func TestDelta(t *testing.T) { + partitiontest.PartitionTest(t) + original := ledgercore.StateDelta{ + Accts: ledgercore.AccountDeltas{ + Accts: []ledgercore.BalanceRecord{ + { + Addr: poolAddr, + AccountData: ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{ + Status: 0, + MicroAlgos: basics.MicroAlgos{Raw: 5000}, + RewardsBase: 2, + RewardedMicroAlgos: basics.MicroAlgos{Raw: 0}, + TotalExtraAppPages: 0, + TotalAppParams: 0, + TotalAppLocalStates: 0, + TotalAssetParams: 0, + TotalAssets: 0, + TotalBoxes: 0, + TotalBoxBytes: 0, + }, + }, + }, + }, + AppResources: []ledgercore.AppResourceRecord{ + { + Aidx: basics.AppIndex(2), + Addr: poolAddr, + Params: ledgercore.AppParamsDelta{ + Params: &basics.AppParams{ + ApprovalProgram: []byte("1"), + ClearStateProgram: []byte("2"), + GlobalState: basics.TealKeyValue{}, + StateSchemas: basics.StateSchemas{}, + ExtraProgramPages: 0, + }, + Deleted: false, + }, + }, + }, + AssetResources: []ledgercore.AssetResourceRecord{ + { + Aidx: basics.AssetIndex(1), + Addr: poolAddr, + Params: ledgercore.AssetParamsDelta{ + Params: nil, + Deleted: true, + }, + }, + }, + }, + KvMods: map[string]ledgercore.KvValueDelta{ + "box1": { + Data: []byte("foobar"), + OldData: []byte("barfoo"), + }, + }, + Txleases: map[ledgercore.Txlease]basics.Round{ + {Sender: poolAddr, Lease: txLease}: 600, + }, + Creatables: map[basics.CreatableIndex]ledgercore.ModifiedCreatable{}, + Hdr: &bookkeeping.BlockHeader{ + Round: 4, + TimeStamp: 0, + RewardsState: bookkeeping.RewardsState{ + FeeSink: basics.Address{}, + RewardsPool: basics.Address{}, + RewardsLevel: 500, + RewardsRate: 510, + RewardsResidue: 0, + RewardsRecalculationRound: 0, + }, + }, + PrevTimestamp: 10, + Totals: ledgercore.AccountTotals{}, + } + + converted, err := stateDeltaToLedgerDelta(original, config.Consensus[protocol.ConsensusCurrentVersion], 25, 4) + require.NoError(t, err) + require.Equal(t, original.Accts.Len(), len(*converted.Accts.Accounts)) + expAccDelta := original.Accts.Accts[0] + actAccDelta := (*converted.Accts.Accounts)[0] + require.Equal(t, expAccDelta.Addr.String(), actAccDelta.Address) + require.Equal(t, expAccDelta.Status.String(), actAccDelta.AccountData.Status) + require.Equal(t, uint64(0), actAccDelta.AccountData.PendingRewards) + require.Equal(t, len(original.Accts.AssetResources), len(*converted.Accts.Assets)) + expAssetDelta := original.Accts.AssetResources[0] + actAssetDelta := (*converted.Accts.Assets)[0] + require.Equal(t, uint64(expAssetDelta.Aidx), actAssetDelta.AssetIndex) + require.Equal(t, expAssetDelta.Addr.String(), actAssetDelta.Address) + require.Equal(t, expAssetDelta.Params.Deleted, actAssetDelta.AssetDeleted) + require.Equal(t, expAssetDelta.Holding.Deleted, actAssetDelta.AssetHoldingDeleted) + require.Equal(t, len(original.Accts.AppResources), len(*converted.Accts.Apps)) + expAppDelta := original.Accts.AppResources[0] + actAppDelta := (*converted.Accts.Apps)[0] + require.Equal(t, uint64(expAppDelta.Aidx), actAppDelta.AppIndex) + require.Equal(t, expAppDelta.Addr.String(), actAppDelta.Address) + require.Equal(t, expAppDelta.Params.Deleted, actAppDelta.AppDeleted) + require.Equal(t, len(original.KvMods), len(*converted.KvMods)) + require.Equal(t, []uint8("box1"), *(*converted.KvMods)[0].Key) + require.Equal(t, original.KvMods["box1"].Data, *(*converted.KvMods)[0].Value) + require.Equal(t, txLease[:], (*converted.TxLeases)[0].Lease) + require.Equal(t, poolAddr.String(), (*converted.TxLeases)[0].Sender) + require.Equal(t, uint64(600), (*converted.TxLeases)[0].Expiration) + require.Nil(t, converted.StateProofNext) + require.Equal(t, uint64(10), *converted.PrevTimestamp) + require.Equal(t, model.AccountTotals{}, *converted.Totals) +} diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index 6a3cc67c07..f11ad58ea4 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -554,11 +554,8 @@ func doDryrunRequest(dr *DryrunRequest, response *model.DryrunResponse) { // This is necessary because the fields can only be represented as unsigned // integers, so a negative cost would underflow. The two fields also provide // more information, which can be useful for testing purposes. - // cost = budgetConsumed - budgetAdded - netCost := uint64(cost) budgetAdded := uint64(proto.MaxAppProgramCost * numInnerTxns(delta)) budgetConsumed := uint64(cost) + budgetAdded - result.Cost = &netCost result.BudgetAdded = &budgetAdded result.BudgetConsumed = &budgetConsumed maxCurrentBudget = pooledAppBudget diff --git a/daemon/algod/api/server/v2/errors.go b/daemon/algod/api/server/v2/errors.go index aa53f101bb..5df5a920d0 100644 --- a/daemon/algod/api/server/v2/errors.go +++ b/daemon/algod/api/server/v2/errors.go @@ -24,8 +24,12 @@ var ( errBoxDoesNotExist = "box not found" errFailedLookingUpLedger = "failed to retrieve information from the ledger" errFailedLookingUpTransactionPool = "failed to retrieve information from the transaction pool" + errFailedRetrievingStateDelta = "failed retrieving State Delta" errFailedRetrievingNodeStatus = "failed retrieving node status" errFailedRetrievingLatestBlockHeaderStatus = "failed retrieving latests block header" + errFailedRetrievingSyncRound = "failed retrieving sync round from ledger" + errFailedSettingSyncRound = "failed to set sync round on the ledger" + errSyncModeNotEnabled = "sync mode must be enabled" errFailedParsingFormatOption = "failed to parse the format option" errFailedToParseAddress = "failed to parse the address" errFailedToParseExclude = "failed to parse exclude" diff --git a/daemon/algod/api/server/v2/generated/data/data_routes.yml b/daemon/algod/api/server/v2/generated/data/data_routes.yml new file mode 100644 index 0000000000..6520d51221 --- /dev/null +++ b/daemon/algod/api/server/v2/generated/data/data_routes.yml @@ -0,0 +1,20 @@ +package: data +generate: + echo-server: true + embedded-spec: true +output-options: + include-tags: + - data + - private + exclude-tags: + - common + - public + - participating + - nonparticipating + type-mappings: + integer: uint64 + skip-prune: true +additional-imports: + - alias: "." + package: "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" +output: ./server/v2/generated/data/routes.go diff --git a/daemon/algod/api/server/v2/generated/data/routes.go b/daemon/algod/api/server/v2/generated/data/routes.go new file mode 100644 index 0000000000..fdc064fb51 --- /dev/null +++ b/daemon/algod/api/server/v2/generated/data/routes.go @@ -0,0 +1,379 @@ +// Package data provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. +package data + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "path" + "strings" + + . "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" + "github.com/algorand/oapi-codegen/pkg/runtime" + "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v4" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Get a LedgerStateDelta object for a given round + // (GET /v2/deltas/{round}) + GetLedgerStateDelta(ctx echo.Context, round uint64) error + // Removes minimum sync round restriction from the ledger. + // (DELETE /v2/ledger/sync) + UnsetSyncRound(ctx echo.Context) error + // Returns the minimum sync round the ledger is keeping in cache. + // (GET /v2/ledger/sync) + GetSyncRound(ctx echo.Context) error + // Given a round, tells the ledger to keep that round in its cache. + // (POST /v2/ledger/sync/{round}) + SetSyncRound(ctx echo.Context, round uint64) error +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// GetLedgerStateDelta converts echo context to params. +func (w *ServerInterfaceWrapper) GetLedgerStateDelta(ctx echo.Context) error { + var err error + // ------------- Path parameter "round" ------------- + var round uint64 + + err = runtime.BindStyledParameterWithLocation("simple", false, "round", runtime.ParamLocationPath, ctx.Param("round"), &round) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter round: %s", err)) + } + + ctx.Set(Api_keyScopes, []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetLedgerStateDelta(ctx, round) + return err +} + +// UnsetSyncRound converts echo context to params. +func (w *ServerInterfaceWrapper) UnsetSyncRound(ctx echo.Context) error { + var err error + + ctx.Set(Api_keyScopes, []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.UnsetSyncRound(ctx) + return err +} + +// GetSyncRound converts echo context to params. +func (w *ServerInterfaceWrapper) GetSyncRound(ctx echo.Context) error { + var err error + + ctx.Set(Api_keyScopes, []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetSyncRound(ctx) + return err +} + +// SetSyncRound converts echo context to params. +func (w *ServerInterfaceWrapper) SetSyncRound(ctx echo.Context) error { + var err error + // ------------- Path parameter "round" ------------- + var round uint64 + + err = runtime.BindStyledParameterWithLocation("simple", false, "round", runtime.ParamLocationPath, ctx.Param("round"), &round) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter round: %s", err)) + } + + ctx.Set(Api_keyScopes, []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.SetSyncRound(ctx, round) + return err +} + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface, m ...echo.MiddlewareFunc) { + RegisterHandlersWithBaseURL(router, si, "", m...) +} + +// Registers handlers, and prepends BaseURL to the paths, so that the paths +// can be served under a prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string, m ...echo.MiddlewareFunc) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.GET(baseURL+"/v2/deltas/:round", wrapper.GetLedgerStateDelta, m...) + router.DELETE(baseURL+"/v2/ledger/sync", wrapper.UnsetSyncRound, m...) + router.GET(baseURL+"/v2/ledger/sync", wrapper.GetSyncRound, m...) + router.POST(baseURL+"/v2/ledger/sync/:round", wrapper.SetSyncRound, m...) + +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "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", + "frz9/wEAAP//msdBKCzJAAA=", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %s", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %s", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %s", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + var res = make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + var resolvePath = PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + var pathToFile = url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/daemon/algod/api/server/v2/generated/model/types.go b/daemon/algod/api/server/v2/generated/model/types.go index 215d06e906..f184d1a5f2 100644 --- a/daemon/algod/api/server/v2/generated/model/types.go +++ b/daemon/algod/api/server/v2/generated/model/types.go @@ -218,6 +218,30 @@ type Account struct { // * lsig type AccountSigType string +// AccountBalanceRecord Account and its address +type AccountBalanceRecord struct { + // AccountData Account information at a given round. + // + // Definition: + // data/basics/userBalance.go : AccountData + AccountData Account `json:"account-data"` + + // Address Address of the updated account. + Address string `json:"address"` +} + +// AccountDeltas Exposes deltas for account based resources in a single round +type AccountDeltas struct { + // Accounts Array of Account updates for the round + Accounts *[]AccountBalanceRecord `json:"accounts,omitempty"` + + // Apps Array of App updates for the round. + Apps *[]AppResourceRecord `json:"apps,omitempty"` + + // Assets Array of Asset updates for the round. + Assets *[]AssetResourceRecord `json:"assets,omitempty"` +} + // AccountParticipation AccountParticipation describes the parameters used by this account in consensus protocol. type AccountParticipation struct { // SelectionParticipationKey \[sel\] Selection public key (if any) currently registered for this round. @@ -247,6 +271,42 @@ type AccountStateDelta struct { Delta StateDelta `json:"delta"` } +// AccountTotals Total Algos in the system grouped by account status +type AccountTotals struct { + // NotParticipating Amount of stake in non-participating accounts + NotParticipating uint64 `json:"not-participating"` + + // Offline Amount of stake in offline accounts + Offline uint64 `json:"offline"` + + // Online Amount of stake in online accounts + Online uint64 `json:"online"` + + // RewardsLevel Total number of algos received per reward unit since genesis + RewardsLevel uint64 `json:"rewards-level"` +} + +// AppResourceRecord Represents AppParams and AppLocalStateDelta in deltas +type AppResourceRecord struct { + // Address App account address + Address string `json:"address"` + + // AppDeleted Whether the app was deleted + AppDeleted bool `json:"app-deleted"` + + // AppIndex App index + AppIndex uint64 `json:"app-index"` + + // AppLocalState Stores local state associated with an application. + AppLocalState *ApplicationLocalState `json:"app-local-state,omitempty"` + + // AppLocalStateDeleted Whether the app local state was deleted + AppLocalStateDeleted bool `json:"app-local-state-deleted"` + + // AppParams Stores the global information associated with an application. + AppParams *ApplicationParams `json:"app-params,omitempty"` +} + // Application Application index and its parameters type Application struct { // Id \[appidx\] application index. @@ -383,6 +443,35 @@ type AssetParams struct { UrlB64 *[]byte `json:"url-b64,omitempty"` } +// AssetResourceRecord Represents AssetParams and AssetHolding in deltas +type AssetResourceRecord struct { + // Address Account address of the asset + Address string `json:"address"` + + // AssetDeleted Whether the asset was deleted + AssetDeleted bool `json:"asset-deleted"` + + // AssetHolding Describes an asset held by an account. + // + // Definition: + // data/basics/userBalance.go : AssetHolding + AssetHolding *AssetHolding `json:"asset-holding,omitempty"` + + // AssetHoldingDeleted Whether the asset holding was deleted + AssetHoldingDeleted bool `json:"asset-holding-deleted"` + + // AssetIndex Index of the asset + AssetIndex uint64 `json:"asset-index"` + + // AssetParams AssetParams specifies the parameters for an asset. + // + // \[apar\] when part of an AssetConfig transaction. + // + // Definition: + // data/transactions/asset.go : AssetParams + AssetParams *AssetParams `json:"asset-params,omitempty"` +} + // Box Box name and its content. type Box struct { // Name \[name\] box name, base64 encoded @@ -460,9 +549,6 @@ type DryrunTxnResult struct { // BudgetConsumed Budget consumed during execution of app call transaction. BudgetConsumed *uint64 `json:"budget-consumed,omitempty"` - // Cost Net cost of app execution. Field is DEPRECATED and is subject for removal. Instead, use `budget-added` and `budget-consumed. - Cost *uint64 `json:"cost,omitempty"` - // Disassembly Disassembled program line by line. Disassembly []string `json:"disassembly"` @@ -503,6 +589,42 @@ type EvalDeltaKeyValue struct { Value EvalDelta `json:"value"` } +// KvDelta A single Delta containing the key, the previous value and the current value for a single round. +type KvDelta struct { + // Key The key, base64 encoded. + Key *[]byte `json:"key,omitempty"` + + // Value The new value of the KV store entry, base64 encoded. + Value *[]byte `json:"value,omitempty"` +} + +// LedgerStateDelta Contains ledger updates. +type LedgerStateDelta struct { + // Accts Exposes deltas for account based resources in a single round + Accts *AccountDeltas `json:"accts,omitempty"` + + // KvMods Array of KV Deltas + KvMods *[]KvDelta `json:"kv-mods,omitempty"` + + // ModifiedApps List of modified Apps + ModifiedApps *[]ModifiedApp `json:"modified-apps,omitempty"` + + // ModifiedAssets List of modified Assets + ModifiedAssets *[]ModifiedAsset `json:"modified-assets,omitempty"` + + // PrevTimestamp Previous block timestamp + PrevTimestamp *uint64 `json:"prev-timestamp,omitempty"` + + // StateProofNext Next round for which we expect a state proof + StateProofNext *uint64 `json:"state-proof-next,omitempty"` + + // Totals Total Algos in the system grouped by account status + Totals *AccountTotals `json:"totals,omitempty"` + + // TxLeases List of transaction leases + TxLeases *[]TxLease `json:"tx-leases,omitempty"` +} + // LightBlockHeaderProof Proof of membership and position of a light block header. type LightBlockHeaderProof struct { // Index The index of the light block header in the vector commitment tree @@ -515,6 +637,30 @@ type LightBlockHeaderProof struct { Treedepth uint64 `json:"treedepth"` } +// ModifiedApp App which was created or deleted. +type ModifiedApp struct { + // Created Created if true, deleted if false + Created bool `json:"created"` + + // Creator Address of the creator. + Creator string `json:"creator"` + + // Id App Id + Id uint64 `json:"id"` +} + +// ModifiedAsset Asset which was created or deleted. +type ModifiedAsset struct { + // Created Created if true, deleted if false + Created bool `json:"created"` + + // Creator Address of the creator. + Creator string `json:"creator"` + + // Id Asset Id + Id uint64 `json:"id"` +} + // ParticipationKey Represents a participation key used by the node. type ParticipationKey struct { // Address Address the key was generated for. @@ -640,6 +786,18 @@ type TealValue struct { Uint uint64 `json:"uint"` } +// TxLease defines model for TxLease. +type TxLease struct { + // Expiration Round that the lease expires + Expiration uint64 `json:"expiration"` + + // Lease Lease data + Lease []byte `json:"lease"` + + // Sender Address of the lease sender + Sender string `json:"sender"` +} + // Version algod version information. type Version struct { Build BuildVersion `json:"build"` @@ -820,6 +978,15 @@ type DryrunResponse struct { Txns []DryrunTxnResult `json:"txns"` } +// GetSyncRoundResponse defines model for GetSyncRoundResponse. +type GetSyncRoundResponse struct { + // Round The minimum sync round for the ledger. + Round uint64 `json:"round"` +} + +// LedgerStateDeltaResponse Contains ledger updates. +type LedgerStateDeltaResponse = LedgerStateDelta + // LightBlockHeaderProofResponse Proof of membership and position of a light block header. type LightBlockHeaderProofResponse = LightBlockHeaderProof diff --git a/daemon/algod/api/server/v2/generated/nonparticipating/private/private_routes.yml b/daemon/algod/api/server/v2/generated/nonparticipating/private/private_routes.yml index 8967301b8e..a17facb0a4 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/private/private_routes.yml +++ b/daemon/algod/api/server/v2/generated/nonparticipating/private/private_routes.yml @@ -9,6 +9,7 @@ output-options: exclude-tags: - public - participating + - data - common type-mappings: integer: uint64 diff --git a/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go b/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go index 538419b26e..64f2bc2841 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go +++ b/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go @@ -130,160 +130,173 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9+5PbNtLgv4LSflV+nCjN+JFdT1Xqu4ntZOfiOC7PbPbus30JRLYk7JAAA4AjKb75", - "36/QAEiQBCXOI86Xqv3JHhGPRqPR6Dc+T1JRlIID12py8nlSUkkL0CDxL5qmouI6YZn5KwOVSlZqJvjk", - "xH8jSkvGV5PphJlfS6rXk+mE0wKaNqb/dCLh14pJyCYnWlYwnah0DQU1A+tdaVrXI22TlUjcEKd2iLNX", - "k+s9H2iWSVCqD+WPPN8RxtO8yoBoSbmiqfmkyIbpNdFrpojrTBgnggMRS6LXrcZkySDP1Mwv8tcK5C5Y", - "pZt8eEnXDYiJFDn04XwpigXj4KGCGqh6Q4gWJIMlNlpTTcwMBlbfUAuigMp0TZZCHgDVAhHCC7wqJicf", - "Jgp4BhJ3KwV2hf9dSoDfINFUrkBPPk1ji1tqkIlmRWRpZw77ElSVa0WwLa5xxa6AE9NrRn6olCYLIJST", - "99++JE+fPn1hFlJQrSFzRDa4qmb2cE22++RkklEN/nOf1mi+EpLyLKnbv//2Jc5/7hY4thVVCuKH5dR8", - "IWevhhbgO0ZIiHENK9yHFvWbHpFD0fy8gKWQMHJPbON73ZRw/j90V1Kq03UpGNeRfSH4ldjPUR4WdN/H", - "w2oAWu1LgylpBv1wlLz49Pl4enx0/ZcPp8l/uT+fP70eufyX9bgHMBBtmFZSAk93yUoCxdOypryPj/eO", - "HtRaVHlG1vQKN58WyOpdX2L6WtZ5RfPK0AlLpTjNV0IR6sgogyWtck38xKTiuWFTZjRH7YQpUkpxxTLI", - "pob7btYsXZOUKjsEtiMblueGBisF2RCtxVe35zBdhygxcN0KH7ig/77IaNZ1ABOwRW6QpLlQkGhx4Hry", - "Nw7lGQkvlOauUje7rMjFGghObj7YyxZxxw1N5/mOaNzXjFBFKPFX05SwJdmJimxwc3J2if3dagzWCmKQ", - "hpvTukfN4R1CXw8ZEeQthMiBckSeP3d9lPElW1USFNmsQa/dnSdBlYIrIGLxL0i12fb/df7jWyIk+QGU", - "oit4R9NLAjwV2fAeu0ljN/i/lDAbXqhVSdPL+HWds4JFQP6BbllRFYRXxQKk2S9/P2hBJOhK8iGA7IgH", - "6Kyg2/6kF7LiKW5uM21LUDOkxFSZ092MnC1JQbdfH00dOIrQPCcl8IzxFdFbPiikmbkPg5dIUfFshAyj", - "zYYFt6YqIWVLBhmpR9kDiZvmEDyM3wyeRrIKwPGDDIJTz3IAHA7bCM2Yo2u+kJKuICCZGfmH41z4VYtL", - "4DWDI4sdfiolXDFRqbrTAIw49X7xmgsNSSlhySI0du7QYbiHbePYa+EEnFRwTRmHzHBeBFposJxoEKZg", - "wv3KTP+KXlAFXz0busCbryN3fym6u753x0ftNjZK7JGM3IvmqzuwcbGp1X+E8hfOrdgqsT/3NpKtLsxV", - "smQ5XjP/Mvvn0VApZAItRPiLR7EVp7qScPKRPzZ/kYSca8ozKjPzS2F/+qHKNTtnK/NTbn96I1YsPWer", - "AWTWsEa1KexW2H/MeHF2rLdRpeGNEJdVGS4obWmlix05ezW0yXbMmxLmaa3KhlrFxdZrGjftobf1Rg4A", - "OYi7kpqGl7CTYKCl6RL/2S6RnuhS/mb+Kcvc9NblMoZaQ8fuvkXbgLMZnJZlzlJqkPjefTZfDRMAqyXQ", - "psUcL9STzwGIpRQlSM3soLQsk1ykNE+UphpH+g8Jy8nJ5C/zxrgyt93VPJj8jel1jp2MPGplnISW5Q3G", - "eGfkGrWHWRgGjZ+QTVi2hxIR43YTDSkxw4JzuKJczxp9pMUP6gP8wc3U4NuKMhbfHf1qEOHENlyAsuKt", - "bfhAkQD1BNFKEK0oba5ysah/eHhalg0G8ftpWVp8oGgIDKUu2DKl1SNcPm1OUjjP2asZ+S4cG+VswfOd", - "uRysqGHuhqW7tdwtVhuO3BqaER8ogtsp5MxsjUeDkeHvg+JQZ1iL3Eg9B2nFNP67axuSmfl9VOc/B4mF", - "uB0mLtSiHOasAoO/BJrLww7l9AnH2XJm5LTb93ZkY0aJE8ytaGXvftpx9+CxRuFG0tIC6L7Yu5Rx1MBs", - "IwvrHbnpSEYXhTk4wwGtIVS3PmsHz0MUEiSFDgzf5CK9/DtV63s48ws/Vv/44TRkDTQDSdZUrWeTmJQR", - "Hq9mtDFHzDRE7Z0sgqlm9RLva3kHlpZRTYOlOXjjYolFPfZDpgcyorv8iP+hOTGfzdk2rN8OOyMXyMCU", - "Pc7Og5AZVd4qCHYm0wBNDIIUVnsnRuu+EZQvm8nj+zRqj15bg4HbIbcI3CGxvfdj8I3YxmD4Rmx7R0Bs", - "Qd0HfZhxUIzUUKgR8L1ykAncf4c+KiXd9ZGMY49BslmgEV0VngYe3vhmlsbyeroQ8nbcp8NWOGnsyYSa", - "UQPmO+0gCZtWZeJIMWKTsg06AzUuvP1Mozt8DGMtLJxr+jtgQZlR7wML7YHuGwuiKFkO90D66yjTX1AF", - "T5+Q87+fPj9+8vOT518ZkiylWElakMVOgyIPnW5GlN7l8Ki/MtSOqlzHR//qmbdCtseNjaNEJVMoaNkf", - "ylo3rQhkmxHTro+1Nppx1TWAYw7nBRhObtFOrOHegPaKKSNhFYt72YwhhGXNLBlxkGRwkJhuurxmml24", - "RLmT1X2osiClkBH7Gh4xLVKRJ1cgFRMRV8k714K4Fl68Lbu/W2jJhipi5kbTb8VRoIhQlt7y8XzfDn2x", - "5Q1u9nJ+u97I6ty8Y/aljXxvSVSkBJnoLScZLKpVSxNaSlEQSjLsiHf0G7Za60BkeSeFWN77rR2dJbYk", - "/GAFvtz06Yt9b0UGRu2u1D2w92awBnuGckKc0YWoNKGEiwxQR69UnPEPOHrRw4SOMR3eJXptZbgFGH0w", - "pZVZbVUSdPv0aLHpmNDUUlGCqFEDdvHaoWFb2emsEzGXQDOjJwInYuGMz84sjouk6LPSnnW6ayeiObfg", - "KqVIQSmj31ut7SBovp0lS70HTwg4AlzPQpQgSyrvDOzl1UE4L2GXoIdVkYff/6Qe/QHwaqFpfgCx2CaG", - "3lqFcB6GPtTjpt9HcN3JQ7KjEojnfUZfMQwiBw1DKLwRTgb3rwtRbxfvjpYrkGjr/10p3k9yNwKqQf2d", - "6f2u0FblQNyQE50vWIGWIE65UJAKnqnoYDlVOjnElk2jlnxvVhBwwhgnxoEHrJFvqNLWP8V4hmq1vU5w", - "HmumNFMMAzwo4piRf/LSTX/s1NyDXFWqFnVUVZZCashia+Cw3TPXW9jWc4llMHYtT2lBKgWHRh7CUjC+", - "Q5ZdiUUQ1bUZ1zlw+4tDY6e553dRVLaAaBCxD5Bz3yrAbhg7MQAIUw2iLeEw1aGcOmBjOlFalKXhFjqp", - "eN1vCE3ntvWp/kfTtk9cVDf3dibAzK49TA7yjcWsjZpZU6OU4cikoJdG9kAVyzrS+jCbw5goxlNI9lG+", - "OZbnplV4BA4c0gHt1sXlBbN1DkeHfqNEN0gEB3ZhaMEDqvY7KjVLWYmS4vewu3fBuTtB1ABMMtCUGfUv", - "+GCF6DLsT6xntDvm7QTpUVpRH/yeWhRZTs4UXhht4C9hh56gdzbk5iII1LkHTSAyqjndlBME1DvyjQAT", - "NoEtTXW+M9ecXsOObEACUdWiYFrbGKq2oqBFmYQDRC1Oe2Z05lUbruJ3YIy99xyHCpbX34rpxEpU++G7", - "6IhVLXQ4SaoUIh/haeshIwrBKE8cKYXZdeZC9nxcl6ekFpBOiEHbes08H6gWmnEF5P+IiqSUo8Baaahv", - "BCGRzeL1a2YwF1g9p/O5NRiCHAqwcjh+efy4u/DHj92eM0WWsPFxrqZhFx2PH6MW/E4o3Tpc92CCMcft", - "LMLb0RRnLgonw3V5ymGfjxt5zE6+6wxe2+/MmVLKEa5Z/p0ZQOdkbsesPaSRcf4uHHeUlS0YOrZu3HcM", - "OPh9bDTN0DHo+hMHbtrm45Cn1shX+e4e+LQdiEgoJSg8VaFeouxXsQxDod2xUzuloeibbmzXnwcEm/de", - "LOhJmYLnjENSCA67aPYP4/ADfoz1tid7oDPy2KG+XbGpBX8HrPY8Y6jwrvjF3Q5I+V0donAPm98dt2O1", - "C4PAUSuFvCSUpDlDnVVwpWWV6o+colQcnOWIK8fL+sN60kvfJK6YRfQmN9RHTtGNV8vKUfPzEiJa8LcA", - "Xl1S1WoFSnfkgyXAR+5aMU4qzjTOVZj9SuyGlSDRnzKzLQu6I0uao1r3G0hBFpVu35gYq6q00bqsCdFM", - "Q8TyI6ea5GA00B8Yv9jicD4k1NMMB70R8rLGwix6HlbAQTGVxF1O39mvGA3glr92kQGYOGQ/W6OTGb8J", - "aN1paCXD/N+H/3ny4TT5L5r8dpS8+B/zT5+fXT963PvxyfXXX/+/9k9Pr79+9J//EdspD3ssktJBfvbK", - "SZNnr1BkaKxOPdi/mMWhYDyJEtnFGkjBOAbkd2iLPDSCjyegR41Zz+36R6633BDSFc1ZRvXtyKHL4npn", - "0Z6ODtW0NqKjQPq1fopFR6xEUtL0Ej22kxXT62oxS0Ux91L0fCVqiXqeUSgEx2/ZnJZsrkpI51fHB670", - "O/ArEmFXHSZ7a4Gg7++NRz+jQdUFNOPJW1bcEkWlnFEXg/u8300sp3WEu81sPSEY/rym3mns/nzy/KvJ", - "tAlbrr8bTd1+/RQ5EyzbxoLTM9jGJDV31PCIPVCkpDsFOs6HEPaoi9H6pcJhCzAivlqz8svzHKXZIs4r", - "fciU0/i2/IzbWCZzEtE8u3NWH7H88nBrCZBBqdexjLeWzIGtmt0E6LjMSimugE8Jm8Gsq3FlK1De2ZkD", - "XWLmFZoYxZgQ0PocWELzVBFgPVzIKLUmRj8oJju+fz2dODFC3btk7waOwdWds7bF+r+1IA++e31B5o71", - "qgc2T8IOHUS2RywZLniz5Uw13Mzm+dpEkY/8I38FS8aZ+X7ykWdU0/mCKpaqeaVAfkNzylOYrQQ58fGg", - "r6imH3lPZhtMxQ8icUlZLXKWkstQtm7I06ZX9kf4+PGD4fgfP37q+ZX6krCbKspf7ATJhum1qHTi8scS", - "CRsqswjoqs4fwpFt9ue+WafEjW1ZsctPc+PHeR4tS9XNI+gvvyxzs/yADJWLkjdbRpQW0ks1RtSx0OD+", - "vhXuYpB045MPKwWK/FLQ8gPj+hNJPlZHR0+BtALrf3HCg6HJXQktm9et8hy69i5cuNWQYKslTUq6AhVd", - "vgZa4u6j5F2gdTXPCXZrBfT7gCUcqlmAx8fwBlg4bhycjIs7t718IYD4EvATbiG2MeJG47S47X4FIf63", - "3q5OmkBvlyq9TszZjq5KGRL3O1PnB6+MkOU9SYqtuDkELpV6ASRdQ3oJGWZ1QlHq3bTV3TsrncjqWQdT", - "NvvZBuhiih6aBxdAqjKjTqinfNfNlVKgtU8Qew+XsLsQTYbfTZKj2rk6auigIqUG0qUh1vDYujG6m+88", - "4pifUJY+5QVjnz1ZnNR04fsMH2Qr8t7DIY4RRSuXZAgRVEYQYYl/AAW3WKgZ706kH1ue0VcW9uaLJEt7", - "3k9ck0YNc87rcDWYImO/F4ClFMRGkQU1crtwVQBsPkrAxSpFVzAgIYcW2pFZHy2rLg5y6N6L3nRi2b3Q", - "evdNFGTbODFrjlIKmC+GVFCZ6YQs+JmsEwBXMCNY3MchbJGjmFTHdlimQ2XLUm6rlQyBFidgkLwRODwY", - "bYyEks2aKl+gAOs4+LM8Sgb4HfOr9mXVngXe9qBYQ50z63lu95z2tEuXW+sTan0WbahajsiINRI+BvjF", - "tkNwFIAyyGFlF24be0Jpcr2aDTJw/Lhc5owDSWKOe6qUSJmtMNFcM24OMPLxY0KsMZmMHiFGxgHY6NzC", - "gclbEZ5NvroJkNzlqlE/NrrFgr8hHlZrQ9mMyCNKw8IZHwia9ByAumiP+v7qxBzhMITxKTFs7ormhs05", - "ja8ZpJfciWJrJ5XTuVcfDYmze2z59mK50ZrsVXSb1YQykwc6LtDtgXghtomNq49KvIvtwtB7NLoPo/xj", - "B9Om0T5QZCG26LLHq8VGkx2AZRgOD0ag4W+ZQnrFfkO3uQVm37T7pakYFSokGWfOq8llSJwYM/WABDNE", - "Lg+DzNhbAdAxdjQ15Jzye1BJbYsn/cu8udWmTcUHHzgdO/5DRyi6SwP461th6lzWd12JJWqnaHue22m8", - "gQgZI3rDJvrunr5TSUEOqBQkLSEquYw5AY1uA3jjnPtugfECk4Up3z0KwhkkrJjS0JjjzcXs/Utf2jxJ", - "sUaJEMvh1elSLs363gtRX1M2CR47tpb5xVdwJTQkSyaVTtCXEV2CafStQqX6W9M0Liu1AyZsuS6WxXkD", - "TnsJuyRjeRWnVzfv96/MtG9rlqiqBfJbxgnQdE0WWF4uGka1Z2obabd3wW/sgt/Qe1vvuNNgmpqJpSGX", - "9hx/knPR4bz72EGEAGPE0d+1QZTuYZAo+7yCXMcyIAO5yR7OzDSc7bO+9g5T5sc+GIBioRi+o+xI0bUE", - "BoO9q2DoJjJiCdNBdbZ+Vs/AGaBlybJtxxZqRx3UmOmNDB6+7EUHC7i7brADGAjsnrHAYgmqXeGkEfBt", - "nb1WgvFsFGYu2nVIQoYQTsWUrxLbR1SdeHAIVxdA8+9h95Npi8uZXE8ndzOdxnDtRjyA63f19kbxjE5+", - "a0preUJuiHJallJc0TxxBuYh0pTiypEmNvf26C/M6uJmzIvXp2/eOfCvp5M0ByqTWlQYXBW2K/80q7LF", - "VAYOiK9CaXQ+L7NbUTLY/LoCRGiU3qzBVfwLpNFeaaLG4RAcRWekXsZjjQ6anJ1vxC5xj48EytpF0pjv", - "rIek7RWhV5Tl3m7moR2IC8LFjatvFeUK4QB39q4ETrLkXtlN73THT0dDXQd4UjjXnpqEhS27qYjgXRe6", - "ESHRHIekWlAsLGStIn3mxKsCLQmJylkat7HyhTLEwa3vzDQm2HhAGDUjVmzAFcsrFoxlmqkRim4HyGCO", - "KDJ9kaoh3C2Eq5decfZrBYRlwLX5JPFUdg4qVnJy1vb+dWpkh/5cbmBroW+Gv4uMERbV6t54CMR+ASP0", - "1PXAfVWrzH6htUXK/BC4JG7g8A9n7F2Je5z1jj4cNdswyHXb4xaWN+/zP0MYthTm4drqXnl11b0G5ojW", - "SmcqWUrxG8T1PFSPI1kHvowYwyiX34DPIslbXRZTW3eaku/N7IPbPSTdhFaodpDCANXjzgduOaxn5C3U", - "lNuttqWLW7FucYIJ41PndvyGYBzMvZjenG4WNFbsyQgZBqbTxgHcsqVrQXxnj3tn9meustuMBL7kui2z", - "+XglyCYhqJ/bf0uBwU47WlRoJAOk2lAmmFr/X65EZJiKbyi3FbBNP3uUXG8F1vhlem2ExGxaFTf7Z5Cy", - "guZxySFL+ybejK2Yrf9cKQgKDLuBbOF8S0WuSLN1sTeoOVuSo2lQwtztRsaumGKLHLDFsW2xoAo5eW2I", - "qruY5QHXa4XNn4xovq54JiHTa2URqwSphTpUb2rn1QL0BoCTI2x3/II8RLedYlfwyGDR3c+Tk+MXaHS1", - "fxzFLgBX6H0fN8mQnfzTsZM4HaPf0o5hGLcbdRbNDbWvcwwzrj2nyXYdc5awpeN1h89SQTldQTxSpDgA", - "k+2Lu4mGtA5eeGZLyystxY4wHZ8fNDX8aSCO3bA/CwZJRVEwXTjnjhKFoaemerCd1A9n69S7wm8eLv8R", - "faSldxF1lMgvazS191ts1ejJfksLaKN1SqhNoc5ZE73gy1GSM1+hASvh1QXwLG7MXGbpKOZgMMOSlJJx", - "jYpFpZfJ30i6ppKmhv3NhsBNFl89i1T/a1eh4jcD/IvjXYICeRVHvRwgey9DuL7kIRc8KQxHyR41eSPB", - "qRx05sbddkO+w/1DjxXKzCjJILlVLXKjAae+E+HxPQPekRTr9dyIHm+8si9OmZWMkwetzA794/0bJ2UU", - "QsbKLjXH3UkcErRkcIWxe/FNMmPecS9kPmoX7gL9H+t58CJnIJb5sxxTBL4REe3UV6SsLekuVj1iHRg6", - "puaDIYOFG2pK2tX/vrzTzxuf+84n88XDin90gf2DtxSR7FcwsIlBZdLodmb198D/Tck3Yjt2UzsnxG/s", - "fwPURFFSsTz7qcnv7BR+lZSn66g/a2E6/tw8UVEvzt5P0epGa8o55NHhrCz4s5cZI1Ltv8TYeQrGR7bt", - "1qK1y+0srgG8DaYHyk9o0Mt0biYIsdpOeKsDqvOVyAjO05TSabhnv4ZxUGny1wqUjiUP4Qcb1IV2S6Pv", - "2kKHBHiG2uKMfGefmFsDaVX6QC2NFVVuq0ZAtgLpDOpVmQuaTYkZ5+L16RtiZ7V9bKF1W2hxhUpKexUd", - "e1VQJWxceLCvmR5PXRg/zv5YarNqpbHwjtK0KGNppqbFhW+AuayhDR/VlxA7M/LKao7K6yV2EkMPSyYL", - "o3HVo1nZBWnC/Edrmq5RJWux1GGSH18h1FOlCl7lqavr16Wz8NwZuF2RUFsjdEqE0Zs3TNmXxeAK2pmt", - "dZq3Mwn4TNf28mTFuaWUqOyxrwzBbdDugbOBGt7MH4Wsg/gbCuS2wO5NC6aeY69oLZpu9dXeczw2u7Gu", - "mu5fjEwpF5ylWAkmdjW7V8rG+MBGFM3pGln9EXcnNHK4ojVf6zA5h8XBKrCeETrE9Y3wwVezqZY67J8a", - "n8NaU01WoJXjbJBNfeliZwdkXIErhYYP1gV8UsiWXxE5ZNRVndQujRuSEabFDCh235pvb53aj/Hil4yj", - "gO/Q5kLTraUOH1HSRitgmqwEKLeedm6w+mD6zDBNNoPtp5l/dAnHsG45s2zrg+4Pdeo90s4DbNq+NG1t", - "UZTm51YEsp30tCzdpMOFraPygN7yQQRHPIuJd+0EyK3HD0fbQ257Q0nwPjWEBlfoiIYS7+EeYdRFnjsP", - "CBih1VIUtiA2hCtaC4HxCBhvGIfmSbDIBZFGrwTcGDyvA/1UKqm2IuAonnYBNEfvc4yhKe1cD3cdqrPB", - "iBJco59jeBub+tQDjKNu0AhulO/ql8gMdQfCxEt8AtEhsl9tGqUqJ0RlmFHQqT8dYxyGcfsK9+0LoH8M", - "+jKR7a4ltSfnJjfRUJLoospWoBOaZbEakt/gV4JfSVah5ABbSKu6Bl9ZkhSrq7TLzfSpzU2UCq6qYs9c", - "vsEdp0tFTI5+ixMonzLRDD4jyH4N6331+t371y9PL16/sveFIqqyWaJG5pZQGIY4I2dcaTCic6WA/BKi", - "8Rfs90tnwXEwg7rzEaINa997QsRcmcUO/43VyRsmIBcrcuNoRR8Ygh1vLN63R+oJ5+boJYqtkvGYwKvv", - "7uhopr7deWz63+uBzMWqDcgXrmCxjxmHexRjw6/N/RYWeOgVf7Q3YF1/AWMDhX8tCLXbOnO4zTzxxu1V", - "g0SfVP0ayX47yfC7IlO8owcihIO6HdSKAdbJORQnnA6GtVPtEuw0JXs55WDSkg0ysulJ9lHsqIF3KLDI", - "xhWZz73e4wTYnjqAY+9FqI9Y6wP0vQ+HJSVlzoPfMIs+Zl3g/LBVc9+haza4uwgXjj5oWIw/7jBcQqcp", - "m4PXQCkUawrWxl59GBkudYEPNwQlgPpj+ViFK0i1EeoDH6wEuElBIDNZ8EbNv0vpDKgfdVSZq6Czr2xO", - "vzTxAWbTy2wJsrNsWdfZ+CIxp3WkDfr/8ZWYFXD3TEw7Zn105OxyCalmVwcyif5ptNQmS2Xq9Vj73FuQ", - "WMTqSEz/DP8N1esGoH2JPnvhCUrL3RmcoTyCS9g9UKRFDdE6s1PP825TgwAxgNwhMSQiVMyTbQ1vzrnI", - "VE0ZiAUfOWK7Q1PNabDAf5AXd8u5PEkSGubK7ZnySsQ091Fzma43yiDFoMKhZKN+ie1hQegVVjRX9eM7", - "9Tv7gVZDzvqV3jauBgLmfdW2Zl8NAZT/zSd52llydgnhEwRo2d9QmfkWUVXVa8HJnvuolyHky0N3gV7W", - "M7Mmzq+fExKpHYTRnGkuFOOrZCgkth1aF779igEEeB1g7XKEawnSPdWCJuRcKEi08HGB++DYhwr3Tult", - "kKAG6/VZ4AaraLxvyoRgBVSKVTOoC44IF2j0Vmqgk0Exj+E59yH7pf3ukyB8BcwRGrmj1+RgNQ4f4clU", - "D4kh1S+Juy0PJ1fcRutlnNunxlSssgc3qAytx6UUWZXaCzo8GI2NYWzdnD2sJKowpv1V9mT/HKtIvQlS", - "1S5hN7fyd7qmvCnn1T7WVoSyawhSwzu7fa8Ggbjuk6/sAlb3AucfqVRPJ6UQeTJgLj7rFyjpnoFLll5C", - "Rszd4WOjBor8k4dopaz9gZv1zhfkKEvgkD2aEWLU8qLUO+8abNfa7UzOH+h9829x1qyyNYOcvj/7yONh", - "fVjNR96Rv/lh9nM1BYb53XEqO8iB8hfbgeIokm4iT16MfdE44qzrPkPQEJWFIial3DIXetT57uv8EdIP", - "6vDv137CUglNDJa0piOUlrxBpyu8/NBYhMa9COA7HAAvVIqDNwE8N3Lg/MGBUj/USAmWMkgJreUf0rP9", - "Q9w1Xwq2SGFkvVmmLVxjneztfQmMKOplbZuI47lvwsC6CIJjrZi+6UOhKRFLzoaEY86lvKL5lzdfYMGM", - "U8SHe9gqvtBQ/w2RbFGpbhet8IaOmjvQde9vav4OzS3/BLNHURuwG8rZUeu3GHwJSSyNRnOSi+ZNFhyS", - "bHBMazQ+/oosXKR1KSFlinWSUDa+Gmat7mFx6Oa9s/365aF1/iT0HcjYKQiiJG+bynpa4P3QQNgc0T+Y", - "qQyc3CiVx6ivRxYR/MV4VJjyfOC6uGxZk22l0k40h5Bwz1blwI19Q6tyP5l77PJwHXjpVAr66xx9W7dw", - "G7mom7WNdYn0kbuv/NoYT0a8qqLpjq4UixAsSUoQVPLL8S9EwhLfHBDk8WOc4PHjqWv6y5P2Z3OcHz+O", - "inFfzInSevrdzRujmJ+Gov9shNtAoGlnPyqWZ4cIoxU23Lz/gYGxP7vEgT/kBZKfrT21f1Rd7fabuG+7", - "m4CIiay1NXkwVRAQPCIW2HWbRR/nV5BWkukd1jPw5jf2c7RO1He1xd55fOoMWHf3aXEJdUWMxr5fKX+7", - "fifsY/6FkanRea7xMbjXW1qUObiD8vWDxV/h6d+eZUdPj/+6+NvR86MUnj1/cXREXzyjxy+eHsOTvz1/", - "dgTHy69eLJ5kT549WTx78uyr5y/Sp8+OF8++evHXB4YPGZAtoBOfPTf53/hMT3L67iy5MMA2OKElq9+A", - "NGTsXwigKZ5EKCjLJyf+p//pT9gsFUUzvP914pJzJmutS3Uyn282m1nYZb5Cg16iRZWu536e/tt7787q", - "AGub8I07amNnDSngpjpSOMVv71+fX5DTd2ezhmAmJ5Oj2dHsGF/WKoHTkk1OJk/xJzw9a9z3uSO2ycnn", - "6+lkvgaao//L/FGAliz1n9SGrlYgZ+6pBPPT1ZO5FyXmn50x83rft3lYdXT+uWXzzQ70xKqE888+2X5/", - "61Y2u7N1Bx1GQrGv2XyBOTxjm4IKGg8vxb7yPf+MIvLg73OX2BD/iKqKPQNz7xiJt2xh6bPeGlg7Pdwj", - "svPPzavO15ZJ5BBzg9h8ABo8Aj0lTBO6EBKz3HW6NnzBp9cy1X4dvCbys8wQt+n1sn7hOqgsdvKhJ+Xb", - "gYgfCTmBIfPmoLZmanixlhWExa7qm6bVvrlvPhwlLz59Pp4eH13/xdwn7s/nT69H+jNfNg9kn9eXxciG", - "nzA3FS2zeH6fHB3d4f23Ux6+1o2bFDwz2Cv34B4ULoa0d7dVnYFIjYwDOXSd4QeeCH52wxXvtR+1ooci", - "z7l8QzPiU2Rw7uMvN/cZR2+y4evE3lvX08nzL7n6M25InuYEWwZFEfpb/w9+ycWG+5ZGyKiKgsqdP8aq", - "xRT8u/V4ldGVQmuiZFcUZTsueKvS++QTWrBj4ZUD/EZpegt+c256/ZvffCl+g5t0H/ymPdA985snNzzz", - "f/4V/5vD/tk47Llld3fisE7gs7mac/swbSMHdh8pif08/9wuktuSbNW60pnY2LTgKCvHSnA0d2Vj0HRZ", - "q0FaED9AE4pGfnRhvPkO7bUsA0IxDVJUutFTTWfvYGw8CWaE5gGjFeM4AZqEcRZbH4kGQR4KUsHtcx+d", - "a8NB9lZk0L828GL4tQK5a24GB+Nk2uIbbuMj1YjuzIb7x/z6ZmSBpmvrd+lrJ/UbH62/5xvKtLlcXEwY", - "YrTfWQPN5y5hrvNrE/zd+4IR7cGP7Sf2I7/O64J+0Y9dFTH21alIvlFjAwptKrjntTXlwyezdVgPxpFD", - "YyI4mc8xkGItlJ5PrqefO+aD8OOnerd8oYB6164/Xf//AAAA//9kloV/+q8AAA==", + "H4sIAAAAAAAC/+x9+3PcNtLgv4Ka/ar8uOFIfmXXqtr6TrGSrM6PuCxt9u6zfAmG7JnBigMwBCjNxKf/", + "/QoNgARJgMORFHvz1f5ka4hHo9Fo9BufJ6lYF4IDV3Jy9HlS0JKuQUGJf9E0FRVXCcv0XxnItGSFYoJP", + "jtw3IlXJ+HIynTD9a0HVajKdcLqGpo3uP52U8GvFSsgmR6qsYDqR6QrWVA+stoVuXY+0SZYisUMcmyFO", + "TyY3Ax9olpUgZR/KH3m+JYyneZUBUSXlkqb6kyTXTK2IWjFJbGfCOBEciFgQtWo1JgsGeSZnbpG/VlBu", + "vVXayeNLumlATEqRQx/OV2I9ZxwcVFADVW8IUYJksMBGK6qInkHD6hoqQSTQMl2RhSh3gGqA8OEFXq0n", + "Rx8nEngGJe5WCuwK/7soAX6DRNFyCWryaRpa3EJBmSi2Dizt1GK/BFnlShJsi2tcsivgRPeakbeVVGQO", + "hHLy4ftX5NmzZy/1QtZUKcgskUVX1czur8l0nxxNMqrAfe7TGs2XoqQ8S+r2H75/hfOf2QWObUWlhPBh", + "OdZfyOlJbAGuY4CEGFewxH1oUb/uETgUzc9zWIgSRu6JaXyvm+LP/1V3JaUqXRWCcRXYF4Jfifkc5GFe", + "9yEeVgPQal9oTJV60I+HyctPn59Mnxze/OnjcfJf9s8Xz25GLv9VPe4ODAQbplVZAk+3ybIEiqdlRXkf", + "Hx8sPciVqPKMrOgVbj5dI6u3fYnua1jnFc0rTScsLcVxvhSSUEtGGSxolSviJiYVzzWb0qNZaidMkqIU", + "VyyDbKq57/WKpSuSUmmGwHbkmuW5psFKQhajtfDqBg7TjY8SDdet8IEL+tdFRrOuHZiADXKDJM2FhESJ", + "HdeTu3Eoz4h/oTR3ldzvsiLnKyA4uf5gLlvEHdc0nedbonBfM0IlocRdTVPCFmQrKnKNm5OzS+xvV6Ox", + "tiYaabg5rXtUH94Y+nrICCBvLkQOlCPy3Lnro4wv2LIqQZLrFaiVvfNKkIXgEoiY/xNSpbf9f539+I6I", + "krwFKekS3tP0kgBPRRbfYztp6Ab/pxR6w9dyWdD0Mnxd52zNAiC/pRu2rtaEV+s5lHq/3P2gBClBVSWP", + "AWRG3EFna7rpT3peVjzFzW2mbQlqmpSYLHK6nZHTBVnTzV8PpxYcSWiekwJ4xviSqA2PCml67t3gJaWo", + "eDZChlF6w7xbUxaQsgWDjNSjDEBip9kFD+P7wdNIVh44bpAoOPUsO8DhsAnQjD66+gsp6BI8kpmRv1vO", + "hV+VuAReMzgy3+KnooQrJipZd4rAiFMPi9dcKEiKEhYsQGNnFh2ae5g2lr2urYCTCq4o45BpzotACwWG", + "E0Vh8iYcVmb6V/ScSvjmeewCb76O3P2F6O764I6P2m1slJgjGbgX9Vd7YMNiU6v/COXPn1uyZWJ+7m0k", + "W57rq2TBcrxm/qn3z6GhksgEWohwF49kS05VVcLRBX+s/yIJOVOUZ7TM9C9r89PbKlfsjC31T7n56Y1Y", + "svSMLSPIrGENalPYbW3+0eOF2bHaBJWGN0JcVoW/oLSllc635PQktslmzH0J87hWZX2t4nzjNI19e6hN", + "vZERIKO4K6hueAnbEjS0NF3gP5sF0hNdlL/pf4oi171VsQihVtOxvW/RNmBtBsdFkbOUaiR+sJ/1V80E", + "wGgJtGlxgBfq0WcPxKIUBZSKmUFpUSS5SGmeSEUVjvQfJSwmR5M/HTTGlQPTXR54k7/Rvc6wk5ZHjYyT", + "0KLYY4z3Wq6RA8xCM2j8hGzCsD2UiBg3m6hJiWkWnMMV5WrW6CMtflAf4I92pgbfRpQx+O7oV1GEE9Nw", + "DtKIt6bhA0k81BNEK0G0orS5zMW8/uHhcVE0GMTvx0Vh8IGiITCUumDDpJKPcPm0OUn+PKcnM/KDPzbK", + "2YLnW305GFFD3w0Le2vZW6w2HNk1NCM+kAS3U5QzvTUODVqGvw+KQ51hJXIt9eykFd34b7atT2b691Gd", + "/xgk5uM2TlyoRVnMGQUGf/E0l4cdyukTjrXlzMhxt+/tyEaPEiaYW9HK4H6acQfwWKPwuqSFAdB+MXcp", + "46iBmUYG1jty05GMLgizd4Y9WkOobn3Wdp6HICRICh0Yvs1Fevk3Klf3cObnbqz+8cNpyApoBiVZUbma", + "TUJShn+8mtHGHDHdELV3MvemmtVLvK/l7VhaRhX1lmbhDYslBvXYD5kelAHd5Uf8D82J/qzPtmb9ZtgZ", + "OUcGJs1xth6ETKvyRkEwM+kGaGIQZG20d6K17r2gfNVMHt6nUXv0nTEY2B2yi8AdEpt7Pwbfik0Ihm/F", + "pncExAbkfdCHHgfFSAVrOQK+EwuZwP236KNlSbd9JOPYY5CsF6hFV4mngfs3vp6lsbwez0V5O+7TYSuc", + "NPZkQvWoHvOddpCETasisaQYsEmZBp2BGhfeMNPoDh/CWAsLZ4r+DliQetT7wEJ7oPvGglgXLId7IP1V", + "kOnPqYRnT8nZ345fPHn689MX32iSLEqxLOmazLcKJHlodTMi1TaHR/2VoXZU5So8+jfPnRWyPW5oHCmq", + "MoU1LfpDGeumEYFMM6Lb9bHWRjOuugZwzOE8B83JDdqJMdxr0E6Y1BLWen4vmxFDWNbMkhELSQY7iWnf", + "5TXTbP0lltuyug9VFspSlAH7Gh4xJVKRJ1dQSiYCrpL3tgWxLZx4W3R/N9CSayqJnhtNvxVHgSJAWWrD", + "x/N9M/T5hje4GeT8Zr2B1dl5x+xLG/nOkihJAWWiNpxkMK+WLU1oUYo1oSTDjnhH/wDqbMtTtKrdB5HG", + "1bQ142jil1ueejqb3qgcsmVrE+6um3Wx4uxzZqoHMgCORscb/Ixq/Qnkit67/NKdIAT7K7eRBliS6Yao", + "Bb9hy5XyBMz3pRCL+4cxNEsIUPxgxPNc9+kL6e9EBnqxlbyHy7gZrKF1vac+hdO5qBShhIsM0KJSyfA1", + "HXHLoz8Q3ZjKv/nVykjcc9CElNJKr7YqCDrpepyj6ZjQ1FBvgqiRES9G7X4yrcx0xuWbl0AzrdUDJ2Ju", + "XQXWiYGLpOhhVO6is0JC4Cy14CpKkYKUkCXWRLETNNfOMBE1gCcEHAGuZyFSkAUt7wzs5dVOOC9hm6A/", + "XJKHr3+Sj74CvEoomu9ALLYJobdW+Kw/qA/1uOmHCK47uU92tATieK7WLjWDyEFBDIV74SS6f12Iert4", + "d7RcQYmemd+V4t0kdyOgGtTfmd7vCm1VRKK8rKJzztZot+OUCwmp4JkMDpZTqZJdbFk3amljegUeJwxx", + "Yhw4IpS8oVIZbyLjGRpBzHWC8xgBRU8RBzgqkOqRf3KyaH/sVN+DXFayFkxlVRSiVJCF1sBhMzDXO9jU", + "c4mFN3Yt/SpBKgm7Ro5hyRvfIsusxCCIqtrobt3t/cWhaVrf89sgKltANIgYAuTMtfKw60e6RABhskG0", + "IRwmO5RTh9dMJ1KJotDcQiUVr/vF0HRmWh+rvzdt+8RFVXNvZwL07MrBZCG/Npg1MU4rqlVoHJms6aWW", + "PVAhNm7PPsz6MCaS8RSSIcrXx/JMt/KPwI5DGrFF2ChKb7bO4ejQb5DookSwYxdiC44YRt7TUrGUFSgp", + "vobtvQvO3QmC5nqSgaJMK+veByNEF35/YvzY3TFvJ0iP0mH74PeU2MBycibxwmgDfwlb1FjemwCpcy+s", + "6h40gcCo+nRTThBQF3ahBRi/CWxoqvKtvubUCrbkGkogspqvmVIm4q2tKChRJP4AQfvgwIzWGG6Ci9wO", + "jLHOn+FQ3vL6WzGdGIlqGL7zjljVQoeVpAoh8hG6dw8ZQQhG+U1JIfSuMxtg6aLwHCW1gLRCDHpCaub5", + "QLbQjCsg/0dUJKUcBdZKQX0jiBLZLF6/egZ9gdVzWg9pgyHIYQ1GDscvjx93F/74sd1zJskCrl1Usm7Y", + "Rcfjx6gFvxdStQ7XPVha9HE7DfB2NJzqi8LKcF2esttDZ0ces5PvO4PX1lZ9pqS0hKuXf2cG0DmZmzFr", + "92lknHcSxx1lE/WGDq0b9x3NPL+PjaYZOgRdf2LPqd58jPnVtXyVb++BT5uBSAlFCRJPla+XSPNVLPzA", + "dXvs5FYqWPdNN6brzxHB5oMTC3pSpuA545CsBYdtMFeLcXiLH0O9zcmOdEYeG+vbFZta8HfAas8zhgrv", + "il/cbY+U39cBJfew+d1xO1Y7P2QftVLIC0JJmjPUWQWXqqxSdcEpSsXeWQ443pysH9eTXrkmYcUsoDfZ", + "oS44RadrLSsHnQULCGjB3wM4dUlWyyVI1ZEPFgAX3LZinFScKZxrrfcrMRtWQIner5lpuaZbsqA5qnW/", + "QSnIvFLtGxMji6XSWpcxIeppiFhccKpIDloDfcv4+QaHcyZ4RzMc1LUoL2sszILnYQkcJJNJ2EH4g/mK", + "sRt2+Ssbx4FpXuazMTrp8Zvw462CVurS/334n0cfj5P/oslvh8nL/3Hw6fPzm0ePez8+vfnrX/9f+6dn", + "N3999J//EdopB3so7tVCfnpipcnTExQZGqtTD/YvZnFYM54Eicz3rXRoizzUgo8joEeNWc/u+gVXG64J", + "6YrmLKPqduTQZXG9s2hOR4dqWhvRUSDdWve8iO/AZUiAyXRY462v8b5PPRxhjmZQGzSO52VRcbOVlbSm", + "WAygdL5NsZjWWQQme/iIYIj5ijrHvP3z6YtvJtMmNLz+rvVr8/VTgJJZtgklAGSwCclX9oDgwXggSUG3", + "ElSYeyDsQTeu8Sb5w65BC+ZyxYovzymkYvMwh3NhaVZP2/BTbuLF9PlBo+rW2mrE4svDrUqADAq1CmUV", + "tiQFbNXsJkDH0VWU4gr4lLAZzLp6UrYE6RzKOdAFZrehYVCMCbOtz4EhNEcVHtb9hYxSRkL0g8Kt5dY3", + "04m9/OW9y+N24BBc3TlrC6r7Wwny4IfvzsmBZZjygclFMUN72QMB+4MNkG25QDU3M7nUJhnngl/wE1gw", + "zvT3owueUUUP5lSyVB5UEspvaU55CrOlIEcu5vaEKnrBe5JWtNyBF+1Mimqes5Rc+hJxQ54mhbU/wsXF", + "R5ovxcXFp543qC+/2qmC/MVMkFwztRKVSmyOXlLCNS2zAOiyztHCkU2G7dCsU2LHNqzY5gDa8cM8jxaF", + "7OZq9JdfFLlevkeG0mYi6C0jUonSySJaQDHQ4P6+E/ZiKOm1S/CsJEjyy5oWHxlXn0hyUR0ePgPSSl74", + "xV75mia3BbQsVbfKJelaqXDhRq+BjSppUtAlyODyFdACdx/l5TXaRPOcYLdW0oQLCsOhmgU4fMQ3wMCx", + "dwA4Lu7M9HLFFsJLwE+4hdhGixuNq+G2++WlUdx6uzqpGL1dqtQq0Wc7uCqpSdztTJ2DvdRClvP/SLbE", + "GBubrj4Hkq4gvYQMM2dhXajttNXduRitoOlYB5Mmw9wEQWMaJBr15kCqIqNWFKd8281Hk6CUC/L5AJew", + "PRdNFuU+CWjtfCgZO6hIqZ50qYnVP7Z2jO7mWz825oAUhUsrwvhyRxZHNV24PvGDbETeezjEIaJo5evE", + "EEHLACIM8UdQcIuF6vHuRPqh5WktY25uvkBCuuP9xDZplCfrcvZXg2lI5vsasFyFuJZkTrXcLmylBZPz", + "43GxStIlRCRk3646MrOmZYvFQXbde8GbTiy6F1rvvgmCbBones1BSgH9RZMKKjOdQAM3kzHd4wpmBAso", + "WYTNcxST6ogMw3Ro2bJvm4owMdDCBAwlbwQOB0YbI75ks6LSFYHAWhnuLI+SAX7HHLahzOVTz0fuFcSo", + "85Idz+2e0552afOXXdKyy1T2VcsRWcdawsewvNB2CI4CUAY5LM3CTWNHKE0+XbNBGo4fF4uccSBJyN1O", + "pRQpM1U8mmvGzgFaPn5MiDEBk9EjhMjYAxtdUjgweSf8s8mX+wDJbT4gdWOjM8v7G8KhyyYATYs8otAs", + "nPFIqKPjANTGaNT3VydSCIchjE+JZnNXNNdszmp8zSC9BFoUWzvpstYp+igmzg5Y4M3FsteazFV0m9X4", + "MpMDOizQDUA8F5vE5C4EJd75Zq7pPRiTh5kUoYNpUpUfSDIXG3S049ViYsB2wBKHw4HhafgbJpFesV/s", + "NjfADE07LE2FqFAiyVhzXk0uMXFizNQRCSZGLg+97ONbAdAxdjR1+qzyu1NJbYsn/cu8udWmTVUNF+4c", + "Ov6xIxTcpQj++laYOl/YmhA+QCrKLG6n0ITKVF34sG9esGUbNd8YnVE8UITxuK1tOBWiv3MRf3ALnmae", + "AUScmGD9HiTfbQqhpVsTzG8yuy1SjJxYgslRksZmJRlf5lYwiKEptGAXjeIwbpbcVGpxA46TnUObG1Hy", + "h2ApijAc+2gqHyx+BqCInPIGDpTD7wiJze4ehOUmTh/vu6J98KC0AyvaNQU8XSt0O2jy6Xsz+z5TCTmg", + "9py0tI3kMuTjvrj4KAFFszPXzbPyYeUCyrePvGidEpZMKmi8TVqCdZj+0nZ8igWThFjEV6eKcqHX90GI", + "Wp4zFTmwY2uZX3wFV0JBsmClVAm66oJL0I2+l2h9+l43DSsV7XggUzuQZeFLFKe9hG2SsbwK06ud9/WJ", + "nvZdLTvIao6CCeMEaLoic6x1GYwSHJjaBJIOLviNWfAbem/rHXcadFM9canJpT3HH+RcdG66IXYQIMAQ", + "cfR3LYrSgQvUy43rc0dPwTCHE6/T2ZCboneYMjf2zvgql6EXE+bMSANrwdCgaFhmICCHLEtRFYapN2Wu", + "g1lsXKikZfwIoKs28EhFL00mRnuD+bK2qYTDpoxePWpo23bHgHz8eHz3cFYITnK4gnx3+CtFjDsDDkZG", + "mBEw9IZgILmL8dgt1fd3oEFYvdIujEFq6Uk3Q47bRjWyhaca3RoJVuPOpoyO9t5pCc3RW0PffdddUSQZ", + "5BBM0PiHl4FBiwLTrF3jULKCHozxDDZhcMynaagYdd94XzGuTOHC+6qJ1hln/LL9ymFjUFCYGlf7112L", + "65jeLvloji8qQpS1c2CQEePgtWbnlfHvUl/kGqdFwbJNx+9pRo1ax+8FY3hB2cF2YMCjjVDqTwmyXTGu", + "MeaZusWtgi2zUZg5b9d182UafyomXdX9PqLq1MBduDoHmr+G7U+6LS5ncjOd3M1NGsK1HXEHrt/X2xvE", + "M4bhGbdZK+phT5TToijFFc0T60yOkWYprixpYnPne/7C0lqY651/d/zmvQX/ZjpJc6BlUms70VVhu+IP", + "sypTnC5yQFxV7xVVtX3OaMPe5tcVtXwH9PUKbAVlT6HulXpsggu8o2gd0otwNPBO97KNgzBLHIiHgKIO", + "h2hcdSYaoh0BQa8oy52PzEEbidzFxY27G4NcwR/gzpEU/l10r+ymd7rDp6Ohrh08yZ9roMbz2pQxl0Tw", + "bric1oLR9YakuqZYqNF4QPrMiVdr9BokMmdp2J/K51ITBzdxMroxwcYRfVqPWLFI2BWvmDeWbiZHGLU7", + "QHpzBJHpin7GcDcX9v2ZirNfKyAsA670pxJPZeegov3Uetb712lYqrQDG298M/xdZAy/SGn3xrMy15CA", + "4Ufl9MA9qa1+bqG190n/4IUf7BHc58/YuxIHAvMsfVhqNokKq3Z0zWgJfedbNc7+ZqulRuYIvj3DZLIo", + "xW8QNlWhhS+QF+jKsjKMaP0N+CwgrndZTO3JaZ7QaWaPbndMuvE9Tu2AxAjV4857IThYH9J5oyk3W22e", + "gmjFtYcJxs8gOTDjNwRjYe5l3eT0ek5DxTO1kKFh8twvLb+5EsR1dri3PhpmK+XOiBc3VrdlJmO+gLJJ", + "2e1X37mlwGCmHS0qNJIBUq0vE0xNrE8uRWCYil9Tbl4UQW8EHiXbWyv4ziB0LUqsdyHDLv4MUrYOGpcu", + "Lj5mad+dm7ElM+9pVBK8BxvsQOYhIkNF9tELE07XoOZ0QQ6n3pMwdjcydsUkm+eALZ6YFnMqwRhVXOSG", + "66KXB1ytJDZ/OqL5quJZCZlaSYNYKUgt1KF6UweqzEFdA3ByiO2evCQPMURHsit4pLFo7+fJ0ZOX6GA1", + "fxyGLgD7cM4QN8mQnTj9P0zHGKNkxtCM2446C1oDzGtnccY1cJpM1zFnCVtaXrf7LK0pp0sIR4Wud8Bk", + "+uJuoi+ggxeemad6pCrFljAVnh8U1fwpkmmm2Z8Bg6RivWZqbQM5pFhrempeYzCTuuHMuz+2kK6Dy33E", + "eKjChYN0lMgv6/cx91to1Ri19o6uoY3WKaGmyEnOmkhFV96bnLoaSlhZuC4obHCj59JLRzEHAxcXpCgZ", + "V6hYVGqR/IWkK1rSVLO/WQzcZP7N80A15XZVT74f4F8c7yVIKK/CqC8jZO9kCNuXPOSCJ2vNUbJHTWan", + "dyqjgVvhEJ1YnNDw0GOFMj1KEiW3qkVu1OPUdyI8PjDgHUmxXs9e9Lj3yr44ZVZlmDxopXfo7x/eWClj", + "LcpQYcTmuFuJowRVMrjCOP3wJukx77gXZT5qF+4C/dd1njqR0xPL3FmOKgL7eHw83QB9Pn5k4m28PW1P", + "T0vmCrp9UMMZ5wExjwXu8nvc5RmRVud9oHIcehx0ESNCKwG2g7H9NOC7mxg8l09rh2I4ai8tRJnfisCS", + "Xe352sdjMyYDdqvYBaI/aAY1t0NNSbvO95ePqHFukX5kh/7iYMU/usB+ZWaDSHYriGyi9wZBcDuz+rsX", + "XEbJt2IzdlM7vNtt7L8AaoIoqVie/dTUBuk88VBSnq6CwSJz3fHn5jG6enHmMAcrY64o5yYaoW+bQC3l", + "Z6fNBPStf4qx86wZH9m2++qEWW5ncQ3gbTAdUG5CjV6mcj2Bj9V22YU6rS9fiozgPE0ZxuZe779W4tWU", + "/7UCqUL3In4wqQVoUV9oKjal3YFnaMeYkR/MY9IrIK0qcWg/YOsqNxXHTIFt4+qpilzQbEr0OOffHb8h", + "ZlbTxzypZEqqL82121pFPD53n0Dbodja+8jo06uWCos2SkXXRahEiW5x7hpgHRTfu4SKtY+dGTkxNg3p", + "NGYziaaHBSvXkJF6OitVI03o/yhF0xUaC1osNU7y498CcFQpvfc363e06rKreO403PY5APMawJQILTlc", + "M2neEIYraFdFqUsEWTHAVUlpL6+sODeUEpSKh0pY3QbtDjgTBekcUEHIOojfU3qxYep7Po1whr2CdQy7", + "7yz0Ht40NTbq95Hc2/Ap5YKzFKsIhq5m+x7xGO/siIKL4cwAG28jJ4HDFXzdoU7WsFiMvvfgGKFFXN89", + "5H3Vm2qow/yp8OHbFVVkCUpazgbZ1D1SYi3UjEuwZXTxaWqPT4qy5fFGDhkMomjk5D3JCJOzIyaH7/W3", + "d9YghVmLl4yj6ulyJEyCpLEh43OpSuurTJGlwAwKeyj8NX3UfWZYrCWDzaeZe14VxzAOY71sEx3RH+rY", + "xUrY2ATd9pVuawrqNT+38uDMpMdFYSeNP2ETlAfUhkcRHPB514FeHnLr8f3RBshtMMgJ71NNaHCFIRJQ", + "EJsaE3nOpZMEo4VWQ1HYgpj46GAdrWCY6BvGoXn8N3BBpMErATcGz2ukn0xLqowIOIqnnQPNMS4ixNCk", + "sk6xuw7V2WAbT1qkEzdHfBubl2gijKNu0AhulG/rN4c1dXvCxCt87Nwisv+uDEpVVoiyyTXtl2ZCjEMz", + "bveWVfsC6B+DvkxkuquSmpOzz00UK1Uyr7IlqIRmWcie8C1+JfiVZBVKDrCBtKrrNxcFSbEyX7tUYZ/a", + "7ESp4LJaD8zlGtxxOu/ppgA1+M9HuR3GwOv5Fv8NFS+O74wND9o7xt7FAmV1+tw+cnN7pJ7Uq2k6kWyZ", + "jMcE3il3R0cz9e0Ivel/r5Sei2UbkC9coGyIy/l7FOJv3+mLw6/f1avIba6WurwWhoMK9+Amqo11YZg2", + "V3JZp705vQf9hg0Q8af5pnj5RfJaPFsvNfer8WvHslvSaDIWVbZ+gqJkkAVFc9JNXJnJPkcowjb9WCyZ", + "CSXTn3u9x0mGPTkbxx5EqAtS7AP02kVAk4IyG7TRMIs+Zm26V9xcOHTomg3uLsImUUUtdq+vYglPLg/Y", + "ZHZ0HjO7BFtUqSjhionKhUO4eDmnEppf7WPSXl5xdP39uBmc6uuaQaNG23P7cIZZptXJX/9koisJcFVu", + "/wVMuL1N7z0FF6pZ3HoIzgpXQXuTGntXntSvyV1eJWuRDSVMv/6JnDjf0qh7xxFyqNySyOzzS8Fk8Te2", + "+L9rpqXP0dO+tZ2Oi2J46kiGeH9y03Df6WOlpvT5HLK6vXfn1zyg55sQArqKl87MYaPCT+X0smGvgcCm", + "AKx16yU2x6tnjCUom+SI2mqSA5UwgGG/apttOxLJ55s3uv24ZPvwE4bxkrNNmVlknoWQrHmWJfS24ciQ", + "43N8ntDzGPbHcvF+V5AqUbbimEqAfQro6sm8d3P/XXo2YiipI7Md/Q+UmZ1OfN4STFS0x4s2JXLQq4Yu", + "10CpetMmwOxtZ6YPSQVTN4T+YUFzGX6lKhrs2ql84gWsBAo9hxd2mo2o9m2XM/ViIFg2jMhwJoAJ/v7v", + "iUwT136/6Oy91jSsVfQKL3jFQ8yjOrM9AkjqKGqUDHG/lsDtk8qLEGp2Z0UtFpAqdrWj0MU/VsC9IgpT", + "ZwlGWBZe3QtWZ9lgQdH9/RwNQEN1KAbh8Qr73xmcWI7oJWwfSNKihuArP1Mn3N+mliRiAG8tLXgUQoai", + "FI3rygaOMVlTBmLBRQWb7tBU5Y4+r+jJObecy5FkW+IZmPJKhGzfo+bSXfeqBIYJI7FaGP0HzuIWjxN8", + "T07WTx+7WpS+XZCc9iv2X9talliWpPbWuqqWIN1vrgaRmSVnl+A/AIm+cSyhYFsEjb3OjpwMyEm97G/3", + "OFcX6EU9M2tyOPr5voEa0Bj9lOZCK8FJLN2pnTZRh3k9kCY4FMUUfDkO4VpAaR/KxZshFxISJVxo3RAc", + "Q6gwEbC3QoKMvrtggItWQ/3QlHvF92dMsQxqA1/9BZIS1lRDV3pFWeNzDiH7lfnuElxdTa6dNu2aXpOd", + "VVVd9g6TPST6VL8g9rbcnTh7G/M249w8yy9DMYVco9L3vxalyKrUFoLxDkbtAhhdsGyAlQQtw2l/lT0j", + "X47VwN94ZQguYXtg7C/pivKlV17Nh96I9mYNXuWyzm7fq+U/bOTMl2YBy3uB82taz6eTQog8iThcT/uF", + "Zrtn4JKll1rMrpq498gTi+Qh+vnqiJrr1dYVVi0K4JA9mhFyzE2mkQuuab901JmcP1BD829w1qwytZ+t", + "YX92wcMpG1jUp7wjf3PDDHM1CZr53XEqM8iOMqabSJHbkl4HHhztx9ONDnfpPgLZEJWBIiSl3LJU16jz", + "3TfuB0jfewVxWPvxK/k1Ucyl8RGhtOQ8N13h5W3j+hn3HqPrsAM831jjvcjouJEF5yuHGr+tkeItJUoJ", + "reXvsv/YBTZ8ydsiiVmTepmmALEJU2vvi2fck69qm1kYz33TGpbtExxr/vZNchJ9hqYMq0c4+lyWVzT/", + "8mY1rOd4jPiwz4qHF+rrvz6SDSrl7eL93tBRc3u67v1Nzd+jGfAfoPco6Oy1Q1nnT/0SpnORYYl7mpNc", + "NC/i4pDkGsc03uEn35C5zaIrSkiZZJ0E42v3qkmt7uEjX81r88P65a51/iTUHcjYKgiiIO+aFxKUwPuh", + "gbA5ol+ZqURObpDKQ9TXI4sA/kI8yi9ns+O6uGy5jc2LM514SFHCPbuPvUCwPd3H/UI9Y5dnXKT60qkk", + "9Nc5+rZu4TZwUTdrGxv70EfuUBn9MSEL4dcxdHeMmTAIwadlCIJKfnnyCylhgW9HCvL4MU7w+PHUNv3l", + "afuzPs6PHwfFuC8WLWFwZMew8wYpxjrTeqkwsClYGSn698Eyd3tho/uOYAcIV+fMIfgaDE7t4ka/cClo", + "lLl3GvjN0mzjXfzMQ5lbcj1RCPc/xXIXTHx+JE2mcxYqlme7DmUr6al5+RbTen62Cblf5e3dn40tu88m", + "7fuH+8TIdQ8AIiaw1tbk3lReOtOITCbbLZC3hMSVViVTW6wT5kyf7OdgTM0PtbfEeoHryjJW7lDiEupK", + "c41vpZJOsvlB0BxlAa3PYISiEiKfke82dF3kYJnUXx/M/wzP/vI8O3z25M/zvxy+OEzh+YuXh4f05XP6", + "5OWzJ/D0Ly+eH8KTxTcv50+zp8+fzp8/ff7Ni5fps+dP5s+/efnnB/oO0CAbQCeuKsXkf+MD1cnx+9Pk", + "XAPb4IQW7DVszVuYmozdK5s0RS4Ia8ryyZH76X867jZLxboZ3v06sUnvk5VShTw6OLi+vp75XQ6WaExN", + "lKjS1YGbp/cM5/H70zo9zMRC4Y6azB9NCriplhSO8duH787OyfH701lDMJOjyeHscPYEaxkXwGnBJkeT", + "Z/gTnp4V7vuBKyJ89PlmOjlYAc3RJ67/WIMqWeo+yWu6XEI5s8+N6p+unh44Me7gszUk3wx9O/Bf7jn4", + "3LK3Zzt6YqDLwWdXxGq4datKlPUzeB1GQjHU7GCOGchjm4L0GseXgsqdPPiM6kn09wOblhn+iGqiOQMH", + "zikVbtnC0me10bB2eqRUpauqOPiM/0GavDFMIoeQC8pkM1LSNJ8SpgidixKrR6l0pfmCK1vDpNdygpRq", + "iPw008Ste70yELgCdaZi79HHfgAiDkTcSMgJNJk3B7U1U8OL0e/uFZGtb5pW++a++XiYvPz0+cn0yeHN", + "n/R9Yv988exmpC/5VT0uOasvi5ENP2HNF7SK4/l9eni419PAPbW0WaTZpDocORDEYHYiWccsJ3arOgOR", + "Ghk7alN0hg89pXwznTzfc8WDtrtWiHbgSeRvaUZcgi/O/eTLzX3K0ZOv+Tox99bNdPLiS67+lGuSpznB", + "ll6xsf7W/51fcnHNXUstZFTrNS237hjLFlMgdrPxKqNLiZbckl1RlO244O1y9Z/QexBKso7wG6noLfjN", + "me71b37zpfgNbtJ98Jv2QPfMb57ueeb/+Cv+N4f9o3HYM8Pu7sRhrcBn8tr6EqiJ7D/A+mLb/s9bngZ/", + "7A/UfTI49PPB5/ZLPC0ZWa4qlYlrUx4leClgrWaa28KOaICuFSoliBugCSgkP9qsq3yLVneWAaEY3S4q", + "1Wi8urNzEzfmJT1C85z4knGcAA37OIupYEq9UB0JqeDm8d3OBWQheycy6F9AeMX8WkG5be4YC+Nk2uJA", + "loQC9ULvzND7DONmPwJDB4TxnvWJo35xt/X3wTVlSl9TNrIPMdrvrIDmB7ZwQOfXJlev9wUTEL0fPZ0o", + "/OtBXQ8r+LGrbIa+WmXLNWqsSb51Bve8tst8/KS3Dis2WnJojA1HBwcYDrMSUh1MbqafO4YI/+Onerdc", + "waR6124+3fz/AAAA///m3Czx7MQAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/algod/api/server/v2/generated/nonparticipating/public/public_routes.yml b/daemon/algod/api/server/v2/generated/nonparticipating/public/public_routes.yml index c549edad3b..0cc18f190c 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/public/public_routes.yml +++ b/daemon/algod/api/server/v2/generated/nonparticipating/public/public_routes.yml @@ -10,6 +10,7 @@ output-options: - private - common - participating + - data type-mappings: integer: uint64 skip-prune: true diff --git a/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go b/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go index 234fa90c88..3ec9dfd564 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go +++ b/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go @@ -548,215 +548,227 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/algod/api/server/v2/generated/participating/private/private_routes.yml b/daemon/algod/api/server/v2/generated/participating/private/private_routes.yml index e9e874bf6e..708b8cba16 100644 --- a/daemon/algod/api/server/v2/generated/participating/private/private_routes.yml +++ b/daemon/algod/api/server/v2/generated/participating/private/private_routes.yml @@ -9,6 +9,7 @@ output-options: exclude-tags: - public - nonparticipating + - data - common type-mappings: integer: uint64 diff --git a/daemon/algod/api/server/v2/generated/participating/private/routes.go b/daemon/algod/api/server/v2/generated/participating/private/routes.go index ca1a6eef44..6b1676e8e8 100644 --- a/daemon/algod/api/server/v2/generated/participating/private/routes.go +++ b/daemon/algod/api/server/v2/generated/participating/private/routes.go @@ -158,162 +158,175 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "pRK6AAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/algod/api/server/v2/generated/participating/public/public_routes.yml b/daemon/algod/api/server/v2/generated/participating/public/public_routes.yml index b1dc1f6753..5dac08d5b6 100644 --- a/daemon/algod/api/server/v2/generated/participating/public/public_routes.yml +++ b/daemon/algod/api/server/v2/generated/participating/public/public_routes.yml @@ -10,6 +10,7 @@ output-options: - private - common - nonparticipating + - data type-mappings: integer: uint64 skip-prune: true diff --git a/daemon/algod/api/server/v2/generated/participating/public/routes.go b/daemon/algod/api/server/v2/generated/participating/public/routes.go index 541e80e663..613a5d1cd0 100644 --- a/daemon/algod/api/server/v2/generated/participating/public/routes.go +++ b/daemon/algod/api/server/v2/generated/participating/public/routes.go @@ -177,168 +177,183 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "Cjn3ciPIAAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 07816a504f..60a0a8e5de 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -30,7 +30,10 @@ import ( "github.com/labstack/echo/v4" + "github.com/algorand/go-codec/codec" + "github.com/algorand/go-algorand/agreement" + "github.com/algorand/go-algorand/catchup" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklearray" @@ -47,7 +50,6 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/go-algorand/stateproof" - "github.com/algorand/go-codec/codec" ) // max compiled teal program is currently 8k @@ -84,6 +86,7 @@ type LedgerForAPI interface { EncodedBlockCert(rnd basics.Round) (blk []byte, cert []byte, err error) Block(rnd basics.Round) (blk bookkeeping.Block, err error) AddressTxns(id basics.Address, r basics.Round) ([]transactions.SignedTxnWithAD, error) + GetStateDeltaForRound(rnd basics.Round) (ledgercore.StateDelta, error) } // NodeInterface represents node fns used by the handlers. @@ -104,6 +107,9 @@ type NodeInterface interface { GetParticipationKey(account.ParticipationID) (account.ParticipationRecord, error) RemoveParticipationKey(account.ParticipationID) error AppendParticipationKeys(id account.ParticipationID, keys account.StateProofKeys) error + SetSyncRound(rnd uint64) error + GetSyncRound() uint64 + UnsetSyncRound() } func roundToPtrOrNil(value basics.Round) *uint64 { @@ -945,6 +951,63 @@ func (v2 *Handlers) TealDryrun(ctx echo.Context) error { return ctx.JSON(http.StatusOK, response) } +// UnsetSyncRound removes the sync round restriction from the ledger. +// (DELETE /v2/ledger/sync) +func (v2 *Handlers) UnsetSyncRound(ctx echo.Context) error { + v2.Node.UnsetSyncRound() + return ctx.NoContent(http.StatusOK) +} + +// SetSyncRound sets the sync round on the ledger. +// (POST /v2/ledger/sync/{round}) +func (v2 *Handlers) SetSyncRound(ctx echo.Context, round uint64) error { + err := v2.Node.SetSyncRound(round) + if err != nil { + switch err { + case catchup.ErrSyncRoundInvalid: + return badRequest(ctx, err, errFailedSettingSyncRound, v2.Log) + default: + return internalError(ctx, err, errFailedSettingSyncRound, v2.Log) + } + } + return ctx.NoContent(http.StatusOK) +} + +// GetSyncRound gets the sync round from the ledger. +// (GET /v2/ledger/sync) +func (v2 *Handlers) GetSyncRound(ctx echo.Context) error { + rnd := v2.Node.GetSyncRound() + if rnd == 0 { + return notFound(ctx, fmt.Errorf("sync round is not set"), errFailedRetrievingSyncRound, v2.Log) + } + return ctx.JSON(http.StatusOK, model.GetSyncRoundResponse{Round: rnd}) +} + +// GetLedgerStateDelta returns the deltas for a given round. +// This should be a representation of the ledgercore.StateDelta object. +// (GET /v2/deltas/{round}) +func (v2 *Handlers) GetLedgerStateDelta(ctx echo.Context, round uint64) error { + sDelta, err := v2.Node.LedgerForAPI().GetStateDeltaForRound(basics.Round(round)) + if err != nil { + return internalError(ctx, err, errFailedRetrievingStateDelta, v2.Log) + } + consensusParams, err := v2.Node.LedgerForAPI().ConsensusParams(basics.Round(round)) + if err != nil { + return internalError(ctx, fmt.Errorf("unable to retrieve consensus params for round %d", round), errInternalFailure, v2.Log) + } + hdr, err := v2.Node.LedgerForAPI().BlockHdr(basics.Round(round)) + if err != nil { + return internalError(ctx, fmt.Errorf("unable to retrieve block header for round %d", round), errInternalFailure, v2.Log) + } + + response, err := stateDeltaToLedgerDelta(sDelta, consensusParams, hdr.RewardsLevel, round) + if err != nil { + return internalError(ctx, err, errInternalFailure, v2.Log) + } + + 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 { diff --git a/daemon/algod/api/server/v2/test/handlers_resources_test.go b/daemon/algod/api/server/v2/test/handlers_resources_test.go index 2937baa2c7..d9ab3832ab 100644 --- a/daemon/algod/api/server/v2/test/handlers_resources_test.go +++ b/daemon/algod/api/server/v2/test/handlers_resources_test.go @@ -47,6 +47,10 @@ type mockLedger struct { blocks []bookkeeping.Block } +func (l *mockLedger) GetStateDeltaForRound(rnd basics.Round) (ledgercore.StateDelta, error) { + panic("implement me") +} + func (l *mockLedger) LookupAccount(round basics.Round, addr basics.Address) (ledgercore.AccountData, basics.Round, basics.MicroAlgos, error) { ad, ok := l.accounts[addr] if !ok { // return empty / not found diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 30ee0f7995..437dca2a34 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -30,9 +30,13 @@ import ( "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/algorand/go-codec/codec" + "github.com/algorand/go-algorand/agreement" + "github.com/algorand/go-algorand/catchup" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklearray" "github.com/algorand/go-algorand/crypto/merklesignature" @@ -51,7 +55,6 @@ import ( "github.com/algorand/go-algorand/stateproof" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/execpool" - "github.com/algorand/go-codec/codec" ) const stateProofIntervalForHandlerTests = uint64(256) @@ -126,6 +129,100 @@ func TestGetBlock(t *testing.T) { getBlockTest(t, 0, "bad format", 400) } +func TestGetLedgerStateDelta(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + a := require.New(t) + + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + insertRounds(a, handler, 3) + + err := handler.GetLedgerStateDelta(c, 2) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + + actualResponse := model.LedgerStateDelta{} + expectedResponse := poolDeltaResponseGolden + (*expectedResponse.Accts.Accounts)[0].AccountData.Round = 2 + err = protocol.DecodeJSON(rec.Body.Bytes(), &actualResponse) + require.NoError(t, err) + require.Equal(t, poolDeltaResponseGolden.Accts, actualResponse.Accts) + require.Equal(t, poolDeltaResponseGolden.KvMods, actualResponse.KvMods) + require.Equal(t, poolDeltaResponseGolden.ModifiedAssets, actualResponse.ModifiedAssets) + require.Equal(t, poolDeltaResponseGolden.ModifiedApps, actualResponse.ModifiedApps) + require.Equal(t, poolDeltaResponseGolden.TxLeases, actualResponse.TxLeases) + require.Equal(t, poolDeltaResponseGolden.Totals, actualResponse.Totals) +} + +func TestSyncRound(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + numAccounts := 1 + numTransactions := 1 + offlineAccounts := true + mockLedger, _, _, _, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + mockNode := makeMockNode(mockLedger, t.Name(), nil) + 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) + + defer releasefunc() + + // TestSetSyncRound 200 + mockCall := mockNode.On("SetSyncRound", mock.Anything).Return(nil) + err := handler.SetSyncRound(c, 0) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + mockCall.Unset() + c, rec = newReq(t) + // TestSetSyncRound 400 SyncRoundInvalid + mockCall = mockNode.On("SetSyncRound", mock.Anything).Return(catchup.ErrSyncRoundInvalid) + err = handler.SetSyncRound(c, 0) + require.NoError(t, err) + require.Equal(t, 400, rec.Code) + mockCall.Unset() + c, rec = newReq(t) + // TestSetSyncRound 500 InternalError + mockCall = mockNode.On("SetSyncRound", mock.Anything).Return(fmt.Errorf("unknown error")) + err = handler.SetSyncRound(c, 0) + require.NoError(t, err) + require.Equal(t, 500, rec.Code) + c, rec = newReq(t) + + // TestGetSyncRound 200 + mockCall = mockNode.On("GetSyncRound").Return(2) + err = handler.GetSyncRound(c) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + mockCall.Unset() + c, rec = newReq(t) + // TestGetSyncRound 404 NotFound + mockCall = mockNode.On("GetSyncRound").Return(0) + err = handler.GetSyncRound(c) + require.NoError(t, err) + require.Equal(t, 404, rec.Code) + c, rec = newReq(t) + + // TestUnsetSyncRound 200 + mockCall = mockNode.On("UnsetSyncRound").Return() + err = handler.UnsetSyncRound(c) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + mockCall.Unset() + c, rec = newReq(t) + + mock.AssertExpectationsForObjects(t, mockNode) +} + func addBlockHelper(t *testing.T) (v2.Handlers, echo.Context, *httptest.ResponseRecorder, transactions.SignedTxn, func()) { handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 65c84ec71b..f18ebf86b0 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -22,6 +22,8 @@ import ( "strconv" "testing" + "github.com/stretchr/testify/mock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -80,11 +82,40 @@ var poolAddrResponseGolden = model.AccountResponse{ MinBalance: 100000, } var txnPoolGolden = make([]transactions.SignedTxn, 2) +var poolDeltaResponseGolden = model.LedgerStateDelta{ + Accts: &model.AccountDeltas{ + Accounts: &[]model.AccountBalanceRecord{ + { + AccountData: model.Account{ + Address: poolAddr.String(), + Amount: 50000000000, + AmountWithoutPendingRewards: 50000000000, + MinBalance: 100000, + CreatedApps: &[]model.Application{}, + AppsTotalSchema: &appsTotalSchema, + AppsLocalState: &[]model.ApplicationLocalState{}, + Status: "Not Participating", + RewardBase: &poolAddrRewardBaseGolden, + CreatedAssets: &[]model.Asset{}, + Assets: &[]model.AssetHolding{}, + }, + Address: poolAddr.String(), + }, + }, + }, + Totals: &model.AccountTotals{ + NotParticipating: 100000000000, + Offline: 0, + Online: 658511, + RewardsLevel: 0, + }, +} // 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 { + mock.Mock ledger v2.LedgerForAPI genesisID string config config.Local @@ -94,22 +125,35 @@ type mockNode struct { usertxns map[basics.Address][]node.TxnWithStatus } -func (m mockNode) InstallParticipationKey(partKeyBinary []byte) (account.ParticipationID, error) { +func (m *mockNode) InstallParticipationKey(partKeyBinary []byte) (account.ParticipationID, error) { panic("implement me") } -func (m mockNode) ListParticipationKeys() ([]account.ParticipationRecord, error) { +func (m *mockNode) ListParticipationKeys() ([]account.ParticipationRecord, error) { panic("implement me") } -func (m mockNode) GetParticipationKey(id account.ParticipationID) (account.ParticipationRecord, error) { +func (m *mockNode) GetParticipationKey(id account.ParticipationID) (account.ParticipationRecord, error) { panic("implement me") } -func (m mockNode) RemoveParticipationKey(id account.ParticipationID) error { +func (m *mockNode) RemoveParticipationKey(id account.ParticipationID) error { panic("implement me") } +func (m *mockNode) SetSyncRound(rnd uint64) error { + args := m.Called(rnd) + return args.Error(0) +} + +func (m *mockNode) UnsetSyncRound() { +} + +func (m *mockNode) GetSyncRound() uint64 { + args := m.Called() + return uint64(args.Int(0)) +} + func (m *mockNode) AppendParticipationKeys(id account.ParticipationID, keys account.StateProofKeys) error { m.id = id m.keys = keys @@ -126,53 +170,53 @@ func makeMockNode(ledger v2.LedgerForAPI, genesisID string, nodeError error) *mo } } -func (m mockNode) LedgerForAPI() v2.LedgerForAPI { +func (m *mockNode) LedgerForAPI() v2.LedgerForAPI { return m.ledger } -func (m mockNode) Status() (s node.StatusReport, err error) { +func (m *mockNode) Status() (s node.StatusReport, err error) { s = cannedStatusReportGolden return } -func (m mockNode) GenesisID() string { +func (m *mockNode) GenesisID() string { return m.genesisID } -func (m mockNode) GenesisHash() crypto.Digest { +func (m *mockNode) GenesisHash() crypto.Digest { return m.ledger.(*data.Ledger).GenesisHash() } -func (m mockNode) BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error { +func (m *mockNode) BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error { return m.err } -func (m mockNode) GetPendingTransaction(txID transactions.Txid) (res node.TxnWithStatus, found bool) { +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) { +func (m *mockNode) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) { return txnPoolGolden, m.err } -func (m mockNode) SuggestedFee() basics.MicroAlgos { +func (m *mockNode) SuggestedFee() basics.MicroAlgos { return basics.MicroAlgos{Raw: 1} } // unused by handlers: -func (m mockNode) Config() config.Local { +func (m *mockNode) Config() config.Local { return m.config } -func (m mockNode) Start() {} +func (m *mockNode) Start() {} -func (m mockNode) ListeningAddress() (string, bool) { +func (m *mockNode) ListeningAddress() (string, bool) { return "mock listening addresses not implemented", false } -func (m mockNode) Stop() {} +func (m *mockNode) Stop() {} -func (m mockNode) ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]node.TxnWithStatus, error) { +func (m *mockNode) ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]node.TxnWithStatus, error) { txns, ok := m.usertxns[addr] if !ok { return nil, fmt.Errorf("no txns for %s", addr) @@ -181,41 +225,41 @@ func (m mockNode) ListTxns(addr basics.Address, minRound basics.Round, maxRound return txns, nil } -func (m mockNode) GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (node.TxnWithStatus, bool) { +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 { +func (m *mockNode) PoolStats() node.PoolStats { return node.PoolStats{} } -func (m mockNode) IsArchival() bool { +func (m *mockNode) IsArchival() bool { return false } -func (m mockNode) OnNewBlock(block bookkeeping.Block, delta ledgercore.StateDelta) {} +func (m *mockNode) OnNewBlock(block bookkeeping.Block, delta ledgercore.StateDelta) {} -func (m mockNode) Uint64() uint64 { +func (m *mockNode) Uint64() uint64 { return 1 } -func (m mockNode) Indexer() (*indexer.Indexer, error) { +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) { +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) (agreement.ValidatedBlock, error) { +func (m *mockNode) AssembleBlock(round basics.Round) (agreement.ValidatedBlock, error) { return nil, fmt.Errorf("assemble block not implemented") } -func (m mockNode) StartCatchup(catchpoint string) error { +func (m *mockNode) StartCatchup(catchpoint string) error { return m.err } -func (m mockNode) AbortCatchup(catchpoint string) error { +func (m *mockNode) AbortCatchup(catchpoint string) error { return m.err } diff --git a/daemon/algod/api/spec/v1/model.go b/daemon/algod/api/spec/v1/model.go deleted file mode 100644 index 9ae6fe23ce..0000000000 --- a/daemon/algod/api/spec/v1/model.go +++ /dev/null @@ -1,1019 +0,0 @@ -// Copyright (C) 2019-2022 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 v1 defines models exposed by algod rest api -package v1 - -// NodeStatus contains the information about a node status -// swagger:model NodeStatus -type NodeStatus struct { - // LastRound indicates the last round seen - // - // required: true - LastRound uint64 `json:"lastRound"` - - // LastVersion indicates the last consensus version supported - // - // required: true - LastVersion string `json:"lastConsensusVersion"` - - // NextVersion of consensus protocol to use - // - // required: true - NextVersion string `json:"nextConsensusVersion"` - - // NextVersionRound is the round at which the next consensus version will apply - // - // required: true - NextVersionRound uint64 `json:"nextConsensusVersionRound"` - - // NextVersionSupported indicates whether the next consensus version is supported by this node - // - // required: true - NextVersionSupported bool `json:"nextConsensusVersionSupported"` - - // TimeSinceLastRound in nanoseconds - // - // required: true - TimeSinceLastRound int64 `json:"timeSinceLastRound"` - - // CatchupTime in nanoseconds - // - // required: true - CatchupTime int64 `json:"catchupTime"` - - // HasSyncedSinceStartup indicates whether a round has completed since startup - // Required: true - HasSyncedSinceStartup bool `json:"hasSyncedSinceStartup"` - - // StoppedAtUnsupportedRound indicates that the node does not support the new rounds and has stopped making progress - // - // Required: true - StoppedAtUnsupportedRound bool `json:"stoppedAtUnsupportedRound"` -} - -// TransactionID Description -// swagger:model transactionID -type TransactionID struct { - // TxId is the string encoding of the transaction hash - // - // required: true - TxID string `json:"txId"` -} - -// Participation Description -// swagger:model Participation -type Participation struct { // Round and Address fields are redundant if Participation embedded in Account. Exclude for now. - // ParticipationPK is the root participation public key (if any) currently registered for this round - // - // required: true - // swagger:strfmt byte - ParticipationPK []byte `json:"partpkb64"` - - // VRFPK is the selection public key (if any) currently registered for this round - // - // required: true - // swagger:strfmt byte - VRFPK []byte `json:"vrfpkb64"` - - // VoteFirst is the first round for which this participation is valid. - // - // required: true - VoteFirst uint64 `json:"votefst"` - - // VoteLast is the last round for which this participation is valid. - // - // required: true - VoteLast uint64 `json:"votelst"` - - // VoteKeyDilution is the number of subkeys in for each batch of participation keys. - // - // required: true - 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 { - // Round indicates the round for which this information is relevant - // - // required: true - Round uint64 `json:"round"` - - // Address indicates the account public key - // - // required: true - Address string `json:"address"` - - // Amount indicates the total number of MicroAlgos in the account - // - // required: true - Amount uint64 `json:"amount"` - - // PendingRewards specifies the amount of MicroAlgos of pending - // rewards in this account. - // - // required: true - PendingRewards uint64 `json:"pendingrewards"` - - // AmountWithoutPendingRewards specifies the amount of MicroAlgos in - // the account, without the pending rewards. - // - // required: true - AmountWithoutPendingRewards uint64 `json:"amountwithoutpendingrewards"` - - // Rewards indicates the total rewards of MicroAlgos the account has received, including pending rewards. - // - // required: true - Rewards uint64 `json:"rewards"` - - // Status indicates the delegation status of the account's MicroAlgos - // Offline - indicates that the associated account is delegated. - // Online - indicates that the associated account used as part of the delegation pool. - // NotParticipating - indicates that the associated account is neither a delegator nor a delegate. - // - // required: true - Status string `json:"status"` - - // Participation is the participation information currently associated with the account, if any. - // This field is optional and may not be set even if participation information is registered. - // In future REST API versions, this field may become required. - // - // required: false - Participation *Participation `json:"participation,omitempty"` - - // AssetParams specifies the parameters of assets created by this account. - // - // required: false - AssetParams map[uint64]AssetParams `json:"thisassettotal,omitempty"` - - // Assets specifies the holdings of assets by this account, - // indexed by the asset ID. - // - // 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 - // - // required: true - AssetIndex uint64 - - // AssetParams specifies the parameters of asset referred to by AssetIndex - // - // required: true - AssetParams AssetParams -} - -// AssetParams specifies the parameters for an asset. -// swagger:model AssetParams -type AssetParams struct { - // Creator specifies the address that created this asset. - // This is the address where the parameters for this asset - // can be found, and also the address where unwanted asset - // units can be sent in the worst case. - // - // required: true - Creator string `json:"creator"` - - // Total specifies the total number of units of this asset. - // - // required: true - Total uint64 `json:"total"` - - // Decimals specifies the number of digits to use after the decimal - // point when displaying this asset. If 0, the asset is not divisible. - // If 1, the base unit of the asset is in tenths. If 2, the base unit - // of the asset is in hundredths, and so on. - // - // required: true - Decimals uint32 `json:"decimals"` - - // DefaultFrozen specifies whether holdings in this asset - // are frozen by default. - // - // required: false - DefaultFrozen bool `json:"defaultfrozen"` - - // UnitName specifies the name of a unit of this asset, - // as supplied by the creator. - // - // required: false - UnitName string `json:"unitname,omitempty"` - - // AssetName specifies the name of this asset, - // as supplied by the creator. - // - // required: false - AssetName string `json:"assetname,omitempty"` - - // URL specifies a URL where more information about the asset can be - // retrieved - // - // required: false - URL string `json:"url,omitempty"` - - // MetadataHash specifies a commitment to some unspecified asset - // metadata. The format of this metadata is up to the application. - // - // required: false - // swagger:strfmt byte - MetadataHash []byte `json:"metadatahash,omitempty"` - - // ManagerAddr specifies the address used to manage the keys of this - // asset and to destroy it. - // - // required: false - ManagerAddr string `json:"managerkey"` - - // ReserveAddr specifies the address holding reserve (non-minted) - // units of this asset. - // - // required: false - ReserveAddr string `json:"reserveaddr"` - - // FreezeAddr specifies the address used to freeze holdings of - // this asset. If empty, freezing is not permitted. - // - // required: false - FreezeAddr string `json:"freezeaddr"` - - // ClawbackAddr specifies the address used to clawback holdings of - // this asset. If empty, clawback is not permitted. - // - // required: false - ClawbackAddr string `json:"clawbackaddr"` -} - -// AssetHolding specifies the holdings of a particular asset. -// swagger:model AssetHolding -type AssetHolding struct { - // Creator specifies the address that created this asset. - // This is the address where the parameters for this asset - // can be found, and also the address where unwanted asset - // units can be sent in the worst case. - // - // required: true - Creator string `json:"creator"` - - // Amount specifies the number of units held. - // - // required: true - Amount uint64 `json:"amount"` - - // Frozen specifies whether this holding is frozen. - // - // required: false - Frozen bool `json:"frozen"` -} - -// Transaction contains all fields common to all transactions and serves as an envelope to all transactions -// type -// swagger:model Transaction -type Transaction struct { - // Type is the transaction type - // - // required: true - Type string `json:"type"` - - // TxID is the transaction ID - // - // required: true - TxID string `json:"tx"` - - // From is the sender's address - // - // required: true - From string `json:"from"` - - // Fee is the transaction fee - // - // required: true - Fee uint64 `json:"fee"` - - // FirstRound indicates the first valid round for this transaction - // - // required: true - FirstRound uint64 `json:"first-round"` - - // LastRound indicates the last valid round for this transaction - // - // required: true - LastRound uint64 `json:"last-round"` - - // Note is a free form data - // - // required: false - // swagger:strfmt byte - Note []byte `json:"noteb64,omitempty"` - - // Lease enforces mutual exclusion of transactions. If this field is - // nonzero, then once the transaction is confirmed, it acquires the - // lease identified by the (Sender, Lease) pair of the transaction until - // the LastValid round passes. While this transaction possesses the - // lease, no other transaction specifying this lease can be confirmed. - // - // required: false - // swagger:strfmt byte - Lease []byte `json:"lease,omitempty"` - - // ConfirmedRound indicates the block number this transaction appeared in - // - // required: false - ConfirmedRound uint64 `json:"round"` - - // TransactionResults contains information about the side effects of a transaction - // - // required: false - TransactionResults *TransactionResults `json:"txresults,omitempty"` - - // PoolError indicates the transaction was evicted from this node's transaction - // pool (if non-empty). A non-empty PoolError does not guarantee that the - // transaction will never be committed; other nodes may not have evicted the - // transaction and may attempt to commit it in the future. - // - // required: false - PoolError string `json:"poolerror,omitempty"` - - // This is a list of all supported transactions. - // To add another one, create a struct with XXXTransactionType and embed it here. - // To prevent extraneous fields, all must have the "omitempty" tag. - - // Payment contains the additional fields for a payment transaction. - // - // required: false - Payment *PaymentTransactionType `json:"payment,omitempty"` - - // Keyreg contains the additional fields for a keyreg transaction. - // - // required: false - Keyreg *KeyregTransactionType `json:"keyreg,omitempty"` - - // AssetConfig contains the additional fields for an asset config transaction. - // - // required: false - AssetConfig *AssetConfigTransactionType `json:"curcfg,omitempty"` - - // AssetTransfer contains the additional fields for an asset transfer transaction. - // - // required: false - AssetTransfer *AssetTransferTransactionType `json:"curxfer,omitempty"` - - // AssetFreeze contains the additional fields for an asset freeze transaction. - // - // required: false - AssetFreeze *AssetFreezeTransactionType `json:"curfrz,omitempty"` - - // ApplicationCall - // - // required: true - ApplicationCall *ApplicationCallTransactionType `json:"app,omitempty"` - - // StateProof - // - // required: true - StateProof *StateProofTransactionType `json:"sp,omitempty"` - - // FromRewards is the amount of pending rewards applied to the From - // account as part of this transaction. - // - // required: false - FromRewards uint64 `json:"fromrewards"` - - // Genesis ID - // - // required: true - GenesisID string `json:"genesisID"` - - // Genesis hash - // - // required: true - // swagger:strfmt byte - GenesisHash []byte `json:"genesishashb64"` - - // Group - // - // required: false - // swagger:strfmt byte - Group []byte `json:"group,omitempty"` -} - -// PaymentTransactionType contains the additional fields for a payment Transaction -// swagger:model PaymentTransactionType -type PaymentTransactionType struct { - // To is the receiver's address - // - // required: true - To string `json:"to"` - - // CloseRemainderTo is the address the sender closed to - // - // required: false - CloseRemainderTo string `json:"close,omitempty"` - - // CloseAmount is the amount sent to CloseRemainderTo, for committed transaction - // - // required: false - CloseAmount uint64 `json:"closeamount,omitempty"` - - // Amount is the amount of MicroAlgos intended to be transferred - // - // required: true - Amount uint64 `json:"amount"` - - // ToRewards is the amount of pending rewards applied to the To account - // as part of this transaction. - // - // required: false - ToRewards uint64 `json:"torewards"` - - // CloseRewards is the amount of pending rewards applied to the CloseRemainderTo - // account as part of this transaction. - // - // required: false - CloseRewards uint64 `json:"closerewards"` -} - -// KeyregTransactionType contains the additional fields for a keyreg Transaction -// swagger:model KeyregTransactionType -type KeyregTransactionType struct { - // VotePK is the participation public key used in key registration transactions - // - // required: false - // swagger:strfmt byte - VotePK []byte `json:"votekey"` - - // SelectionPK is the VRF public key used in key registration transactions - // - // required: false - // swagger:strfmt byte - SelectionPK []byte `json:"selkey"` - - // VoteFirst is the first round this participation key is valid - // - // required: false - VoteFirst uint64 `json:"votefst"` - - // VoteLast is the last round this participation key is valid - // - // required: false - VoteLast uint64 `json:"votelst"` - - // VoteKeyDilution is the dilution for the 2-level participation key - // - // required: false - VoteKeyDilution uint64 `json:"votekd"` -} - -// TransactionResults contains information about the side effects of a transaction -// swagger:model TransactionResults -type TransactionResults struct { - // CreatedAssetIndex indicates the asset index of an asset created by this txn - // - // 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 -// swagger:model AssetConfigTransactionType -type AssetConfigTransactionType struct { - // AssetID is the asset being configured (or empty if creating) - // - // required: false - AssetID uint64 `json:"id"` - - // Params specifies the new asset parameters (or empty if deleting) - // - // required: false - Params AssetParams `json:"params"` -} - -// AssetTransferTransactionType contains the additional fields for an asset transfer transaction -// swagger:model AssetTransferTransactionType -type AssetTransferTransactionType struct { - // AssetID is the asset being configured (or empty if creating) - // - // required: true - AssetID uint64 `json:"id"` - - // Amount is the amount being transferred. - // - // required: true - Amount uint64 `json:"amt"` - - // Sender is the source account (if using clawback). - // - // required: false - Sender string `json:"snd"` - - // Receiver is the recipient account. - // - // required: true - Receiver string `json:"rcv"` - - // CloseTo is the destination for remaining funds (if closing). - // - // required: false - CloseTo string `json:"closeto"` - - // CloseToAmount is amount of the remaining funds that were transferred to the close to address (if closing). - // - // required: false - CloseToAmount uint64 `json:"closetoamount,omitempty"` -} - -// AssetFreezeTransactionType contains the additional fields for an asset freeze transaction -// swagger:model AssetFreezeTransactionType -type AssetFreezeTransactionType struct { - // AssetID is the asset being configured (or empty if creating) - // - // required: true - AssetID uint64 `json:"id"` - - // Account specifies the account where the asset is being frozen or thawed. - // - // required: true - Account string `json:"acct"` - - // NewFreezeStatus specifies the new freeze status. - // - // required: true - 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"` - - // 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 - // - // 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"` -} - -// StateProofTransactionType contains the additional fields for a state proof transaction -// swagger:model StateProofTransactionType -type StateProofTransactionType struct { - // StateProof is the msgpack encoding of the state proof. - // - // required: true - // swagger:strfmt byte - StateProof []byte `json:"sp"` - - // StateProofMessage is the msgpack encoding of the state proof message. - // - // required: true - // swagger:strfmt byte - StateProofMessage []byte `json:"spmsg"` -} - -// TransactionList contains a list of transactions -// swagger:model TransactionList -type TransactionList struct { - // TransactionList is a list of transactions - // - // required: true - Transactions []Transaction `json:"transactions,omitempty"` -} - -// AssetList contains a list of assets -// swagger:model AssetList -type AssetList struct { - // Assets is a list of assets - // - // required: true - Assets []Asset `json:"assets,omitempty"` -} - -// TransactionFee contains the suggested fee -// swagger:model TransactionFee -type TransactionFee struct { - // Fee is transaction fee - // Fee is in units of micro-Algos per byte. - // Fee may fall to zero but transactions must still have a fee of - // at least MinTxnFee for the current network protocol. - // - // required: true - Fee uint64 `json:"fee"` -} - -// TransactionParams contains the parameters that help a client construct -// a new transaction. -// swagger:model TransactionParams -type TransactionParams struct { - // Fee is the suggested transaction fee - // Fee is in units of micro-Algos per byte. - // Fee may fall to zero but transactions must still have a fee of - // at least MinTxnFee for the current network protocol. - // - // required: true - Fee uint64 `json:"fee"` - - // Genesis ID - // - // required: true - GenesisID string `json:"genesisID"` - - // Genesis hash - // - // required: true - // swagger:strfmt byte - GenesisHash []byte `json:"genesishashb64"` - - // LastRound indicates the last round seen - // - // required: true - LastRound uint64 `json:"lastRound"` - - // ConsensusVersion indicates the consensus protocol version - // as of LastRound. - // - // required: true - ConsensusVersion string `json:"consensusVersion"` - - // The minimum transaction fee (not per byte) required for the - // txn to validate for the current network protocol. - // - // required: false - MinTxnFee uint64 `json:"minFee"` -} - -// RawResponse is fulfilled by responses that should not be decoded as msgpack -type RawResponse interface { - SetBytes([]byte) -} - -// RawBlock represents an encoded msgpack block -// swagger:model RawBlock -// swagger:strfmt byte -type RawBlock []byte - -// SetBytes fulfills the RawResponse interface on RawBlock -func (rb *RawBlock) SetBytes(b []byte) { - *rb = b -} - -// Block contains a block information -// swagger:model Block -type Block struct { - // Hash is the current block hash - // - // required: true - Hash string `json:"hash"` - - // PreviousBlockHash is the previous block hash - // - // required: true - PreviousBlockHash string `json:"previousBlockHash"` - - // Seed is the sortition seed - // - // required: true - Seed string `json:"seed"` - - // Proposer is the address of this block proposer - // - // required: true - Proposer string `json:"proposer"` - - // Round is the current round on which this block was appended to the chain - // - // required: true - Round uint64 `json:"round"` - - // Period is the period on which the block was confirmed - // - // required: true - Period uint64 `json:"period"` - - // TransactionsRoot authenticates the set of transactions appearing in the block. - // More specifically, it's the root of a merkle tree whose leaves are the block's Txids, in lexicographic order. - // For the empty block, it's 0. - // Note that the TxnCommitments does not authenticate the signatures on the transactions, only the transactions themselves. - // Two blocks with the same transactions but in a different order and with different signatures will have the same TxnCommitments. - // - // required: true - TransactionsRoot string `json:"txnRoot"` - - // RewardsLevel specifies how many rewards, in MicroAlgos, - // have been distributed to each config.Protocol.RewardUnit - // of MicroAlgos since genesis. - RewardsLevel uint64 `json:"reward"` - - // The number of new MicroAlgos added to the participation stake from rewards at the next round. - RewardsRate uint64 `json:"rate"` - - // The number of leftover MicroAlgos after the distribution of RewardsRate/rewardUnits - // MicroAlgos for every reward unit in the next round. - RewardsResidue uint64 `json:"frac"` - - // Transactions is the list of transactions in this block - Transactions TransactionList `json:"txns"` - - // TimeStamp in seconds since epoch - // - // required: true - Timestamp int64 `json:"timestamp"` - - UpgradeState - UpgradeVote -} - -// UpgradeState contains the information about a current state of an upgrade -// swagger:model UpgradeState -type UpgradeState struct { - // CurrentProtocol is a string that represents the current protocol - // - // required: true - CurrentProtocol string `json:"currentProtocol"` - - // NextProtocol is a string that represents the next proposed protocol - // - // required: true - NextProtocol string `json:"nextProtocol"` - - // NextProtocolApprovals is the number of blocks which approved the protocol upgrade - // - // required: true - NextProtocolApprovals uint64 `json:"nextProtocolApprovals"` - - // NextProtocolVoteBefore is the deadline round for this protocol upgrade (No votes will be consider after this round) - // - // required: true - NextProtocolVoteBefore uint64 `json:"nextProtocolVoteBefore"` - - // NextProtocolSwitchOn is the round on which the protocol upgrade will take effect - // - // required: true - NextProtocolSwitchOn uint64 `json:"nextProtocolSwitchOn"` -} - -// UpgradeVote represents the vote of the block proposer with respect to protocol upgrades. -// swagger:model UpgradeVote -type UpgradeVote struct { - // UpgradePropose indicates a proposed upgrade - // - // required: true - UpgradePropose string `json:"upgradePropose"` - - // UpgradeApprove indicates a yes vote for the current proposal - // - // required: true - UpgradeApprove bool `json:"upgradeApprove"` -} - -// Supply represents the current supply of MicroAlgos in the system -// swagger:model Supply -type Supply struct { - // Round - // - // required: true - Round uint64 `json:"round"` - - // TotalMoney - // - // required: true - TotalMoney uint64 `json:"totalMoney"` - - // OnlineMoney - // - // required: true - OnlineMoney uint64 `json:"onlineMoney"` -} - -// PendingTransactions represents a potentially truncated list of transactions currently in the -// node's transaction pool. -// swagger:model PendingTransactions -type PendingTransactions struct { - // TruncatedTxns - // required: true - TruncatedTxns TransactionList `json:"truncatedTxns"` - // TotalTxns - // required: true - TotalTxns uint64 `json:"totalTxns"` -} diff --git a/daemon/algod/api/swagger.json b/daemon/algod/api/swagger.json deleted file mode 100644 index 0455702221..0000000000 --- a/daemon/algod/api/swagger.json +++ /dev/null @@ -1,2057 +0,0 @@ -{ - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "swagger": "2.0", - "info": { - "description": "API Endpoint for AlgoD Operations.", - "title": "Algod REST API.", - "contact": { - "email": "contact@algorand.com" - }, - "version": "0.0.1" - }, - "host": "localhost", - "basePath": "/", - "paths": { - "/genesis": { - "get": { - "description": "Returns the entire genesis file in json.", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Gets the genesis information", - "operationId": "GenesisJSON", - "responses": { - "200": { - "description": "The current genesis information", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/health": { - "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Returns OK if healthy.", - "operationId": "HealthCheck", - "responses": { - "200": { - "description": "OK." - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/metrics": { - "get": { - "produces": [ - "text/plain" - ], - "schemes": [ - "http" - ], - "summary": "Return metrics about algod functioning.", - "operationId": "Metrics", - "responses": { - "200": { - "description": "text with \\#-comments and key:value lines" - }, - "404": { - "description": "metrics were compiled out" - } - } - } - }, - "/swagger.json": { - "get": { - "description": "Returns the entire swagger spec in json.", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Gets the current swagger spec.", - "operationId": "SwaggerJSON", - "responses": { - "200": { - "description": "The current swagger spec", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/account/{address}": { - "get": { - "description": "Given a specific account public key, this call returns the accounts status, balance and spendable amounts", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get account information.", - "operationId": "AccountInformation", - "parameters": [ - { - "pattern": "[A-Z0-9]{58}", - "type": "string", - "description": "An account public key", - "name": "address", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "$ref": "#/responses/AccountInformationResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/account/{address}/transaction/{txid}": { - "get": { - "description": "Given a wallet address and a transaction id, it returns the confirmed transaction information. This call scans up to \u003cCurrentProtocol\u003e.MaxTxnLife blocks in the past.\n", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get a specific confirmed transaction.", - "operationId": "TransactionInformation", - "parameters": [ - { - "pattern": "[A-Z0-9]{58}", - "type": "string", - "description": "An account public key", - "name": "address", - "in": "path", - "required": true - }, - { - "pattern": "[A-Z0-9]+", - "type": "string", - "description": "A transaction id", - "name": "txid", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "$ref": "#/responses/TransactionResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "404": { - "description": "Transaction Not Found", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/account/{address}/transactions": { - "get": { - "description": "Returns the list of confirmed transactions between within a date range. When indexer is disabled this call requires firstRound and lastRound and returns an error if firstRound is not available to the node. The transaction results start from the oldest round.", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get a list of confirmed transactions.", - "operationId": "Transactions", - "parameters": [ - { - "pattern": "[A-Z0-9]{58}", - "type": "string", - "description": "An account public key", - "name": "address", - "in": "path", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "format": "int64", - "description": "Do not fetch any transactions before this round.", - "name": "firstRound", - "in": "query" - }, - { - "minimum": 0, - "type": "integer", - "format": "int64", - "description": "Do not fetch any transactions after this round.", - "name": "lastRound", - "in": "query" - }, - { - "type": "string", - "format": "date", - "description": "Do not fetch any transactions before this date. (enabled only with indexer)", - "name": "fromDate", - "in": "query" - }, - { - "type": "string", - "format": "date", - "description": "Do not fetch any transactions after this date. (enabled only with indexer)", - "name": "toDate", - "in": "query" - }, - { - "type": "integer", - "format": "int64", - "description": "maximum transactions to show (default to 100)", - "name": "max", - "in": "query" - } - ], - "responses": { - "200": { - "$ref": "#/responses/TransactionsResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/account/{addr}/transactions/pending": { - "get": { - "description": "Get the list of pending transactions by address, sorted by priority, in decreasing order, truncated at the end at MAX. If MAX = 0, returns all pending transactions.\n", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get a list of unconfirmed transactions currently in the transaction pool by address.", - "operationId": "GetPendingTransactionsByAddress", - "parameters": [ - { - "pattern": "[A-Z0-9]{58}", - "type": "string", - "description": "An account public key", - "name": "addr", - "in": "path", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "format": "int64", - "description": "Truncated number of transactions to display. If max=0, returns all pending txns.", - "name": "max", - "in": "query" - } - ], - "responses": { - "200": { - "$ref": "#/responses/PendingTransactionsResponse" - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/asset/{index}": { - "get": { - "description": "Given the asset's unique index, this call returns the asset's creator, manager, reserve, freeze, and clawback addresses\n", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get asset information.", - "operationId": "AssetInformation", - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "Asset index", - "name": "index", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "$ref": "#/responses/AssetInformationResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/assets": { - "get": { - "description": "Returns list of up to `max` assets, where the maximum assetIdx is \u003c= `assetIdx`", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "List assets", - "operationId": "Assets", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "format": "int64", - "description": "Fetch assets with asset index \u003c= assetIdx. If zero, fetch most recent assets.", - "name": "assetIdx", - "in": "query" - }, - { - "maximum": 100, - "minimum": 0, - "type": "integer", - "format": "int64", - "description": "Fetch no more than this many assets", - "name": "max", - "in": "query" - } - ], - "responses": { - "200": { - "$ref": "#/responses/AssetsResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/block/{round}": { - "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get the block for the given round.", - "operationId": "GetBlock", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "format": "int64", - "description": "The round from which to fetch block information.", - "name": "round", - "in": "path", - "required": true - }, - { - "type": "integer", - "format": "int64", - "description": "Return raw msgpack block bytes", - "name": "raw", - "in": "query" - } - ], - "responses": { - "200": { - "$ref": "#/responses/BlockResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/ledger/supply": { - "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get the current supply reported by the ledger.", - "operationId": "GetSupply", - "responses": { - "200": { - "$ref": "#/responses/SupplyResponse" - }, - "401": { - "description": "Invalid API Token" - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/status": { - "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Gets the current node status.", - "operationId": "GetStatus", - "responses": { - "200": { - "$ref": "#/responses/StatusResponse" - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/status/wait-for-block-after/{round}/": { - "get": { - "description": "Waits for a block to appear after round {round} and returns the node's status at the time.", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Gets the node status after waiting for the given round.", - "operationId": "WaitForBlock", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "format": "int64", - "description": "The round to wait until returning status", - "name": "round", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "$ref": "#/responses/StatusResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "503": { - "description": "Service", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/transaction/{txid}": { - "get": { - "description": "Returns the transaction information of the given txid. Works only if the indexer is enabled.", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get an information of a single transaction.", - "operationId": "Transaction", - "parameters": [ - { - "pattern": "[A-Z0-9]+", - "type": "string", - "description": "A transaction id", - "name": "txid", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "$ref": "#/responses/TransactionResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "404": { - "description": "Transaction Not Found", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/transactions": { - "post": { - "consumes": [ - "application/x-binary" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Broadcasts a raw transaction to the network.", - "operationId": "RawTransaction", - "parameters": [ - { - "description": "The byte encoded signed transaction to broadcast to network", - "name": "rawtxn", - "in": "body", - "required": true, - "schema": { - "type": "string", - "format": "binary" - } - } - ], - "responses": { - "200": { - "$ref": "#/responses/TransactionIDResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/transactions/fee": { - "get": { - "description": "Suggested Fee is returned in units of micro-Algos per byte. Suggested Fee may fall to zero but submitted transactions must still have a fee of at least MinTxnFee for the current network protocol.\n", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get the suggested fee", - "operationId": "SuggestedFee", - "responses": { - "200": { - "$ref": "#/responses/TransactionFeeResponse" - }, - "401": { - "description": "Invalid API Token" - }, - "503": { - "description": "Service Unavailable", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/transactions/params": { - "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get parameters for constructing a new transaction", - "operationId": "TransactionParams", - "responses": { - "200": { - "$ref": "#/responses/TransactionParamsResponse" - }, - "401": { - "description": "Invalid API Token" - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/transactions/pending": { - "get": { - "description": "Get the list of pending transactions, sorted by priority, in decreasing order, truncated at the end at MAX. If MAX = 0, returns all pending transactions.\n", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get a list of unconfirmed transactions currently in the transaction pool.", - "operationId": "GetPendingTransactions", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "format": "int64", - "description": "Truncated number of transactions to display. If max=0, returns all pending txns.", - "name": "max", - "in": "query" - } - ], - "responses": { - "200": { - "$ref": "#/responses/PendingTransactionsResponse" - }, - "401": { - "description": "Invalid API Token" - }, - "500": { - "description": "Internal Error", - "schema": { - "type": "string" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/v1/transactions/pending/{txid}": { - "get": { - "description": "Given a transaction id of a recently submitted transaction, it returns information about it. There are several cases when this might succeed:\n- transaction committed (committed round \u003e 0) - transaction still in the pool (committed round = 0, pool error = \"\") - transaction removed from pool due to error (committed round = 0, pool error != \"\")\nOr the transaction may have happened sufficiently long ago that the node no longer remembers it, and this will return an error.\n", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "summary": "Get a specific pending transaction.", - "operationId": "PendingTransactionInformation", - "parameters": [ - { - "pattern": "[A-Z0-9]+", - "type": "string", - "description": "A transaction id", - "name": "txid", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "$ref": "#/responses/TransactionResponse" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Invalid API Token" - }, - "404": { - "description": "Transaction Not Found", - "schema": { - "type": "string" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "type": "string" - } - }, - "default": { - "description": "Unknown Error" - } - } - } - }, - "/versions": { - "get": { - "description": "Retrieves the current version", - "produces": [ - "application/json" - ], - "schemes": [ - "http" - ], - "operationId": "GetVersion", - "responses": { - "200": { - "$ref": "#/responses/VersionsResponse" - } - } - } - } - }, - "definitions": { - "Account": { - "description": "Account Description", - "type": "object", - "required": [ - "round", - "address", - "amount", - "pendingrewards", - "amountwithoutpendingrewards", - "rewards", - "status" - ], - "properties": { - "address": { - "description": "Address indicates the account public key", - "type": "string", - "x-go-name": "Address" - }, - "amount": { - "description": "Amount indicates the total number of MicroAlgos in the account", - "type": "integer", - "format": "uint64", - "x-go-name": "Amount" - }, - "amountwithoutpendingrewards": { - "description": "AmountWithoutPendingRewards specifies the amount of MicroAlgos in\nthe account, without the pending rewards.", - "type": "integer", - "format": "uint64", - "x-go-name": "AmountWithoutPendingRewards" - }, - "applocalstates": { - "description": "AppLocalStates is a map of local states for applications this\naccount has opted in to, as well as a copy of each application's\nLocalStateSchema", - "x-go-name": "AppLocalStates" - }, - "appparams": { - "description": "AppParams is a map of application parameters for applications that\nwere created by this account. These parameters include the\napplication's global state map", - "x-go-name": "AppParams" - }, - "assets": { - "description": "Assets specifies the holdings of assets by this account,\nindexed by the asset ID.", - "x-go-name": "Assets" - }, - "participation": { - "$ref": "#/definitions/Participation" - }, - "pendingrewards": { - "description": "PendingRewards specifies the amount of MicroAlgos of pending\nrewards in this account.", - "type": "integer", - "format": "uint64", - "x-go-name": "PendingRewards" - }, - "rewards": { - "description": "Rewards indicates the total rewards of MicroAlgos the account has received, including pending rewards.", - "type": "integer", - "format": "uint64", - "x-go-name": "Rewards" - }, - "round": { - "description": "Round indicates the round for which this information is relevant", - "type": "integer", - "format": "uint64", - "x-go-name": "Round" - }, - "status": { - "description": "Status indicates the delegation status of the account's MicroAlgos\nOffline - indicates that the associated account is delegated.\nOnline - indicates that the associated account used as part of the delegation pool.\nNotParticipating - indicates that the associated account is neither a delegator nor a delegate.", - "type": "string", - "x-go-name": "Status" - }, - "thisassettotal": { - "description": "AssetParams specifies the parameters of assets created by this account.", - "x-go-name": "AssetParams" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "ApplicationCallTransactionType": { - "description": "ApplicationCallTransactionType contains the additional fields for an ApplicationCall transaction", - "type": "object", - "required": [ - "id", - "accounts", - "foreignapps", - "foreignassets", - "appargs", - "approvprog", - "clearprog" - ], - "properties": { - "accounts": { - "description": "Accounts lists the accounts (in addition to the sender) that may be\naccessed from the application's ApprovalProgram and ClearStateProgram.", - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Accounts" - }, - "appargs": { - "description": "ApplicationArgs lists some transaction-specific arguments accessible\nfrom application logic", - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "ApplicationArgs" - }, - "approvprog": { - "description": "ApprovalProgram determines whether or not this ApplicationCall\ntransaction will be approved or not. It does not execute when\nOnCompletion == ClearStateOC, because clearing local state is always\nallowed.", - "type": "string", - "x-go-name": "ApprovalProgram" - }, - "clearprog": { - "description": "ClearStateProgram executes when an ApplicationCall transaction\nexecutes with OnCompletion == ClearStateOC. However, this program\nmay not reject the transaction (only update state). If this program", - "type": "string", - "x-go-name": "ClearStateProgram" - }, - "foreignapps": { - "description": "ForeignApps lists the applications (in addition to txn.ApplicationID)\nwhose global states may be accessed by this application's\nApprovalProgram and ClearStateProgram. The access is read-only.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64" - }, - "x-go-name": "ForeignApps" - }, - "foreignassets": { - "description": "ForeignAssets lists the assets whose parameters may be accessed by\nthis application's ApprovalProgram and ClearStateProgram. The access\nis read-only.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64" - }, - "x-go-name": "ForeignAssets" - }, - "globalschema": { - "$ref": "#/definitions/StateSchema" - }, - "id": { - "description": "ApplicationID is the application being interacted with, or 0 if\ncreating a new application.", - "type": "integer", - "format": "uint64", - "x-go-name": "ApplicationID" - }, - "localschema": { - "$ref": "#/definitions/StateSchema" - }, - "oncompletion": { - "description": "OnCompletion specifies what side effects this transaction will have\nif it successfully makes it into a block.\n\nrequire: true", - "type": "string", - "x-go-name": "OnCompletion" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "Asset": { - "description": "Asset specifies both the unique identifier and the parameters for an asset", - "type": "object", - "required": [ - "AssetIndex", - "AssetParams" - ], - "properties": { - "AssetIndex": { - "description": "AssetIndex is the unique asset identifier", - "type": "integer", - "format": "uint64" - }, - "AssetParams": { - "$ref": "#/definitions/AssetParams" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "AssetConfigTransactionType": { - "description": "AssetConfigTransactionType contains the additional fields for an asset config transaction", - "type": "object", - "properties": { - "id": { - "description": "AssetID is the asset being configured (or empty if creating)", - "type": "integer", - "format": "uint64", - "x-go-name": "AssetID" - }, - "params": { - "$ref": "#/definitions/AssetParams" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "AssetFreezeTransactionType": { - "description": "AssetFreezeTransactionType contains the additional fields for an asset freeze transaction", - "type": "object", - "required": [ - "id", - "acct", - "freeze" - ], - "properties": { - "acct": { - "description": "Account specifies the account where the asset is being frozen or thawed.", - "type": "string", - "x-go-name": "Account" - }, - "freeze": { - "description": "NewFreezeStatus specifies the new freeze status.", - "type": "boolean", - "x-go-name": "NewFreezeStatus" - }, - "id": { - "description": "AssetID is the asset being configured (or empty if creating)", - "type": "integer", - "format": "uint64", - "x-go-name": "AssetID" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "AssetList": { - "description": "AssetList contains a list of assets", - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "Assets is a list of assets", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "x-go-name": "Assets" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "AssetParams": { - "type": "object", - "title": "AssetParams specifies the parameters for an asset.", - "required": [ - "creator", - "total", - "decimals" - ], - "properties": { - "assetname": { - "description": "AssetName specifies the name of this asset,\nas supplied by the creator.", - "type": "string", - "x-go-name": "AssetName" - }, - "clawbackaddr": { - "description": "ClawbackAddr specifies the address used to clawback holdings of\nthis asset. If empty, clawback is not permitted.", - "type": "string", - "x-go-name": "ClawbackAddr" - }, - "creator": { - "description": "Creator specifies the address that created this asset.\nThis is the address where the parameters for this asset\ncan be found, and also the address where unwanted asset\nunits can be sent in the worst case.", - "type": "string", - "x-go-name": "Creator" - }, - "decimals": { - "description": "Decimals specifies the number of digits to use after the decimal\npoint when displaying this asset. If 0, the asset is not divisible.\nIf 1, the base unit of the asset is in tenths. If 2, the base unit\nof the asset is in hundredths, and so on.", - "type": "integer", - "format": "uint32", - "x-go-name": "Decimals" - }, - "defaultfrozen": { - "description": "DefaultFrozen specifies whether holdings in this asset\nare frozen by default.", - "type": "boolean", - "x-go-name": "DefaultFrozen" - }, - "freezeaddr": { - "description": "FreezeAddr specifies the address used to freeze holdings of\nthis asset. If empty, freezing is not permitted.", - "type": "string", - "x-go-name": "FreezeAddr" - }, - "managerkey": { - "description": "ManagerAddr specifies the address used to manage the keys of this\nasset and to destroy it.", - "type": "string", - "x-go-name": "ManagerAddr" - }, - "metadatahash": { - "description": "MetadataHash specifies a commitment to some unspecified asset\nmetadata. The format of this metadata is up to the application.", - "type": "string", - "format": "byte", - "x-go-name": "MetadataHash" - }, - "reserveaddr": { - "description": "ReserveAddr specifies the address holding reserve (non-minted)\nunits of this asset.", - "type": "string", - "x-go-name": "ReserveAddr" - }, - "total": { - "description": "Total specifies the total number of units of this asset.", - "type": "integer", - "format": "uint64", - "x-go-name": "Total" - }, - "unitname": { - "description": "UnitName specifies the name of a unit of this asset,\nas supplied by the creator.", - "type": "string", - "x-go-name": "UnitName" - }, - "url": { - "description": "URL specifies a URL where more information about the asset can be\nretrieved", - "type": "string", - "x-go-name": "URL" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "AssetTransferTransactionType": { - "description": "AssetTransferTransactionType contains the additional fields for an asset transfer transaction", - "type": "object", - "required": [ - "id", - "amt", - "rcv" - ], - "properties": { - "amt": { - "description": "Amount is the amount being transferred.", - "type": "integer", - "format": "uint64", - "x-go-name": "Amount" - }, - "closeto": { - "description": "CloseTo is the destination for remaining funds (if closing).", - "type": "string", - "x-go-name": "CloseTo" - }, - "closetoamount": { - "description": "CloseToAmount is amount of the remaining funds that were transferred to the close to address (if closing).", - "type": "integer", - "format": "uint64", - "x-go-name": "CloseToAmount" - }, - "id": { - "description": "AssetID is the asset being configured (or empty if creating)", - "type": "integer", - "format": "uint64", - "x-go-name": "AssetID" - }, - "rcv": { - "description": "Receiver is the recipient account.", - "type": "string", - "x-go-name": "Receiver" - }, - "snd": { - "description": "Sender is the source account (if using clawback).", - "type": "string", - "x-go-name": "Sender" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "Block": { - "description": "Block contains a block information", - "type": "object", - "required": [ - "currentProtocol", - "nextProtocol", - "nextProtocolApprovals", - "nextProtocolVoteBefore", - "nextProtocolSwitchOn", - "upgradePropose", - "upgradeApprove", - "hash", - "previousBlockHash", - "seed", - "proposer", - "round", - "period", - "txnRoot", - "timestamp" - ], - "properties": { - "currentProtocol": { - "description": "CurrentProtocol is a string that represents the current protocol", - "type": "string", - "x-go-name": "CurrentProtocol" - }, - "frac": { - "description": "The number of leftover MicroAlgos after the distribution of RewardsRate/rewardUnits\nMicroAlgos for every reward unit in the next round.", - "type": "integer", - "format": "uint64", - "x-go-name": "RewardsResidue" - }, - "hash": { - "description": "Hash is the current block hash", - "type": "string", - "x-go-name": "Hash" - }, - "nextProtocol": { - "description": "NextProtocol is a string that represents the next proposed protocol", - "type": "string", - "x-go-name": "NextProtocol" - }, - "nextProtocolApprovals": { - "description": "NextProtocolApprovals is the number of blocks which approved the protocol upgrade", - "type": "integer", - "format": "uint64", - "x-go-name": "NextProtocolApprovals" - }, - "nextProtocolSwitchOn": { - "description": "NextProtocolSwitchOn is the round on which the protocol upgrade will take effect", - "type": "integer", - "format": "uint64", - "x-go-name": "NextProtocolSwitchOn" - }, - "nextProtocolVoteBefore": { - "description": "NextProtocolVoteBefore is the deadline round for this protocol upgrade (No votes will be consider after this round)", - "type": "integer", - "format": "uint64", - "x-go-name": "NextProtocolVoteBefore" - }, - "period": { - "description": "Period is the period on which the block was confirmed", - "type": "integer", - "format": "uint64", - "x-go-name": "Period" - }, - "previousBlockHash": { - "description": "PreviousBlockHash is the previous block hash", - "type": "string", - "x-go-name": "PreviousBlockHash" - }, - "proposer": { - "description": "Proposer is the address of this block proposer", - "type": "string", - "x-go-name": "Proposer" - }, - "rate": { - "description": "The number of new MicroAlgos added to the participation stake from rewards at the next round.", - "type": "integer", - "format": "uint64", - "x-go-name": "RewardsRate" - }, - "reward": { - "description": "RewardsLevel specifies how many rewards, in MicroAlgos,\nhave been distributed to each config.Protocol.RewardUnit\nof MicroAlgos since genesis.", - "type": "integer", - "format": "uint64", - "x-go-name": "RewardsLevel" - }, - "round": { - "description": "Round is the current round on which this block was appended to the chain", - "type": "integer", - "format": "uint64", - "x-go-name": "Round" - }, - "seed": { - "description": "Seed is the sortition seed", - "type": "string", - "x-go-name": "Seed" - }, - "timestamp": { - "description": "TimeStamp in seconds since epoch", - "type": "integer", - "format": "int64", - "x-go-name": "Timestamp" - }, - "txnRoot": { - "description": "TransactionsRoot authenticates the set of transactions appearing in the block.\nMore specifically, it's the root of a merkle tree whose leaves are the block's Txids, in lexicographic order.\nFor the empty block, it's 0.\nNote that the TxnCommitments does not authenticate the signatures on the transactions, only the transactions themselves.\nTwo blocks with the same transactions but in a different order and with different signatures will have the same TxnCommitments.", - "type": "string", - "x-go-name": "TransactionsRoot" - }, - "txns": { - "$ref": "#/definitions/TransactionList" - }, - "upgradeApprove": { - "description": "UpgradeApprove indicates a yes vote for the current proposal", - "type": "boolean", - "x-go-name": "UpgradeApprove" - }, - "upgradePropose": { - "description": "UpgradePropose indicates a proposed upgrade", - "type": "string", - "x-go-name": "UpgradePropose" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "BuildVersion": { - "type": "object", - "title": "BuildVersion contains the current algod build version information.", - "required": [ - "major", - "minor", - "build_number", - "commit_hash", - "branch", - "channel" - ], - "properties": { - "branch": { - "type": "string", - "x-go-name": "Branch" - }, - "build_number": { - "type": "integer", - "format": "int64", - "x-go-name": "BuildNumber" - }, - "channel": { - "type": "string", - "x-go-name": "Channel" - }, - "commit_hash": { - "type": "string", - "x-go-name": "CommitHash" - }, - "major": { - "type": "integer", - "format": "int64", - "x-go-name": "Major" - }, - "minor": { - "type": "integer", - "format": "int64", - "x-go-name": "Minor" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/common" - }, - "KeyregTransactionType": { - "description": "KeyregTransactionType contains the additional fields for a keyreg Transaction", - "type": "object", - "properties": { - "selkey": { - "description": "SelectionPK is the VRF public key used in key registration transactions", - "type": "string", - "format": "byte", - "x-go-name": "SelectionPK" - }, - "votefst": { - "description": "VoteFirst is the first round this participation key is valid", - "type": "integer", - "format": "uint64", - "x-go-name": "VoteFirst" - }, - "votekd": { - "description": "VoteKeyDilution is the dilution for the 2-level participation key", - "type": "integer", - "format": "uint64", - "x-go-name": "VoteKeyDilution" - }, - "votekey": { - "description": "VotePK is the participation public key used in key registration transactions", - "type": "string", - "format": "byte", - "x-go-name": "VotePK" - }, - "votelst": { - "description": "VoteLast is the last round this participation key is valid", - "type": "integer", - "format": "uint64", - "x-go-name": "VoteLast" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "NodeStatus": { - "description": "NodeStatus contains the information about a node status", - "type": "object", - "required": [ - "lastRound", - "lastConsensusVersion", - "nextConsensusVersion", - "nextConsensusVersionRound", - "nextConsensusVersionSupported", - "timeSinceLastRound", - "catchupTime", - "hasSyncedSinceStartup", - "stoppedAtUnsupportedRound" - ], - "properties": { - "catchupTime": { - "description": "CatchupTime in nanoseconds", - "type": "integer", - "format": "int64", - "x-go-name": "CatchupTime" - }, - "hasSyncedSinceStartup": { - "description": "HasSyncedSinceStartup indicates whether a round has completed since startup", - "type": "boolean", - "x-go-name": "HasSyncedSinceStartup" - }, - "lastConsensusVersion": { - "description": "LastVersion indicates the last consensus version supported", - "type": "string", - "x-go-name": "LastVersion" - }, - "lastRound": { - "description": "LastRound indicates the last round seen", - "type": "integer", - "format": "uint64", - "x-go-name": "LastRound" - }, - "nextConsensusVersion": { - "description": "NextVersion of consensus protocol to use", - "type": "string", - "x-go-name": "NextVersion" - }, - "nextConsensusVersionRound": { - "description": "NextVersionRound is the round at which the next consensus version will apply", - "type": "integer", - "format": "uint64", - "x-go-name": "NextVersionRound" - }, - "nextConsensusVersionSupported": { - "description": "NextVersionSupported indicates whether the next consensus version is supported by this node", - "type": "boolean", - "x-go-name": "NextVersionSupported" - }, - "stoppedAtUnsupportedRound": { - "description": "StoppedAtUnsupportedRound indicates that the node does not support the new rounds and has stopped making progress", - "type": "boolean", - "x-go-name": "StoppedAtUnsupportedRound" - }, - "timeSinceLastRound": { - "description": "TimeSinceLastRound in nanoseconds", - "type": "integer", - "format": "int64", - "x-go-name": "TimeSinceLastRound" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "Participation": { - "description": "Participation Description", - "type": "object", - "required": [ - "partpkb64", - "vrfpkb64", - "votefst", - "votelst", - "votekd" - ], - "properties": { - "partpkb64": { - "description": "ParticipationPK is the root participation public key (if any) currently registered for this round", - "type": "string", - "format": "byte", - "x-go-name": "ParticipationPK" - }, - "votefst": { - "description": "VoteFirst is the first round for which this participation is valid.", - "type": "integer", - "format": "uint64", - "x-go-name": "VoteFirst" - }, - "votekd": { - "description": "VoteKeyDilution is the number of subkeys in for each batch of participation keys.", - "type": "integer", - "format": "uint64", - "x-go-name": "VoteKeyDilution" - }, - "votelst": { - "description": "VoteLast is the last round for which this participation is valid.", - "type": "integer", - "format": "uint64", - "x-go-name": "VoteLast" - }, - "vrfpkb64": { - "description": "VRFPK is the selection public key (if any) currently registered for this round", - "type": "string", - "format": "byte", - "x-go-name": "VRFPK" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "PaymentTransactionType": { - "description": "PaymentTransactionType contains the additional fields for a payment Transaction", - "type": "object", - "required": [ - "to", - "amount" - ], - "properties": { - "amount": { - "description": "Amount is the amount of MicroAlgos intended to be transferred", - "type": "integer", - "format": "uint64", - "x-go-name": "Amount" - }, - "close": { - "description": "CloseRemainderTo is the address the sender closed to", - "type": "string", - "x-go-name": "CloseRemainderTo" - }, - "closeamount": { - "description": "CloseAmount is the amount sent to CloseRemainderTo, for committed transaction", - "type": "integer", - "format": "uint64", - "x-go-name": "CloseAmount" - }, - "closerewards": { - "description": "CloseRewards is the amount of pending rewards applied to the CloseRemainderTo\naccount as part of this transaction.", - "type": "integer", - "format": "uint64", - "x-go-name": "CloseRewards" - }, - "to": { - "description": "To is the receiver's address", - "type": "string", - "x-go-name": "To" - }, - "torewards": { - "description": "ToRewards is the amount of pending rewards applied to the To account\nas part of this transaction.", - "type": "integer", - "format": "uint64", - "x-go-name": "ToRewards" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "PendingTransactions": { - "description": "PendingTransactions represents a potentially truncated list of transactions currently in the\nnode's transaction pool.", - "type": "object", - "required": [ - "truncatedTxns", - "totalTxns" - ], - "properties": { - "totalTxns": { - "description": "TotalTxns", - "type": "integer", - "format": "uint64", - "x-go-name": "TotalTxns" - }, - "truncatedTxns": { - "$ref": "#/definitions/TransactionList" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "StateProofTransactionType": { - "description": "StateProofTransactionType contains the additional fields for a state proof transaction", - "type": "object", - "required": [ - "sp", - "spmsg" - ], - "properties": { - "sp": { - "description": "StateProof is the msgpack encoding of the state proof.", - "type": "string", - "format": "byte", - "x-go-name": "StateProof" - }, - "spmsg": { - "description": "StateProofMessage is the msgpack encoding of the state proof message.", - "type": "string", - "format": "byte", - "x-go-name": "StateProofMessage" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "StateSchema": { - "description": "swagger: model StateSchema", - "type": "object", - "title": "StateSchema represents a LocalStateSchema or GlobalStateSchema. These\nschemas determine how much storage may be used in a LocalState or\nGlobalState for an application. The more space used, the larger minimum\nbalance must be maintained in the account holding the data.", - "required": [ - "uints", - "byteslices" - ], - "properties": { - "byteslices": { - "description": "NumByteSlice is the maximum number of TEAL byte slices that may be\nstored in the key/value store", - "type": "integer", - "format": "uint64", - "x-go-name": "NumByteSlice" - }, - "uints": { - "description": "NumUint is the maximum number of TEAL uints that may be stored in\nthe key/value store", - "type": "integer", - "format": "uint64", - "x-go-name": "NumUint" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "Supply": { - "description": "Supply represents the current supply of MicroAlgos in the system", - "type": "object", - "required": [ - "round", - "totalMoney", - "onlineMoney" - ], - "properties": { - "onlineMoney": { - "description": "OnlineMoney", - "type": "integer", - "format": "uint64", - "x-go-name": "OnlineMoney" - }, - "round": { - "description": "Round", - "type": "integer", - "format": "uint64", - "x-go-name": "Round" - }, - "totalMoney": { - "description": "TotalMoney", - "type": "integer", - "format": "uint64", - "x-go-name": "TotalMoney" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "Transaction": { - "description": "Transaction contains all fields common to all transactions and serves as an envelope to all transactions\ntype", - "type": "object", - "required": [ - "type", - "tx", - "from", - "fee", - "first-round", - "last-round", - "app", - "sp", - "genesisID", - "genesishashb64" - ], - "properties": { - "app": { - "$ref": "#/definitions/ApplicationCallTransactionType" - }, - "curcfg": { - "$ref": "#/definitions/AssetConfigTransactionType" - }, - "curfrz": { - "$ref": "#/definitions/AssetFreezeTransactionType" - }, - "curxfer": { - "$ref": "#/definitions/AssetTransferTransactionType" - }, - "fee": { - "description": "Fee is the transaction fee", - "type": "integer", - "format": "uint64", - "x-go-name": "Fee" - }, - "first-round": { - "description": "FirstRound indicates the first valid round for this transaction", - "type": "integer", - "format": "uint64", - "x-go-name": "FirstRound" - }, - "from": { - "description": "From is the sender's address", - "type": "string", - "x-go-name": "From" - }, - "fromrewards": { - "description": "FromRewards is the amount of pending rewards applied to the From\naccount as part of this transaction.", - "type": "integer", - "format": "uint64", - "x-go-name": "FromRewards" - }, - "genesisID": { - "description": "Genesis ID", - "type": "string", - "x-go-name": "GenesisID" - }, - "genesishashb64": { - "description": "Genesis hash", - "type": "string", - "format": "byte", - "x-go-name": "GenesisHash" - }, - "group": { - "description": "Group", - "type": "string", - "format": "byte", - "x-go-name": "Group" - }, - "keyreg": { - "$ref": "#/definitions/KeyregTransactionType" - }, - "last-round": { - "description": "LastRound indicates the last valid round for this transaction", - "type": "integer", - "format": "uint64", - "x-go-name": "LastRound" - }, - "lease": { - "description": "Lease enforces mutual exclusion of transactions. If this field is\nnonzero, then once the transaction is confirmed, it acquires the\nlease identified by the (Sender, Lease) pair of the transaction until\nthe LastValid round passes. While this transaction possesses the\nlease, no other transaction specifying this lease can be confirmed.", - "type": "string", - "format": "byte", - "x-go-name": "Lease" - }, - "noteb64": { - "description": "Note is a free form data", - "type": "string", - "format": "byte", - "x-go-name": "Note" - }, - "payment": { - "$ref": "#/definitions/PaymentTransactionType" - }, - "poolerror": { - "description": "PoolError indicates the transaction was evicted from this node's transaction\npool (if non-empty). A non-empty PoolError does not guarantee that the\ntransaction will never be committed; other nodes may not have evicted the\ntransaction and may attempt to commit it in the future.", - "type": "string", - "x-go-name": "PoolError" - }, - "round": { - "description": "ConfirmedRound indicates the block number this transaction appeared in", - "type": "integer", - "format": "uint64", - "x-go-name": "ConfirmedRound" - }, - "sp": { - "$ref": "#/definitions/StateProofTransactionType" - }, - "tx": { - "description": "TxID is the transaction ID", - "type": "string", - "x-go-name": "TxID" - }, - "txresults": { - "$ref": "#/definitions/TransactionResults" - }, - "type": { - "description": "Type is the transaction type", - "type": "string", - "x-go-name": "Type" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "TransactionFee": { - "description": "TransactionFee contains the suggested fee", - "type": "object", - "required": [ - "fee" - ], - "properties": { - "fee": { - "description": "Fee is transaction fee\nFee is in units of micro-Algos per byte.\nFee may fall to zero but transactions must still have a fee of\nat least MinTxnFee for the current network protocol.", - "type": "integer", - "format": "uint64", - "x-go-name": "Fee" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "TransactionList": { - "description": "TransactionList contains a list of transactions", - "type": "object", - "required": [ - "transactions" - ], - "properties": { - "transactions": { - "description": "TransactionList is a list of transactions", - "type": "array", - "items": { - "$ref": "#/definitions/Transaction" - }, - "x-go-name": "Transactions" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "TransactionParams": { - "description": "TransactionParams contains the parameters that help a client construct\na new transaction.", - "type": "object", - "required": [ - "fee", - "genesisID", - "genesishashb64", - "lastRound", - "consensusVersion" - ], - "properties": { - "consensusVersion": { - "description": "ConsensusVersion indicates the consensus protocol version\nas of LastRound.", - "type": "string", - "x-go-name": "ConsensusVersion" - }, - "fee": { - "description": "Fee is the suggested transaction fee\nFee is in units of micro-Algos per byte.\nFee may fall to zero but transactions must still have a fee of\nat least MinTxnFee for the current network protocol.", - "type": "integer", - "format": "uint64", - "x-go-name": "Fee" - }, - "genesisID": { - "description": "Genesis ID", - "type": "string", - "x-go-name": "GenesisID" - }, - "genesishashb64": { - "description": "Genesis hash", - "type": "string", - "format": "byte", - "x-go-name": "GenesisHash" - }, - "lastRound": { - "description": "LastRound indicates the last round seen", - "type": "integer", - "format": "uint64", - "x-go-name": "LastRound" - }, - "minFee": { - "description": "The minimum transaction fee (not per byte) required for the\ntxn to validate for the current network protocol.", - "type": "integer", - "format": "uint64", - "x-go-name": "MinTxnFee" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "TransactionResults": { - "description": "TransactionResults contains information about the side effects of a transaction", - "type": "object", - "properties": { - "createdapp": { - "description": "CreatedAppIndex indicates the app index of an app created by this txn", - "type": "integer", - "format": "uint64", - "x-go-name": "CreatedAppIndex" - }, - "createdasset": { - "description": "CreatedAssetIndex indicates the asset index of an asset created by this txn", - "type": "integer", - "format": "uint64", - "x-go-name": "CreatedAssetIndex" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - }, - "Version": { - "description": "Note that we annotate this as a model so that legacy clients\ncan directly import a swagger generated Version model.", - "type": "object", - "title": "Version contains the current algod version.", - "required": [ - "versions", - "genesis_id", - "genesis_hash_b64", - "build" - ], - "properties": { - "build": { - "$ref": "#/definitions/BuildVersion" - }, - "genesis_hash_b64": { - "type": "string", - "format": "byte", - "x-go-name": "GenesisHash" - }, - "genesis_id": { - "type": "string", - "x-go-name": "GenesisID" - }, - "versions": { - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Versions" - } - }, - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/common" - }, - "transactionID": { - "description": "TransactionID Description", - "type": "object", - "required": [ - "txId" - ], - "properties": { - "txId": { - "description": "TxId is the string encoding of the transaction hash", - "type": "string", - "x-go-name": "TxID" - } - }, - "x-go-name": "TransactionID", - "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - } - }, - "responses": { - "AccountInformationResponse": { - "description": "AccountInformationResponse contains an account information", - "schema": { - "$ref": "#/definitions/Account" - } - }, - "AssetInformationResponse": { - "description": "AssetInformationResponse contains asset information", - "schema": { - "$ref": "#/definitions/AssetParams" - } - }, - "AssetsResponse": { - "description": "AssetsResponse contains a list of assets", - "schema": { - "$ref": "#/definitions/AssetList" - } - }, - "BlockResponse": { - "description": "BlockResponse contains block information", - "schema": { - "$ref": "#/definitions/Block" - } - }, - "PendingTransactionsResponse": { - "description": "PendingTransactionsResponse contains a (potentially truncated) list of transactions and\nthe total number of transactions currently in the pool.", - "schema": { - "$ref": "#/definitions/PendingTransactions" - } - }, - "StatusResponse": { - "description": "StatusResponse contains the node's status information", - "schema": { - "$ref": "#/definitions/NodeStatus" - } - }, - "SupplyResponse": { - "description": "SupplyResponse contains the ledger supply information", - "schema": { - "$ref": "#/definitions/Supply" - } - }, - "TransactionFeeResponse": { - "description": "TransactionFeeResponse contains a suggested fee", - "schema": { - "$ref": "#/definitions/TransactionFee" - } - }, - "TransactionIDResponse": { - "description": "TransactionIDResponse contains a transaction information", - "schema": { - "$ref": "#/definitions/transactionID" - } - }, - "TransactionParamsResponse": { - "description": "TransactionParamsResponse contains the parameters for\nconstructing a new transaction.", - "schema": { - "$ref": "#/definitions/TransactionParams" - } - }, - "TransactionResponse": { - "description": "TransactionResponse contains a transaction information", - "schema": { - "$ref": "#/definitions/Transaction" - } - }, - "TransactionsResponse": { - "description": "TransactionsResponse contains a list of transactions", - "schema": { - "$ref": "#/definitions/TransactionList" - } - }, - "VersionsResponse": { - "description": "VersionsResponse is the response to 'GET /versions'", - "schema": { - "$ref": "#/definitions/Version" - } - } - }, - "securityDefinitions": { - "api_key": { - "description": "Generated header parameter. This token can be generated using the Goal command line tool. Example value ='b7e384d0317b8050ce45900a94a1931e28540e1f69b2d242b424659c341b4697'", - "type": "apiKey", - "name": "X-Algo-API-Token", - "in": "header", - "x-example": "b7e384d0317b8050ce45900a94a1931e28540e1f69b2d242b424659c341b4697" - } - }, - "security": [ - { - "api_key": [] - } - ] -} \ No newline at end of file diff --git a/daemon/algod/api/swagger.json.validated b/daemon/algod/api/swagger.json.validated deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/data/pools/errors.go b/data/pools/errors.go new file mode 100644 index 0000000000..e465983fa9 --- /dev/null +++ b/data/pools/errors.go @@ -0,0 +1,47 @@ +// Copyright (C) 2019-2022 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 pools + +import ( + "errors" + "fmt" + + "github.com/algorand/go-algorand/data/basics" +) + +// ErrStaleBlockAssemblyRequest returned by AssembleBlock when requested block number is older than the current transaction pool round +// i.e. typically it means that we're trying to make a proposal for an older round than what the ledger is currently pointing at. +var ErrStaleBlockAssemblyRequest = errors.New("AssembleBlock: requested block assembly specified a round that is older than current transaction pool round") + +// ErrPendingQueueReachedMaxCap indicates the current transaction pool has reached its max capacity +var ErrPendingQueueReachedMaxCap = errors.New("TransactionPool.checkPendingQueueSize: transaction pool have reached capacity") + +// ErrNoPendingBlockEvaluator indicates there is no pending block evaluator to accept a new tx group +var ErrNoPendingBlockEvaluator = errors.New("TransactionPool.ingest: no pending block evaluator") + +// ErrTxPoolFeeError is an error type for txpool fee escalation checks +type ErrTxPoolFeeError struct { + fee basics.MicroAlgos + feeThreshold uint64 + feePerByte uint64 + encodedLength int +} + +func (e *ErrTxPoolFeeError) Error() string { + return fmt.Sprintf("fee %d below threshold %d (%d per byte * %d bytes)", + e.fee, e.feeThreshold, e.feePerByte, e.encodedLength) +} diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go index 4302d14a2d..a4670c3020 100644 --- a/data/pools/transactionPool.go +++ b/data/pools/transactionPool.go @@ -168,10 +168,6 @@ const ( generateBlockTransactionDuration = 2155 * time.Nanosecond ) -// ErrStaleBlockAssemblyRequest returned by AssembleBlock when requested block number is older than the current transaction pool round -// i.e. typically it means that we're trying to make a proposal for an older round than what the ledger is currently pointing at. -var ErrStaleBlockAssemblyRequest = fmt.Errorf("AssembleBlock: requested block assembly specified a round that is older than current transaction pool round") - // Reset resets the content of the transaction pool func (pool *TransactionPool) Reset() { pool.mu.Lock() @@ -291,7 +287,7 @@ func (pool *TransactionPool) checkPendingQueueSize(txnGroup []transactions.Signe return nil } } - return fmt.Errorf("TransactionPool.checkPendingQueueSize: transaction pool have reached capacity") + return ErrPendingQueueReachedMaxCap } return nil } @@ -360,8 +356,12 @@ func (pool *TransactionPool) checkSufficientFee(txgroup []transactions.SignedTxn for _, t := range txgroup { feeThreshold := feePerByte * uint64(t.GetEncodedLength()) if t.Txn.Fee.Raw < feeThreshold { - return fmt.Errorf("fee %d below threshold %d (%d per byte * %d bytes)", - t.Txn.Fee, feeThreshold, feePerByte, t.GetEncodedLength()) + return &ErrTxPoolFeeError{ + fee: t.Txn.Fee, + feeThreshold: feeThreshold, + feePerByte: feePerByte, + encodedLength: t.GetEncodedLength(), + } } } @@ -415,7 +415,7 @@ func (pool *TransactionPool) add(txgroup []transactions.SignedTxn, stats *teleme // while it waits for OnNewBlock() to be called. func (pool *TransactionPool) ingest(txgroup []transactions.SignedTxn, params poolIngestParams) error { if pool.pendingBlockEvaluator == nil { - return fmt.Errorf("TransactionPool.ingest: no pending block evaluator") + return ErrNoPendingBlockEvaluator } if !params.recomputing { @@ -427,7 +427,7 @@ func (pool *TransactionPool) ingest(txgroup []transactions.SignedTxn, params poo for pool.pendingBlockEvaluator.Round() <= latest && time.Now().Before(waitExpires) { condvar.TimedWait(&pool.cond, timeoutOnNewBlock) if pool.pendingBlockEvaluator == nil { - return fmt.Errorf("TransactionPool.ingest: no pending block evaluator") + return ErrNoPendingBlockEvaluator } } @@ -467,7 +467,7 @@ func (pool *TransactionPool) Remember(txgroup []transactions.SignedTxn) error { err := pool.remember(txgroup) if err != nil { - return fmt.Errorf("TransactionPool.Remember: %v", err) + return fmt.Errorf("TransactionPool.Remember: %w", err) } pool.rememberCommit(false) @@ -581,7 +581,7 @@ func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactio r := pool.pendingBlockEvaluator.Round() + pool.numPendingWholeBlocks for _, tx := range txgroup { if tx.Txn.LastValid < r { - return transactions.TxnDeadError{ + return &transactions.TxnDeadError{ Round: r, FirstValid: tx.Txn.FirstValid, LastValid: tx.Txn.LastValid, @@ -600,7 +600,7 @@ func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactio if recomputing { if !pool.assemblyResults.assemblyCompletedOrAbandoned { - transactionGroupDuration := time.Now().Sub(transactionGroupStartsTime) + transactionGroupDuration := time.Since(transactionGroupStartsTime) pool.assemblyMu.Lock() defer pool.assemblyMu.Unlock() if pool.assemblyRound > pool.pendingBlockEvaluator.Round() { @@ -630,7 +630,7 @@ func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactio } else { pool.assemblyResults.blk = lvb } - stats.BlockGenerationDuration = uint64(time.Now().Sub(blockGenerationStarts)) + stats.BlockGenerationDuration = uint64(time.Since(blockGenerationStarts)) pool.assemblyResults.stats = *stats pool.assemblyCond.Broadcast() } else { @@ -742,7 +742,7 @@ func (pool *TransactionPool) recomputeBlockEvaluator(committedTxIds map[transact case *ledgercore.TransactionInLedgerError: asmStats.CommittedCount++ stats.RemovedInvalidCount++ - case transactions.TxnDeadError: + case *transactions.TxnDeadError: if int(terr.LastValid-terr.FirstValid) > 20 { // cutoff value here is picked as a somewhat arbitrary cutoff trying to separate longer lived transactions from very short lived ones asmStats.ExpiredLongLivedCount++ @@ -753,7 +753,7 @@ func (pool *TransactionPool) recomputeBlockEvaluator(committedTxIds map[transact asmStats.LeaseErrorCount++ stats.RemovedInvalidCount++ pool.log.Infof("Cannot re-add pending transaction to pool: %v", err) - case transactions.MinFeeError: + case *transactions.MinFeeError: asmStats.MinFeeErrorCount++ stats.RemovedInvalidCount++ pool.log.Infof("Cannot re-add pending transaction to pool: %v", err) @@ -784,7 +784,7 @@ func (pool *TransactionPool) recomputeBlockEvaluator(committedTxIds map[transact } else { pool.assemblyResults.blk = lvb } - asmStats.BlockGenerationDuration = uint64(time.Now().Sub(blockGenerationStarts)) + asmStats.BlockGenerationDuration = uint64(time.Since(blockGenerationStarts)) pool.assemblyResults.stats = asmStats pool.assemblyCond.Broadcast() } @@ -835,7 +835,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim } // Measure time here because we want to know how close to deadline we are - dt := time.Now().Sub(start) + dt := time.Since(start) stats.Nanoseconds = dt.Nanoseconds() payset := assembled.Block().Payset @@ -906,7 +906,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim pool.assemblyDeadline = deadline pool.assemblyRound = round for time.Now().Before(deadline) && (!pool.assemblyResults.ok || pool.assemblyResults.roundStartedEvaluating != round) { - condvar.TimedWait(&pool.assemblyCond, deadline.Sub(time.Now())) + condvar.TimedWait(&pool.assemblyCond, time.Until(deadline)) } if !pool.assemblyResults.ok { @@ -919,7 +919,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim if pool.assemblyResults.roundStartedEvaluating > round { // this case is expected to happen only if the transaction pool was able to construct *two* rounds during the time we were trying to assemble the empty block. - // while this is extreamly unlikely, we need to handle this. the handling it quite straight-forward : + // while this is extremely unlikely, we need to handle this. the handling it quite straight-forward : // since the network is already ahead of us, there is no issue here in not generating a block ( since the block would get discarded anyway ) pool.log.Infof("AssembleBlock: requested round is behind transaction pool round after timing out %d < %d", round, pool.assemblyResults.roundStartedEvaluating) return nil, ErrStaleBlockAssemblyRequest @@ -927,7 +927,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim deadline = deadline.Add(assemblyWaitEps) for time.Now().Before(deadline) && (!pool.assemblyResults.ok || pool.assemblyResults.roundStartedEvaluating != round) { - condvar.TimedWait(&pool.assemblyCond, deadline.Sub(time.Now())) + condvar.TimedWait(&pool.assemblyCond, time.Until(deadline)) } // check to see if the extra time helped us to get a block. @@ -949,7 +949,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim if pool.assemblyResults.roundStartedEvaluating > round { // this scenario should not happen unless the txpool is receiving the new blocks via OnNewBlock // with "jumps" between consecutive blocks ( which is why it's a warning ) - // The "normal" usecase is evaluated on the top of the function. + // The "normal" use case is evaluated on the top of the function. pool.log.Warnf("AssembleBlock: requested round is behind transaction pool round %d < %d", round, pool.assemblyResults.roundStartedEvaluating) return nil, ErrStaleBlockAssemblyRequest } else if pool.assemblyResults.roundStartedEvaluating == round.SubSaturate(1) { diff --git a/data/pools/transactionPool_test.go b/data/pools/transactionPool_test.go index acbc5a9bc1..be0546c3b1 100644 --- a/data/pools/transactionPool_test.go +++ b/data/pools/transactionPool_test.go @@ -100,7 +100,7 @@ func mockLedger(t TestingT, initAccounts map[basics.Address]basics.AccountData, genesisInitState := ledgercore.InitState{Block: initBlock, Accounts: initAccounts, GenesisHash: hash} cfg := config.GetDefaultLocal() cfg.Archival = true - l, err := ledger.OpenLedger(logging.Base(), fn, true, genesisInitState, cfg) + l, err := ledger.OpenLedger(logging.Base(), fn, inMem, genesisInitState, cfg) require.NoError(t, err) return l } @@ -967,7 +967,7 @@ func TestTransactionPool_CurrentFeePerByte(t *testing.T) { Amount: basics.MicroAlgos{Raw: proto.MinBalance}, }, } - tx.Note = make([]byte, 8, 8) + tx.Note = make([]byte, 8) crypto.RandBytes(tx.Note) signedTx := tx.Sign(secrets[i]) err := transactionPool.RememberOne(signedTx) @@ -1018,7 +1018,7 @@ func BenchmarkTransactionPoolRememberOne(b *testing.B) { Amount: basics.MicroAlgos{Raw: proto.MinBalance}, }, } - tx.Note = make([]byte, 8, 8) + tx.Note = make([]byte, 8) crypto.RandBytes(tx.Note) signedTx := tx.Sign(secrets[i]) signedTransactions = append(signedTransactions, signedTx) @@ -1081,7 +1081,7 @@ func BenchmarkTransactionPoolPending(b *testing.B) { Amount: basics.MicroAlgos{Raw: proto.MinBalance}, }, } - tx.Note = make([]byte, 8, 8) + tx.Note = make([]byte, 8) crypto.RandBytes(tx.Note) signedTx := tx.Sign(secrets[i]) err := transactionPool.RememberOne(signedTx) @@ -1247,7 +1247,7 @@ func BenchmarkTransactionPoolSteadyState(b *testing.B) { Amount: basics.MicroAlgos{Raw: proto.MinBalance}, }, } - tx.Note = make([]byte, 8, 8) + tx.Note = make([]byte, 8) crypto.RandBytes(tx.Note) signedTx, err := transactions.AssembleSignedTxn(tx, crypto.Signature{}, crypto.MultisigSig{}) @@ -1453,7 +1453,7 @@ func TestStateProofLogging(t *testing.T) { require.NoError(t, err) b.BlockHeader.Branch = phdr.Hash() - eval, err := mockLedger.StartEvaluator(b.BlockHeader, 0, 10000) + _, err = mockLedger.StartEvaluator(b.BlockHeader, 0, 10000) require.NoError(t, err) // Simulate the blocks up to round 512 without any transactions @@ -1477,7 +1477,7 @@ func TestStateProofLogging(t *testing.T) { break } - eval, err = mockLedger.StartEvaluator(b.BlockHeader, 0, 10000) + _, err = mockLedger.StartEvaluator(b.BlockHeader, 0, 10000) require.NoError(t, err) } @@ -1520,7 +1520,7 @@ func TestStateProofLogging(t *testing.T) { require.NoError(t, err) // Add it to the transaction pool and assemble the block - eval, err = mockLedger.StartEvaluator(b.BlockHeader, 0, 1000000) + eval, err := mockLedger.StartEvaluator(b.BlockHeader, 0, 1000000) require.NoError(t, err) err = eval.Transaction(stxn, transactions.ApplyData{}) diff --git a/data/transactions/error.go b/data/transactions/error.go index a95deeb3f1..d29db15fc8 100644 --- a/data/transactions/error.go +++ b/data/transactions/error.go @@ -23,14 +23,16 @@ import ( ) // MinFeeError defines an error type which could be returned from the method WellFormed +//msgp:ignore MinFeeError type MinFeeError string -func (err MinFeeError) Error() string { - return string(err) +func (err *MinFeeError) Error() string { + return string(*err) } -func makeMinFeeErrorf(format string, args ...interface{}) MinFeeError { - return MinFeeError(fmt.Sprintf(format, args...)) +func makeMinFeeErrorf(format string, args ...interface{}) *MinFeeError { + err := MinFeeError(fmt.Sprintf(format, args...)) + return &err } // TxnDeadError defines an error type which indicates a transaction is outside of the @@ -41,6 +43,6 @@ type TxnDeadError struct { LastValid basics.Round } -func (err TxnDeadError) Error() string { +func (err *TxnDeadError) Error() string { return fmt.Sprintf("txn dead: round %d outside of %d--%d", err.Round, err.FirstValid, err.LastValid) } diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 34db841c12..f89da449bc 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -778,11 +778,12 @@ func EvalApp(program []byte, gi int, aid basics.AppIndex, params *EvalParams) (b return pass, err } -// EvalSignature evaluates the logicsig of the ith transaction in params. +// EvalSignatureFull evaluates the logicsig of the ith transaction in params. // A program passes successfully if it finishes with one int element on the stack that is non-zero. -func EvalSignature(gi int, params *EvalParams) (pass bool, err error) { +// It returns EvalContext suitable for obtaining additional info about the execution. +func EvalSignatureFull(gi int, params *EvalParams) (pass bool, pcx *EvalContext, err error) { if params.SigLedger == nil { - return false, errors.New("no sig ledger in signature eval") + return false, nil, errors.New("no sig ledger in signature eval") } cx := EvalContext{ EvalParams: params, @@ -790,7 +791,15 @@ func EvalSignature(gi int, params *EvalParams) (pass bool, err error) { groupIndex: gi, txn: ¶ms.TxnGroup[gi], } - return eval(cx.txn.Lsig.Logic, &cx) + pass, err = eval(cx.txn.Lsig.Logic, &cx) + return pass, &cx, err +} + +// EvalSignature evaluates the logicsig of the ith transaction in params. +// A program passes successfully if it finishes with one int element on the stack that is non-zero. +func EvalSignature(gi int, params *EvalParams) (pass bool, err error) { + pass, _, err = EvalSignatureFull(gi, params) + return pass, err } // eval implementation @@ -997,6 +1006,11 @@ func boolToSV(x bool) stackValue { return stackValue{Uint: boolToUint(x)} } +// Cost return cost incurred so far +func (cx *EvalContext) Cost() int { + return cx.cost +} + func (cx *EvalContext) remainingBudget() int { if cx.runModeFlags == modeSig { return int(cx.Proto.LogicSigMaxCost) - cx.cost diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 9fa1753734..1529186e33 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -389,9 +389,10 @@ byte base64 5rZMNsevs5sULO+54aN+OvU6lQ503z2X+SSYUABIx7E= ep := defaultEvalParams(txn) err := CheckSignature(0, ep) require.NoError(t, err) - pass, err := EvalSignature(0, ep) + pass, cx, err := EvalSignatureFull(0, ep) require.True(t, pass) require.NoError(t, err) + require.Greater(t, cx.Cost(), 0) }) } } @@ -454,10 +455,12 @@ func TestTLHC(t *testing.T) { t.Log(ep.Trace.String()) } require.NoError(t, err) - pass, err := EvalSignature(0, ep) + pass, cx, err := EvalSignatureFull(0, ep) if pass { t.Log(hex.EncodeToString(ops.Program)) t.Log(ep.Trace.String()) + require.Greater(t, cx.cost, 0) + require.Greater(t, cx.Cost(), 0) } require.False(t, pass) isNotPanic(t, err) diff --git a/data/transactions/logic/sourcemap.go b/data/transactions/logic/sourcemap.go index f78a1b88e4..b2e6e2acc7 100644 --- a/data/transactions/logic/sourcemap.go +++ b/data/transactions/logic/sourcemap.go @@ -36,9 +36,7 @@ type SourceMap struct { SourceRoot string `json:"sourceRoot,omitempty"` Sources []string `json:"sources"` Names []string `json:"names"` - // Mapping field is deprecated. Use `Mappings` field instead. - Mapping string `json:"mapping"` - Mappings string `json:"mappings"` + Mappings string `json:"mappings"` } // GetSourceMap returns a struct containing details about @@ -64,11 +62,9 @@ func GetSourceMap(sourceNames []string, offsetToLine map[int]int) SourceMap { } return SourceMap{ - Version: sourceMapVersion, - Sources: sourceNames, - Names: []string{}, // TEAL code does not generate any names. - // Mapping is deprecated, and only for backwards compatibility. - Mapping: strings.Join(pcToLine, ";"), + Version: sourceMapVersion, + Sources: sourceNames, + Names: []string{}, // TEAL code does not generate any names. Mappings: strings.Join(pcToLine, ";"), } } diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index bcb87f64e0..641e541c7b 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -93,14 +93,6 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // -// MinFeeError -// |-----> MarshalMsg -// |-----> CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> Msgsize -// |-----> MsgIsZero -// // OnCompletion // |-----> MarshalMsg // |-----> CanMarshalMsg @@ -3082,52 +3074,6 @@ func (z *LogicSig) MsgIsZero() bool { return (len((*z).Logic) == 0) && ((*z).Sig.MsgIsZero()) && ((*z).Msig.MsgIsZero()) && (len((*z).Args) == 0) } -// MarshalMsg implements msgp.Marshaler -func (z MinFeeError) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - o = msgp.AppendString(o, string(z)) - return -} - -func (_ MinFeeError) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(MinFeeError) - if !ok { - _, ok = (z).(*MinFeeError) - } - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *MinFeeError) UnmarshalMsg(bts []byte) (o []byte, err error) { - { - var zb0001 string - zb0001, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - (*z) = MinFeeError(zb0001) - } - o = bts - return -} - -func (_ *MinFeeError) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*MinFeeError) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z MinFeeError) Msgsize() (s int) { - s = msgp.StringPrefixSize + len(string(z)) - return -} - -// MsgIsZero returns whether this is a zero value -func (z MinFeeError) MsgIsZero() bool { - return z == "" -} - // MarshalMsg implements msgp.Marshaler func (z OnCompletion) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index a22abd7ff7..8aa93740c7 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -184,6 +184,7 @@ func (tx Transaction) ID() Txid { } // IDSha256 returns the digest (i.e., hash) of the transaction. +// This is different from the canonical ID computed with Sum512_256 hashing function. func (tx Transaction) IDSha256() crypto.Digest { enc := tx.MarshalMsg(append(protocol.GetEncodingBuf(), []byte(protocol.Transaction)...)) defer protocol.PutEncodingBuf(enc) @@ -234,7 +235,7 @@ func (tx Header) Alive(tc TxnContext) error { // Check round validity round := tc.Round() if round < tx.FirstValid || round > tx.LastValid { - return TxnDeadError{ + return &TxnDeadError{ Round: round, FirstValid: tx.FirstValid, LastValid: tx.LastValid, diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index 115bd95362..4dc8bc6563 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -36,10 +36,17 @@ import ( var logicGoodTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_logic_ok", Description: "Total transaction scripts executed and accepted"}) var logicRejTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_logic_rej", Description: "Total transaction scripts executed and rejected"}) var logicErrTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_logic_err", Description: "Total transaction scripts executed and errored"}) +var logicCostTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_logic_cost", Description: "Total cost of transaction scripts executed"}) +var msigLessOrEqual4 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_4", Description: "Total transactions with 1-4 msigs"}) +var msigLessOrEqual10 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_5_10", Description: "Total transactions with 5-10 msigs"}) +var msigMore10 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_16", Description: "Total transactions with 11+ msigs"}) +var msigLsigLessOrEqual4 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_lsig_4", Description: "Total transaction scripts with 1-4 msigs"}) +var msigLsigLessOrEqual10 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_lsig_5_10", Description: "Total transaction scripts with 5-10 msigs"}) +var msigLsigMore10 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_lsig_10", Description: "Total transaction scripts with 11+ msigs"}) // The PaysetGroups is taking large set of transaction groups and attempt to verify their validity using multiple go-routines. // When doing so, it attempts to break these into smaller "worksets" where each workset takes about 2ms of execution time in order -// to avoid context switching overhead while providing good validation cancelation responsiveness. Each one of these worksets is +// to avoid context switching overhead while providing good validation cancellation responsiveness. Each one of these worksets is // "populated" with roughly txnPerWorksetThreshold transactions. ( note that the real evaluation time is unknown, but benchmarks // show that these are realistic numbers ) const txnPerWorksetThreshold = 32 @@ -48,7 +55,7 @@ const txnPerWorksetThreshold = 32 // purposes : // - if the verification task need to be aborted, there are only concurrentWorksets entries that are currently redundant on the execution pool queue. // - that number of concurrent tasks would not get beyond the capacity of the execution pool back buffer. -// - if we were to "redundantly" execute all these during context cancelation, we would spent at most 2ms * 16 = 32ms time. +// - if we were to "redundantly" execute all these during context cancellation, we would spent at most 2ms * 16 = 32ms time. // - it allows us to linearly scan the input, and process elements only once we're going to queue them into the pool. const concurrentWorksets = 16 @@ -98,20 +105,20 @@ const ( TxGroupErrorReasonNumValues ) -// ErrTxGroupError is an error from txn pre-validation (well form-ness, signature format, etc). +// TxGroupError is an error from txn pre-validation (well form-ness, signature format, etc). // It can be unwrapped into underlying error, as well as has a specific failure reason code. -type ErrTxGroupError struct { +type TxGroupError struct { err error Reason TxGroupErrorReason } // Error returns an error message from the underlying error -func (e *ErrTxGroupError) Error() string { +func (e *TxGroupError) Error() string { return e.err.Error() } // Unwrap returns an underlying error -func (e *ErrTxGroupError) Unwrap() error { +func (e *TxGroupError) Unwrap() error { return e.err } @@ -148,13 +155,13 @@ func (g *GroupContext) Equal(other *GroupContext) bool { // txnBatchPrep verifies a SignedTxn having no obviously inconsistent data. // Block-assembly time checks of LogicSig and accounting rules may still block the txn. // It is the caller responsibility to call batchVerifier.Verify(). -func txnBatchPrep(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, verifier *crypto.BatchVerifier) *ErrTxGroupError { +func txnBatchPrep(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, verifier *crypto.BatchVerifier) *TxGroupError { if !groupCtx.consensusParams.SupportRekeying && (s.AuthAddr != basics.Address{}) { - return &ErrTxGroupError{err: errRekeyingNotSupported, Reason: TxGroupErrorReasonGeneric} + return &TxGroupError{err: errRekeyingNotSupported, Reason: TxGroupErrorReasonGeneric} } if err := s.Txn.WellFormed(groupCtx.specAddrs, groupCtx.consensusParams); err != nil { - return &ErrTxGroupError{err: err, Reason: TxGroupErrorReasonNotWellFormed} + return &TxGroupError{err: err, Reason: TxGroupErrorReasonNotWellFormed} } return stxnCoreChecks(s, txnIdx, groupCtx, verifier) @@ -203,14 +210,14 @@ func txnGroupBatchPrep(stxs []transactions.SignedTxn, contextHdr bookkeeping.Blo } feeNeeded, overflow := basics.OMul(groupCtx.consensusParams.MinTxnFee, minFeeCount) if overflow { - err = &ErrTxGroupError{err: errTxGroupInvalidFee, Reason: TxGroupErrorReasonInvalidFee} + err = &TxGroupError{err: errTxGroupInvalidFee, Reason: TxGroupErrorReasonInvalidFee} return nil, err } // feesPaid may have saturated. That's ok. Since we know // feeNeeded did not overflow, simple comparison tells us // feesPaid was enough. if feesPaid < feeNeeded { - err = &ErrTxGroupError{ + err = &TxGroupError{ err: fmt.Errorf( "txgroup had %d in fees, which is less than the minimum %d * %d", feesPaid, minFeeCount, groupCtx.consensusParams.MinTxnFee), @@ -223,7 +230,7 @@ func txnGroupBatchPrep(stxs []transactions.SignedTxn, contextHdr bookkeeping.Blo } // stxnCoreChecks runs signatures validity checks and enqueues signature into batchVerifier for verification. -func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, batchVerifier *crypto.BatchVerifier) *ErrTxGroupError { +func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, batchVerifier *crypto.BatchVerifier) *TxGroupError { numSigs := 0 hasSig := false hasMsig := false @@ -248,10 +255,10 @@ func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContex if s.Txn.Sender == transactions.StateProofSender && s.Txn.Type == protocol.StateProofTx { return nil } - return &ErrTxGroupError{err: errTxnSigHasNoSig, Reason: TxGroupErrorReasonHasNoSig} + return &TxGroupError{err: errTxnSigHasNoSig, Reason: TxGroupErrorReasonHasNoSig} } if numSigs > 1 { - return &ErrTxGroupError{err: errTxnSigNotWellFormed, Reason: TxGroupErrorReasonSigNotWellFormed} + return &TxGroupError{err: errTxnSigNotWellFormed, Reason: TxGroupErrorReasonSigNotWellFormed} } if hasSig { @@ -260,17 +267,30 @@ func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContex } if hasMsig { if err := crypto.MultisigBatchPrep(s.Txn, crypto.Digest(s.Authorizer()), s.Msig, batchVerifier); err != nil { - return &ErrTxGroupError{err: fmt.Errorf("multisig validation failed: %w", err), Reason: TxGroupErrorReasonMsigNotWellFormed} + return &TxGroupError{err: fmt.Errorf("multisig validation failed: %w", err), Reason: TxGroupErrorReasonMsigNotWellFormed} + } + counter := 0 + for _, subsigi := range s.Msig.Subsigs { + if (subsigi.Sig != crypto.Signature{}) { + counter++ + } + } + if counter <= 4 { + msigLessOrEqual4.Inc(nil) + } else if counter <= 10 { + msigLessOrEqual10.Inc(nil) + } else { + msigMore10.Inc(nil) } return nil } if hasLogicSig { if err := logicSigVerify(s, txnIdx, groupCtx); err != nil { - return &ErrTxGroupError{err: err, Reason: TxGroupErrorReasonLogicSigFailed} + return &TxGroupError{err: err, Reason: TxGroupErrorReasonLogicSigFailed} } return nil } - return &ErrTxGroupError{err: errUnknownSignature, Reason: TxGroupErrorReasonGeneric} + return &TxGroupError{err: errUnknownSignature, Reason: TxGroupErrorReasonGeneric} } // LogicSigSanityCheck checks that the signature is valid and that the program is basically well formed. @@ -352,6 +372,19 @@ func logicSigSanityCheckBatchPrep(txn *transactions.SignedTxn, groupIndex int, g if err := crypto.MultisigBatchPrep(&program, crypto.Digest(txn.Authorizer()), lsig.Msig, batchVerifier); err != nil { return fmt.Errorf("logic multisig validation failed: %w", err) } + counter := 0 + for _, subsigi := range lsig.Msig.Subsigs { + if (subsigi.Sig != crypto.Signature{}) { + counter++ + } + } + if counter <= 4 { + msigLsigLessOrEqual4.Inc(nil) + } else if counter <= 10 { + msigLsigLessOrEqual10.Inc(nil) + } else { + msigLsigMore10.Inc(nil) + } } return nil } @@ -372,7 +405,7 @@ func logicSigVerify(txn *transactions.SignedTxn, groupIndex int, groupCtx *Group MinAvmVersion: &groupCtx.minAvmVersion, SigLedger: groupCtx.ledger, } - pass, err := logic.EvalSignature(groupIndex, &ep) + pass, cx, err := logic.EvalSignatureFull(groupIndex, &ep) if err != nil { logicErrTotal.Inc(nil) return fmt.Errorf("transaction %v: rejected by logic err=%v", txn.ID(), err) @@ -382,6 +415,7 @@ func logicSigVerify(txn *transactions.SignedTxn, groupIndex int, groupCtx *Group return fmt.Errorf("transaction %v: rejected by logic", txn.ID()) } logicGoodTotal.Inc(nil) + logicCostTotal.AddUint64(uint64(cx.Cost()), nil) return nil } diff --git a/data/transactions/verify/txn_test.go b/data/transactions/verify/txn_test.go index 8988f7aeae..3e23c8f352 100644 --- a/data/transactions/verify/txn_test.go +++ b/data/transactions/verify/txn_test.go @@ -609,7 +609,7 @@ func TestTxnGroupCacheUpdateFailLogic(t *testing.T) { _, signedTxn, _, _ := generateTestObjects(100, 20, 50) blkHdr := createDummyBlockHeader() - // sign the transcation with logic + // sign the transaction with logic for i := 0; i < len(signedTxn); i++ { // add a simple logic that verifies this condition: // sha256(arg0) == base64decode(5rZMNsevs5sULO+54aN+OvU6lQ503z2X+SSYUABIx7E=) @@ -637,8 +637,10 @@ byte base64 5rZMNsevs5sULO+54aN+OvU6lQ503z2X+SSYUABIx7E= restoreSignatureFunc := func(txn *transactions.SignedTxn) { txn.Lsig.Args[0][0]-- } + initCounter := logicCostTotal.GetUint64Value() verifyGroup(t, txnGroups, blkHdr, breakSignatureFunc, restoreSignatureFunc, "rejected by logic") - + currentCounter := logicCostTotal.GetUint64Value() + require.Greater(t, currentCounter, initCounter) } // TestTxnGroupCacheUpdateLogicWithSig makes sure that a payment transaction contains logicsig signed with single signature is valid (and added to the cache) only diff --git a/data/txDupCache.go b/data/txDupCache.go new file mode 100644 index 0000000000..026a168578 --- /dev/null +++ b/data/txDupCache.go @@ -0,0 +1,256 @@ +// Copyright (C) 2019-2022 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 data + +import ( + "context" + "encoding/binary" + "math" + "sync" + "time" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-deadlock" + + "golang.org/x/crypto/blake2b" +) + +// digestCache is a rotating cache of size N accepting crypto.Digest as a key +// and keeping up to 2*N elements in memory +type digestCache struct { + cur map[crypto.Digest]struct{} + prev map[crypto.Digest]struct{} + + maxSize int + mu deadlock.RWMutex +} + +func makeDigestCache(size int) *digestCache { + c := &digestCache{ + cur: map[crypto.Digest]struct{}{}, + maxSize: size, + } + return c +} + +// check if digest d is in a cache. +// locking semantic: write lock must be taken +func (c *digestCache) check(d *crypto.Digest) bool { + _, found := c.cur[*d] + if !found { + _, found = c.prev[*d] + } + return found +} + +// swap rotates cache pages. +// locking semantic: write lock must be taken +func (c *digestCache) swap() { + c.prev = c.cur + c.cur = map[crypto.Digest]struct{}{} +} + +// put adds digest d into a cache. +// locking semantic: write lock must be taken +func (c *digestCache) put(d *crypto.Digest) { + if len(c.cur) >= c.maxSize { + c.swap() + } + c.cur[*d] = struct{}{} +} + +// CheckAndPut adds digest d into a cache if not found +func (c *digestCache) CheckAndPut(d *crypto.Digest) bool { + c.mu.Lock() + defer c.mu.Unlock() + if c.check(d) { + return true + } + c.put(d) + return false +} + +// Len returns size of a cache +func (c *digestCache) Len() int { + c.mu.Lock() + defer c.mu.Unlock() + + return len(c.cur) + len(c.prev) +} + +// Delete from the cache +func (c *digestCache) Delete(d *crypto.Digest) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.cur, *d) + delete(c.prev, *d) +} + +// txSaltedCache is a digest cache with a rotating salt +// uses blake2b hash function +type txSaltedCache struct { + digestCache + + curSalt [4]byte + prevSalt [4]byte + ctx context.Context +} + +func makeSaltedCache(size int) *txSaltedCache { + return &txSaltedCache{ + digestCache: *makeDigestCache(size), + } +} + +func (c *txSaltedCache) start(ctx context.Context, refreshInterval time.Duration) { + c.ctx = ctx + if refreshInterval != 0 { + go c.salter(refreshInterval) + } + + c.mu.Lock() + defer c.mu.Unlock() + c.moreSalt() +} + +// salter is a goroutine refreshing the cache by schedule +func (c *txSaltedCache) salter(refreshInterval time.Duration) { + ticker := time.NewTicker(refreshInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + c.Remix() + case <-c.ctx.Done(): + return + } + } +} + +// moreSalt updates salt value used for hashing +func (c *txSaltedCache) moreSalt() { + r := uint32(crypto.RandUint64() % math.MaxUint32) + binary.LittleEndian.PutUint32(c.curSalt[:], r) +} + +// Remix is a locked version of innerSwap, called on schedule +func (c *txSaltedCache) Remix() { + c.mu.Lock() + defer c.mu.Unlock() + c.innerSwap(true) +} + +// innerSwap rotates cache pages and update the salt used. +// locking semantic: write lock must be held +func (c *txSaltedCache) innerSwap(scheduled bool) { + c.prevSalt = c.curSalt + c.prev = c.cur + + if scheduled { + // updating by timer, the prev size is a good estimation of a current load => preallocate + c.cur = make(map[crypto.Digest]struct{}, len(c.prev)) + } else { + // otherwise start empty + c.cur = map[crypto.Digest]struct{}{} + } + c.moreSalt() +} + +// innerCheck returns true if exists, and the current salted hash if does not. +// locking semantic: write lock must be held +func (c *txSaltedCache) innerCheck(msg []byte) (*crypto.Digest, bool) { + ptr := saltedPool.Get() + defer saltedPool.Put(ptr) + + buf := ptr.([]byte) + toBeHashed := append(buf[:0], msg...) + toBeHashed = append(toBeHashed, c.curSalt[:]...) + toBeHashed = toBeHashed[:len(msg)+len(c.curSalt)] + + d := crypto.Digest(blake2b.Sum256(toBeHashed)) + + _, found := c.cur[d] + if found { + return nil, true + } + + toBeHashed = append(toBeHashed[:len(msg)], c.prevSalt[:]...) + toBeHashed = toBeHashed[:len(msg)+len(c.prevSalt)] + pd := crypto.Digest(blake2b.Sum256(toBeHashed)) + _, found = c.prev[pd] + if found { + return nil, true + } + return &d, false +} + +// CheckAndPut adds msg into a cache if not found +// returns a hashing key used for insertion if the message not found. +func (c *txSaltedCache) CheckAndPut(msg []byte) (*crypto.Digest, bool) { + c.mu.RLock() + d, found := c.innerCheck(msg) + salt := c.curSalt + c.mu.RUnlock() + // fast read-only path: assuming most messages are duplicates, hash msg and check cache + if found { + return d, found + } + + // not found: acquire write lock to add this msg hash to cache + c.mu.Lock() + defer c.mu.Unlock() + // salt may have changed between RUnlock() and Lock(), rehash if needed + if salt != c.curSalt { + d, found = c.innerCheck(msg) + if found { + // already added to cache between RUnlock() and Lock(), return + return d, found + } + } + + if len(c.cur) >= c.maxSize { + c.innerSwap(false) + ptr := saltedPool.Get() + defer saltedPool.Put(ptr) + + buf := ptr.([]byte) + toBeHashed := append(buf[:0], msg...) + toBeHashed = append(toBeHashed, c.curSalt[:]...) + toBeHashed = toBeHashed[:len(msg)+len(c.curSalt)] + + dn := crypto.Digest(blake2b.Sum256(toBeHashed)) + d = &dn + } + + c.cur[*d] = struct{}{} + return d, false +} + +// DeleteByKey from the cache by using a key used for insertion +func (c *txSaltedCache) DeleteByKey(d *crypto.Digest) { + c.digestCache.Delete(d) +} + +var saltedPool = sync.Pool{ + New: func() interface{} { + // 2 x MaxAvailableAppProgramLen that covers + // max approve + clear state programs with max args for app create txn. + // other transactions are much smaller. + return make([]byte, 2*config.MaxAvailableAppProgramLen) + }, +} diff --git a/data/txDupCache_test.go b/data/txDupCache_test.go new file mode 100644 index 0000000000..e0bfac25a5 --- /dev/null +++ b/data/txDupCache_test.go @@ -0,0 +1,373 @@ +// Copyright (C) 2019-2022 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 data + +import ( + "context" + "fmt" + "math/rand" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/algorand/go-deadlock" + + "golang.org/x/crypto/blake2b" +) + +func TestTxHandlerDigestCache(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + const size = 20 + cache := makeDigestCache(size) + require.Zero(t, cache.Len()) + + // add some unique random + var ds [size]crypto.Digest + for i := 0; i < size; i++ { + crypto.RandBytes([]byte(ds[i][:])) + exist := cache.CheckAndPut(&ds[i]) + require.False(t, exist) + + exist = cache.check(&ds[i]) + require.True(t, exist) + } + + require.Equal(t, size, cache.Len()) + + // try to re-add, ensure not added + for i := 0; i < size; i++ { + exist := cache.CheckAndPut(&ds[i]) + require.True(t, exist) + } + + require.Equal(t, size, cache.Len()) + + // add some more and ensure capacity switch + var ds2 [size]crypto.Digest + for i := 0; i < size; i++ { + crypto.RandBytes(ds2[i][:]) + exist := cache.CheckAndPut(&ds2[i]) + require.False(t, exist) + + exist = cache.check(&ds2[i]) + require.True(t, exist) + } + + require.Equal(t, 2*size, cache.Len()) + + var d crypto.Digest + crypto.RandBytes(d[:]) + exist := cache.CheckAndPut(&d) + require.False(t, exist) + exist = cache.check(&d) + require.True(t, exist) + + require.Equal(t, size+1, cache.Len()) + + // ensure hashes from the prev batch are still there + for i := 0; i < size; i++ { + exist := cache.check(&ds2[i]) + require.True(t, exist) + } + + // ensure hashes from the first batch are gone + for i := 0; i < size; i++ { + exist := cache.check(&ds[i]) + require.False(t, exist) + } + + // check deletion works + for i := 0; i < size; i++ { + cache.Delete(&ds[i]) + cache.Delete(&ds2[i]) + } + + require.Equal(t, 1, cache.Len()) + + cache.Delete(&d) + require.Equal(t, 0, cache.Len()) +} + +func (c *txSaltedCache) check(msg []byte) bool { + _, found := c.innerCheck(msg) + return found +} + +// TestTxHandlerSaltedCacheBasic is the same as TestTxHandlerDigestCache but for the salted cache +func TestTxHandlerSaltedCacheBasic(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + const size = 20 + cache := makeSaltedCache(size) + cache.start(context.Background(), 0) + require.Zero(t, cache.Len()) + + // add some unique random + var ds [size][8]byte + var ks [size]*crypto.Digest + var exist bool + for i := 0; i < size; i++ { + crypto.RandBytes([]byte(ds[i][:])) + ks[i], exist = cache.CheckAndPut(ds[i][:]) + require.False(t, exist) + require.NotEmpty(t, ks[i]) + + exist = cache.check(ds[i][:]) + require.True(t, exist) + } + + require.Equal(t, size, cache.Len()) + + // try to re-add, ensure not added + for i := 0; i < size; i++ { + k, exist := cache.CheckAndPut(ds[i][:]) + require.True(t, exist) + require.Empty(t, k) + } + + require.Equal(t, size, cache.Len()) + + // add some more and ensure capacity switch + var ds2 [size][8]byte + var ks2 [size]*crypto.Digest + for i := 0; i < size; i++ { + crypto.RandBytes(ds2[i][:]) + ks2[i], exist = cache.CheckAndPut(ds2[i][:]) + require.False(t, exist) + require.NotEmpty(t, ks2[i]) + + exist = cache.check(ds2[i][:]) + require.True(t, exist) + } + + require.Equal(t, 2*size, cache.Len()) + + var d [8]byte + crypto.RandBytes(d[:]) + k, exist := cache.CheckAndPut(d[:]) + require.False(t, exist) + require.NotEmpty(t, k) + exist = cache.check(d[:]) + require.True(t, exist) + + require.Equal(t, size+1, cache.Len()) + + // ensure hashes from the prev batch are still there + for i := 0; i < size; i++ { + exist := cache.check(ds2[i][:]) + require.True(t, exist) + } + + // ensure hashes from the first batch are gone + for i := 0; i < size; i++ { + exist := cache.check(ds[i][:]) + require.False(t, exist) + } + + // check deletion works + for i := 0; i < size; i++ { + cache.DeleteByKey(ks[i]) + cache.DeleteByKey(ks2[i]) + } + + require.Equal(t, 1, cache.Len()) + + cache.DeleteByKey(k) + require.Equal(t, 0, cache.Len()) +} + +func TestTxHandlerSaltedCacheScheduled(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + const size = 20 + updateInterval := 1000 * time.Microsecond + cache := makeSaltedCache(size) + cache.start(context.Background(), updateInterval) + require.Zero(t, cache.Len()) + + // add some unique random + var ds [size][8]byte + for i := 0; i < size; i++ { + crypto.RandBytes([]byte(ds[i][:])) + k, exist := cache.CheckAndPut(ds[i][:]) + require.False(t, exist) + require.NotEmpty(t, k) + + if rand.Int()%2 == 0 { + time.Sleep(updateInterval / 2) + } + } + + require.Less(t, cache.Len(), size) +} + +func TestTxHandlerSaltedCacheManual(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + const size = 20 + cache := makeSaltedCache(2 * size) + cache.start(context.Background(), 0) + require.Zero(t, cache.Len()) + + // add some unique random + var ds [size][8]byte + for i := 0; i < size; i++ { + crypto.RandBytes([]byte(ds[i][:])) + k, exist := cache.CheckAndPut(ds[i][:]) + require.False(t, exist) + require.NotEmpty(t, k) + exist = cache.check(ds[i][:]) + require.True(t, exist) + } + + require.Equal(t, size, cache.Len()) + + // rotate and add more data + cache.Remix() + + var ds2 [size][8]byte + for i := 0; i < size; i++ { + crypto.RandBytes([]byte(ds2[i][:])) + k, exist := cache.CheckAndPut(ds2[i][:]) + require.False(t, exist) + require.NotEmpty(t, k) + exist = cache.check(ds2[i][:]) + require.True(t, exist) + } + require.Equal(t, 2*size, cache.Len()) + + // ensure the old data still in + for i := 0; i < size; i++ { + exist := cache.check(ds[i][:]) + require.True(t, exist) + } + + // rotate again, check only new data left + cache.Remix() + + require.Equal(t, size, cache.Len()) + for i := 0; i < size; i++ { + exist := cache.check(ds[i][:]) + require.False(t, exist) + exist = cache.check(ds2[i][:]) + require.True(t, exist) + } +} + +// benchmark abstractions +type cachePusher interface { + push() +} + +type cacheMaker interface { + make(size int) cachePusher +} + +type digestCacheMaker struct{} +type saltedCacheMaker struct{} + +func (m digestCacheMaker) make(size int) cachePusher { + return &digestCachePusher{c: makeDigestCache(size)} +} +func (m saltedCacheMaker) make(size int) cachePusher { + scp := &saltedCachePusher{c: makeSaltedCache(size)} + scp.c.start(context.Background(), 0) + return scp +} + +type digestCachePusher struct { + c *digestCache +} +type saltedCachePusher struct { + c *txSaltedCache +} + +func (p *digestCachePusher) push() { + var d [crypto.DigestSize]byte + crypto.RandBytes(d[:]) + h := crypto.Digest(blake2b.Sum256(d[:])) // digestCache does not hashes so calculate hash here + p.c.CheckAndPut(&h) +} + +func (p *saltedCachePusher) push() { + var d [crypto.DigestSize]byte + crypto.RandBytes(d[:]) + p.c.CheckAndPut(d[:]) // saltedCache hashes inside +} + +func BenchmarkDigestCaches(b *testing.B) { + deadlockDisable := deadlock.Opts.Disable + deadlock.Opts.Disable = true + defer func() { + deadlock.Opts.Disable = deadlockDisable + }() + + digestCacheMaker := digestCacheMaker{} + saltedCacheMaker := saltedCacheMaker{} + var benchmarks = []struct { + maker cacheMaker + numThreads int + }{ + {digestCacheMaker, 1}, + {saltedCacheMaker, 1}, + {digestCacheMaker, 4}, + {saltedCacheMaker, 4}, + {digestCacheMaker, 16}, + {saltedCacheMaker, 16}, + {digestCacheMaker, 128}, + {saltedCacheMaker, 128}, + } + for _, bench := range benchmarks { + b.Run(fmt.Sprintf("%T/threads=%d", bench.maker, bench.numThreads), func(b *testing.B) { + benchmarkDigestCache(b, bench.maker, bench.numThreads) + }) + } +} + +func calcCacheSize(numIter int) int { + size := numIter / 3 // in order to exercise map swaps + if size == 0 { + size++ + } + return size +} + +func benchmarkDigestCache(b *testing.B, m cacheMaker, numThreads int) { + p := m.make(calcCacheSize(b.N)) + numHashes := b.N / numThreads // num hashes per goroutine + // b.Logf("inserting %d (%d) values in %d threads into cache of size %d", b.N, numHashes, numThreads, calcCacheSize(b.N)) + var wg sync.WaitGroup + wg.Add(numThreads) + for i := 0; i < numThreads; i++ { + go func() { + defer wg.Done() + for j := 0; j < numHashes; j++ { + p.push() + } + }() + } + wg.Wait() +} diff --git a/data/txHandler.go b/data/txHandler.go index 8580e220f4..59bb572e8e 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -31,6 +31,7 @@ import ( "github.com/algorand/go-algorand/data/pools" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/verify" + "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/protocol" @@ -56,17 +57,52 @@ var transactionMessagesTxnLogicSig = metrics.MakeCounter(metrics.TransactionMess var transactionMessagesTxnSigVerificationFailed = metrics.MakeCounter(metrics.TransactionMessagesTxnSigVerificationFailed) var transactionMessagesBacklogErr = metrics.MakeCounter(metrics.TransactionMessagesBacklogErr) var transactionMessagesRemember = metrics.MakeCounter(metrics.TransactionMessagesRemember) +var transactionMessageTxGroupExcessive = metrics.MakeCounter(metrics.TransactionMessageTxGroupExcessive) +var transactionMessageTxGroupFull = metrics.MakeCounter(metrics.TransactionMessageTxGroupFull) +var transactionMessagesDupRawMsg = metrics.MakeCounter(metrics.TransactionMessagesDupRawMsg) +var transactionMessagesDupCanonical = metrics.MakeCounter(metrics.TransactionMessagesDupCanonical) var transactionMessagesBacklogSizeGauge = metrics.MakeGauge(metrics.TransactionMessagesBacklogSize) var transactionGroupTxSyncHandled = metrics.MakeCounter(metrics.TransactionGroupTxSyncHandled) var transactionGroupTxSyncRemember = metrics.MakeCounter(metrics.TransactionGroupTxSyncRemember) var transactionGroupTxSyncAlreadyCommitted = metrics.MakeCounter(metrics.TransactionGroupTxSyncAlreadyCommitted) +var transactionMessageTxPoolRememberCounter = metrics.NewTagCounter( + "algod_transaction_messages_txpool_remember_{TAG}", "Number of transaction messages not remembered by txpool b/c if {TAG}", + txPoolRememberTagCap, txPoolRememberPendingEval, txPoolRememberTagNoSpace, txPoolRememberTagFee, txPoolRememberTagTxnDead, txPoolRememberTagTooLarge, txPoolRememberTagGroupID, + txPoolRememberTagTxID, txPoolRememberTagLease, txPoolRememberTagTxIDEval, txPoolRememberTagLeaseEval, txPoolRememberTagEvalGeneric, +) + +var transactionMessageTxPoolCheckCounter = metrics.NewTagCounter( + "algod_transaction_messages_txpool_check_{TAG}", "Number of transaction messages that didn't pass check by txpool b/c if {TAG}", + txPoolRememberTagTxnNotWellFormed, txPoolRememberTagTxnDead, txPoolRememberTagTooLarge, txPoolRememberTagGroupID, + txPoolRememberTagTxID, txPoolRememberTagLease, txPoolRememberTagTxIDEval, txPoolRememberTagLeaseEval, txPoolRememberTagEvalGeneric, +) + +const ( + txPoolRememberTagCap = "cap" + txPoolRememberPendingEval = "pending_eval" + txPoolRememberTagNoSpace = "no_space" + txPoolRememberTagFee = "fee" + txPoolRememberTagTxnDead = "txn_dead" + txPoolRememberTagTooLarge = "too_large" + txPoolRememberTagGroupID = "groupid" + txPoolRememberTagTxID = "txid" + txPoolRememberTagLease = "lease" + txPoolRememberTagTxIDEval = "txid_eval" + txPoolRememberTagLeaseEval = "lease_eval" + txPoolRememberTagEvalGeneric = "eval" + + txPoolRememberTagTxnNotWellFormed = "not_well" +) + // The txBacklogMsg structure used to track a single incoming transaction from the gossip network, type txBacklogMsg struct { - rawmsg *network.IncomingMessage // the raw message from the network - unverifiedTxGroup []transactions.SignedTxn // the unverified ( and signed ) transaction group - verificationErr error // The verification error generated by the verification function, if any. + rawmsg *network.IncomingMessage // the raw message from the network + unverifiedTxGroup []transactions.SignedTxn // the unverified ( and signed ) transaction group + rawmsgDataHash *crypto.Digest // hash (if any) of raw message data from the network + unverifiedTxGroupHash *crypto.Digest // hash (if any) of the unverifiedTxGroup + verificationErr error // The verification error generated by the verification function, if any. } // TxHandler handles transaction messages @@ -80,32 +116,55 @@ type TxHandler struct { postVerificationQueue chan *txBacklogMsg backlogWg sync.WaitGroup net network.GossipNode + msgCache *txSaltedCache + txCanonicalCache *digestCache + cacheConfig txHandlerConfig ctx context.Context ctxCancel context.CancelFunc } +// TxHandlerOpts is TxHandler configuration options +type TxHandlerOpts struct { + TxPool *pools.TransactionPool + ExecutionPool execpool.BacklogPool + Ledger *Ledger + Net network.GossipNode + GenesisID string + GenesisHash crypto.Digest + Config config.Local +} + +// txHandlerConfig is a subset of tx handler related options from config.Local +type txHandlerConfig struct { + enableFilteringRawMsg bool + enableFilteringCanonical bool +} + // MakeTxHandler makes a new handler for transaction messages -func MakeTxHandler(txPool *pools.TransactionPool, ledger *Ledger, net network.GossipNode, genesisID string, genesisHash crypto.Digest, executionPool execpool.BacklogPool) *TxHandler { +func MakeTxHandler(opts TxHandlerOpts) *TxHandler { - if txPool == nil { + if opts.TxPool == nil { logging.Base().Fatal("MakeTxHandler: txPool is nil on initialization") return nil } - if ledger == nil { + if opts.Ledger == nil { logging.Base().Fatal("MakeTxHandler: ledger is nil on initialization") return nil } handler := &TxHandler{ - txPool: txPool, - genesisID: genesisID, - genesisHash: genesisHash, - ledger: ledger, - txVerificationPool: executionPool, + txPool: opts.TxPool, + genesisID: opts.GenesisID, + genesisHash: opts.GenesisHash, + ledger: opts.Ledger, + txVerificationPool: opts.ExecutionPool, backlogQueue: make(chan *txBacklogMsg, txBacklogSize), postVerificationQueue: make(chan *txBacklogMsg, txBacklogSize), - net: net, + net: opts.Net, + msgCache: makeSaltedCache(2 * txBacklogSize), + txCanonicalCache: makeDigestCache(2 * txBacklogSize), + cacheConfig: txHandlerConfig{opts.Config.TxFilterRawMsgEnabled(), opts.Config.TxFilterCanonicalEnabled()}, } return handler } @@ -113,6 +172,7 @@ func MakeTxHandler(txPool *pools.TransactionPool, ledger *Ledger, net network.Go // Start enables the processing of incoming messages at the transaction handler func (handler *TxHandler) Start() { handler.ctx, handler.ctxCancel = context.WithCancel(context.Background()) + handler.msgCache.start(handler.ctx, 60*time.Second) handler.net.RegisterHandlers([]network.TaggedMessageHandler{ {Tag: protocol.TxnTag, MessageHandler: network.HandlerFunc(handler.processIncomingTxn)}, }) @@ -201,7 +261,7 @@ func (handler *TxHandler) postProcessReportErrors(err error) { return } - var txGroupErr *verify.ErrTxGroupError + var txGroupErr *verify.TxGroupError if errors.As(err, &txGroupErr) { switch txGroupErr.Reason { case verify.TxGroupErrorReasonNotWellFormed: @@ -224,6 +284,99 @@ func (handler *TxHandler) postProcessReportErrors(err error) { } } +func (handler *TxHandler) checkReportErrors(err error) { + switch err := err.(type) { + case *ledgercore.TxnNotWellFormedError: + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagTxnNotWellFormed, 1) + return + case *transactions.TxnDeadError: + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagTxnDead, 1) + return + case *ledgercore.TransactionInLedgerError: + if err.InBlockEvaluator { + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagTxIDEval, 1) + } else { + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagTxID, 1) + } + return + case *ledgercore.LeaseInLedgerError: + if err.InBlockEvaluator { + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagLeaseEval, 1) + } else { + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagLease, 1) + } + return + case *ledgercore.TxGroupMalformedError: + switch err.Reason { + case ledgercore.TxGroupMalformedErrorReasonExceedMaxSize: + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagTooLarge, 1) + default: + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagGroupID, 1) + } + return + } + + transactionMessageTxPoolCheckCounter.Add(txPoolRememberTagEvalGeneric, 1) +} + +func (handler *TxHandler) rememberReportErrors(err error) { + if errors.Is(err, pools.ErrPendingQueueReachedMaxCap) { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagCap, 1) + return + } + + if errors.Is(err, pools.ErrNoPendingBlockEvaluator) { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberPendingEval, 1) + return + } + + if errors.Is(err, ledgercore.ErrNoSpace) { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagNoSpace, 1) + return + } + + // it is possible to call errors.As but it requires additional allocations + // instead, unwrap and type assert. + underlyingErr := errors.Unwrap(err) + if underlyingErr == nil { + // something went wrong + return + } + + switch err := underlyingErr.(type) { + case *pools.ErrTxPoolFeeError: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagFee, 1) + return + case *transactions.TxnDeadError: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagTxnDead, 1) + return + case *ledgercore.TransactionInLedgerError: + if err.InBlockEvaluator { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagTxIDEval, 1) + } else { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagTxID, 1) + } + return + case *ledgercore.LeaseInLedgerError: + if err.InBlockEvaluator { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagLeaseEval, 1) + } else { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagLease, 1) + } + return + case *ledgercore.TxGroupMalformedError: + switch err.Reason { + case ledgercore.TxGroupMalformedErrorReasonExceedMaxSize: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagTooLarge, 1) + default: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagGroupID, 1) + } + return + } + + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagEvalGeneric, 1) +} + func (handler *TxHandler) postProcessCheckedTxn(wi *txBacklogMsg) { if wi.verificationErr != nil { // disconnect from peer. @@ -242,6 +395,7 @@ func (handler *TxHandler) postProcessCheckedTxn(wi *txBacklogMsg) { // save the transaction, if it has high enough fee and not already in the cache err := handler.txPool.Remember(verifiedTxGroup) if err != nil { + handler.rememberReportErrors(err) logging.Base().Debugf("could not remember tx: %v", err) return } @@ -279,46 +433,156 @@ func (handler *TxHandler) asyncVerifySignature(arg interface{}) interface{} { // we failed to write to the output queue, since the queue was full. // adding the metric here allows us to monitor how frequently it happens. transactionMessagesDroppedFromPool.Inc(nil) + + // delete from duplicate caches to give it chance to be re-submitted + // this relatively rare operation and implementation is expensive (requires re-hashing) + handler.deleteFromCaches(tx.rawmsgDataHash, tx.unverifiedTxGroupHash) + } return nil } +func (handler *TxHandler) deleteFromCaches(msgKey *crypto.Digest, canonicalKey *crypto.Digest) { + if handler.cacheConfig.enableFilteringCanonical && canonicalKey != nil { + handler.txCanonicalCache.Delete(canonicalKey) + } + + if handler.cacheConfig.enableFilteringRawMsg && msgKey != nil { + handler.msgCache.DeleteByKey(msgKey) + } +} + +// dedupCanonical checks if the transaction group has been seen before after reencoding to canonical representation. +// returns a key used for insertion if the group was not found. +func (handler *TxHandler) dedupCanonical(ntx int, unverifiedTxGroup []transactions.SignedTxn, consumed int) (key *crypto.Digest, isDup bool) { + // consider situations where someone want to censor transactions A + // 1. Txn A is not part of a group => txn A with a valid signature is OK + // Censorship attempts are: + // - txn A with an invalid signature => cache/dedup canonical txn with its signature + // - txn A with a valid/invalid signature and part of a valid or invalid group => cache/dedup the entire group + // + // 2. Txn A is part of a group => txn A with valid GroupID and signature is OK + // Censorship attempts are: + // - txn A with a valid or invalid signature => cache/dedup canonical txn with its signature. + // - txn A as part of a group => cache/dedup the entire group + // + // caching approaches that would not work: + // - using txid: {A} could be poisoned by {A, B} where B is invalid + // - using individual txn from a group: {A, Z} could be poisoned by {A, B}, where B is invalid + + var d crypto.Digest + if ntx == 1 { + // a single transaction => cache/dedup canonical txn with its signature + enc := unverifiedTxGroup[0].MarshalMsg(nil) + d = crypto.Hash(enc) + if handler.txCanonicalCache.CheckAndPut(&d) { + return nil, true + } + } else { + // a transaction group => cache/dedup the entire group canonical group + encodeBuf := make([]byte, 0, unverifiedTxGroup[0].Msgsize()*ntx) + for i := range unverifiedTxGroup { + encodeBuf = unverifiedTxGroup[i].MarshalMsg(encodeBuf) + } + if len(encodeBuf) != consumed { + // reallocated, some assumption on size was wrong + // log and skip + logging.Base().Warnf("Decoded size %d does not match to encoded %d", consumed, len(encodeBuf)) + return nil, false + } + d = crypto.Hash(encodeBuf) + if handler.txCanonicalCache.CheckAndPut(&d) { + return nil, true + } + } + return &d, false +} + +// processIncomingTxn decodes a transaction group from incoming message and enqueues into the back log for processing. +// The function also performs some input data pre-validation; +// - txn groups are cut to MaxTxGroupSize size +// - message are checked for duplicates +// - transactions are checked for duplicates + func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) network.OutgoingMessage { + var msgKey *crypto.Digest + var isDup bool + if handler.cacheConfig.enableFilteringRawMsg { + // check for duplicate messages + // this helps against relaying duplicates + if msgKey, isDup = handler.msgCache.CheckAndPut(rawmsg.Data); isDup { + transactionMessagesDupRawMsg.Inc(nil) + return network.OutgoingMessage{Action: network.Ignore} + } + } + + unverifiedTxGroup := make([]transactions.SignedTxn, 1) dec := protocol.NewMsgpDecoderBytes(rawmsg.Data) ntx := 0 - unverifiedTxGroup := make([]transactions.SignedTxn, 1) + consumed := 0 for { if len(unverifiedTxGroup) == ntx { n := make([]transactions.SignedTxn, len(unverifiedTxGroup)*2) copy(n, unverifiedTxGroup) unverifiedTxGroup = n } - err := dec.Decode(&unverifiedTxGroup[ntx]) - if err == io.EOF { - break - } if err != nil { + if err == io.EOF { + break + } logging.Base().Warnf("Received a non-decodable txn: %v", err) return network.OutgoingMessage{Action: network.Disconnect} } + consumed = dec.Consumed() ntx++ + if ntx >= config.MaxTxGroupSize { + // max ever possible group size reached, done reading input. + if dec.Remaining() > 0 { + // if something else left in the buffer - this is an error, drop + transactionMessageTxGroupExcessive.Inc(nil) + return network.OutgoingMessage{Action: network.Disconnect} + } + } } if ntx == 0 { logging.Base().Warnf("Received empty tx group") return network.OutgoingMessage{Action: network.Disconnect} } + unverifiedTxGroup = unverifiedTxGroup[:ntx] + if ntx == config.MaxTxGroupSize { + transactionMessageTxGroupFull.Inc(nil) + } + + var canonicalKey *crypto.Digest + if handler.cacheConfig.enableFilteringCanonical { + if canonicalKey, isDup = handler.dedupCanonical(ntx, unverifiedTxGroup, consumed); isDup { + transactionMessagesDupCanonical.Inc(nil) + return network.OutgoingMessage{Action: network.Ignore} + } + } + select { case handler.backlogQueue <- &txBacklogMsg{ - rawmsg: &rawmsg, - unverifiedTxGroup: unverifiedTxGroup, + rawmsg: &rawmsg, + unverifiedTxGroup: unverifiedTxGroup, + rawmsgDataHash: msgKey, + unverifiedTxGroupHash: canonicalKey, }: default: // if we failed here we want to increase the corresponding metric. It might suggest that we // want to increase the queue size. transactionMessagesDroppedFromBacklog.Inc(nil) + + // additionally, remove the txn from duplicate caches to ensure it can be re-submitted + if canonicalKey != nil { + handler.txCanonicalCache.Delete(canonicalKey) + } + if msgKey != nil { + handler.msgCache.DeleteByKey(msgKey) + } } return network.OutgoingMessage{Action: network.Ignore} @@ -341,6 +605,7 @@ func (handler *TxHandler) checkAlreadyCommitted(tx *txBacklogMsg) (processingDon // do a quick test to check that this transaction could potentially be committed, to reject dup pending transactions err := handler.txPool.Test(tx.unverifiedTxGroup) if err != nil { + handler.checkReportErrors(err) logging.Base().Debugf("txPool rejected transaction: %v", err) return true } diff --git a/data/txHandler_test.go b/data/txHandler_test.go index 46ea215627..389eb6d2be 100644 --- a/data/txHandler_test.go +++ b/data/txHandler_test.go @@ -23,6 +23,9 @@ import ( "fmt" "io" "math/rand" + "os" + "runtime" + "runtime/pprof" "strings" "sync" "testing" @@ -30,6 +33,7 @@ import ( "github.com/stretchr/testify/require" + "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" @@ -38,21 +42,21 @@ import ( "github.com/algorand/go-algorand/data/pools" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/verify" + realledger "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/execpool" "github.com/algorand/go-algorand/util/metrics" + + "github.com/algorand/go-deadlock" ) -func BenchmarkTxHandlerProcessing(b *testing.B) { - const numUsers = 100 - log := logging.TestingLog(b) - log.SetLevel(logging.Warn) - secrets := make([]*crypto.SignatureSecrets, numUsers) +func makeTestGenesisAccounts(tb require.TestingT, numUsers int) ([]basics.Address, []*crypto.SignatureSecrets, map[basics.Address]basics.AccountData) { addresses := make([]basics.Address, numUsers) - + secrets := make([]*crypto.SignatureSecrets, numUsers) genesis := make(map[basics.Address]basics.AccountData) for i := 0; i < numUsers; i++ { secret := keypair() @@ -70,7 +74,16 @@ func BenchmarkTxHandlerProcessing(b *testing.B) { MicroAlgos: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinBalance}, } - require.Equal(b, len(genesis), numUsers+1) + require.Equal(tb, len(genesis), numUsers+1) + return addresses, secrets, genesis +} + +func BenchmarkTxHandlerProcessing(b *testing.B) { + const numUsers = 100 + log := logging.TestingLog(b) + log.SetLevel(logging.Warn) + + addresses, secrets, genesis := makeTestGenesisAccounts(b, numUsers) genBal := bookkeeping.MakeGenesisBalances(genesis, sinkAddr, poolAddr) ledgerName := fmt.Sprintf("%s-mem-%d", b.Name(), b.N) const inMem = true @@ -83,9 +96,7 @@ func BenchmarkTxHandlerProcessing(b *testing.B) { cfg.TxPoolSize = 75000 cfg.EnableProcessBlockStats = false - tp := pools.MakeTransactionPool(l.Ledger, cfg, logging.Base()) - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - txHandler := MakeTxHandler(tp, l, &mocks.MockNetwork{}, "", crypto.Digest{}, backlogPool) + txHandler := makeTestTxHandler(l, cfg) makeTxns := func(N int) [][]transactions.SignedTxn { ret := make([][]transactions.SignedTxn, 0, N) @@ -198,11 +209,10 @@ func makeRandomTransactions(num int) ([]transactions.SignedTxn, []byte) { func TestTxHandlerProcessIncomingTxn(t *testing.T) { partitiontest.PartitionTest(t) + t.Parallel() const numTxns = 11 - handler := TxHandler{ - backlogQueue: make(chan *txBacklogMsg, 1), - } + handler := makeTestTxHandlerOrphaned(1) stxns, blob := makeRandomTransactions(numTxns) action := handler.processIncomingTxn(network.IncomingMessage{Data: blob}) require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) @@ -215,6 +225,768 @@ func TestTxHandlerProcessIncomingTxn(t *testing.T) { } } +// BenchmarkTxHandlerProcessIncomingTxn is single-threaded ProcessIncomingTxn benchmark +func BenchmarkTxHandlerProcessIncomingTxn(b *testing.B) { + deadlockDisable := deadlock.Opts.Disable + deadlock.Opts.Disable = true + defer func() { + deadlock.Opts.Disable = deadlockDisable + }() + + const numTxnsPerGroup = 16 + handler := makeTestTxHandlerOrphaned(txBacklogSize) + + // prepare tx groups + blobs := make([][]byte, b.N) + stxns := make([][]transactions.SignedTxn, b.N) + for i := 0; i < b.N; i++ { + stxns[i], blobs[i] = makeRandomTransactions(numTxnsPerGroup) + } + + ctx, cancelFun := context.WithCancel(context.Background()) + + // start consumer + var wg sync.WaitGroup + wg.Add(1) + go func(ctx context.Context, n int) { + defer wg.Done() + outer: + for i := 0; i < n; i++ { + select { + case <-ctx.Done(): + break outer + default: + } + msg := <-handler.backlogQueue + require.Equal(b, numTxnsPerGroup, len(msg.unverifiedTxGroup)) + } + }(ctx, b.N) + + // submit tx groups + b.ResetTimer() + for i := 0; i < b.N; i++ { + action := handler.processIncomingTxn(network.IncomingMessage{Data: blobs[i]}) + require.Equal(b, network.OutgoingMessage{Action: network.Ignore}, action) + } + cancelFun() + wg.Wait() +} + +func ipow(x, n int) int { + var res int = 1 + for n != 0 { + if n&1 != 0 { + res *= x + } + n >>= 1 + x *= x + } + return res +} + +func TestPow(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + require.Equal(t, 1, ipow(10, 0)) + require.Equal(t, 10, ipow(10, 1)) + require.Equal(t, 100, ipow(10, 2)) + require.Equal(t, 8, ipow(2, 3)) +} + +func getNumBacklogDropped() int { + return int(transactionMessagesDroppedFromBacklog.GetUint64Value()) +} + +func getNumRawMsgDup() int { + return int(transactionMessagesDupRawMsg.GetUint64Value()) +} + +func getNumCanonicalDup() int { + return int(transactionMessagesDupCanonical.GetUint64Value()) +} + +type benchFinalize func() + +func benchTxHandlerProcessIncomingTxnSubmit(b *testing.B, handler *TxHandler, blobs [][]byte, numThreads int) benchFinalize { + // submit tx groups + var wgp sync.WaitGroup + wgp.Add(numThreads) + + hashesPerThread := b.N / numThreads + if hashesPerThread == 0 { + hashesPerThread = 1 + } + + finalize := func() {} + + if b.N == 100001 { + profpath := b.Name() + "_cpuprof.pprof" + profout, err := os.Create(profpath) + if err != nil { + b.Fatal(err) + return finalize + } + b.Logf("%s: cpu profile for b.N=%d", profpath, b.N) + pprof.StartCPUProfile(profout) + + finalize = func() { + pprof.StopCPUProfile() + profout.Close() + } + } + + for g := 0; g < numThreads; g++ { + start := g * hashesPerThread + end := (g + 1) * (hashesPerThread) + // workaround for trivial runs with b.N = 1 + if start >= b.N { + start = 0 + } + if end >= b.N { + end = b.N + } + // handle the remaining blobs + if g == numThreads-1 { + end = b.N + } + // b.Logf("%d: %d %d", b.N, start, end) + go func(start int, end int) { + defer wgp.Done() + for i := start; i < end; i++ { + action := handler.processIncomingTxn(network.IncomingMessage{Data: blobs[i]}) + require.Equal(b, network.OutgoingMessage{Action: network.Ignore}, action) + } + }(start, end) + } + wgp.Wait() + + return finalize +} + +func benchTxHandlerProcessIncomingTxnConsume(b *testing.B, handler *TxHandler, numTxnsPerGroup int, avgDelay time.Duration, statsCh chan<- [4]int) benchFinalize { + droppedStart := getNumBacklogDropped() + dupStart := getNumRawMsgDup() + cdupStart := getNumCanonicalDup() + // start consumer + var wg sync.WaitGroup + wg.Add(1) + go func(statsCh chan<- [4]int) { + defer wg.Done() + received := 0 + dropped := getNumBacklogDropped() - droppedStart + dups := getNumRawMsgDup() - dupStart + cdups := getNumCanonicalDup() - cdupStart + for dups+dropped+received+cdups < b.N { + select { + case msg := <-handler.backlogQueue: + require.Equal(b, numTxnsPerGroup, len(msg.unverifiedTxGroup)) + received++ + default: + dropped = getNumBacklogDropped() - droppedStart + dups = getNumRawMsgDup() - dupStart + cdups = getNumCanonicalDup() - cdupStart + } + if avgDelay > 0 { + time.Sleep(avgDelay) + } + } + statsCh <- [4]int{dropped, received, dups, cdups} + }(statsCh) + + return func() { + wg.Wait() + } +} + +// BenchmarkTxHandlerProcessIncomingTxn16 is the same BenchmarkTxHandlerProcessIncomingTxn with 16 goroutines +func BenchmarkTxHandlerProcessIncomingTxn16(b *testing.B) { + deadlockDisable := deadlock.Opts.Disable + deadlock.Opts.Disable = true + defer func() { + deadlock.Opts.Disable = deadlockDisable + }() + + const numSendThreads = 16 + const numTxnsPerGroup = 16 + handler := makeTestTxHandlerOrphaned(txBacklogSize) + // uncomment to benchmark no-dedup version + // handler.cacheConfig = txHandlerConfig{} + + // prepare tx groups + blobs := make([][]byte, b.N) + stxns := make([][]transactions.SignedTxn, b.N) + for i := 0; i < b.N; i++ { + stxns[i], blobs[i] = makeRandomTransactions(numTxnsPerGroup) + } + + statsCh := make(chan [4]int, 1) + defer close(statsCh) + finConsume := benchTxHandlerProcessIncomingTxnConsume(b, handler, numTxnsPerGroup, 0, statsCh) + + // submit tx groups + b.ResetTimer() + finalizeSubmit := benchTxHandlerProcessIncomingTxnSubmit(b, handler, blobs, numSendThreads) + + finalizeSubmit() + finConsume() +} + +// BenchmarkTxHandlerIncDeDup checks txn receiving with duplicates +// simulating processing delay +func BenchmarkTxHandlerIncDeDup(b *testing.B) { + deadlockDisable := deadlock.Opts.Disable + deadlock.Opts.Disable = true + defer func() { + deadlock.Opts.Disable = deadlockDisable + }() + + // parameters + const numSendThreads = 16 + const numTxnsPerGroup = 16 + + var tests = []struct { + dedup bool + dupFactor int + workerDelay time.Duration + firstLevelOnly bool + }{ + {false, 4, 10 * time.Microsecond, false}, + {true, 4, 10 * time.Microsecond, false}, + {false, 8, 10 * time.Microsecond, false}, + {true, 8, 10 * time.Microsecond, false}, + {false, 4, 4 * time.Microsecond, false}, + {true, 4, 4 * time.Microsecond, false}, + {false, 4, 0, false}, + {true, 4, 0, false}, + {true, 4, 10 * time.Microsecond, true}, + } + + for _, test := range tests { + var name string + var enabled string = "Y" + if !test.dedup { + enabled = "N" + } + name = fmt.Sprintf("x%d/on=%s/delay=%v", test.dupFactor, enabled, test.workerDelay) + if test.firstLevelOnly { + name = fmt.Sprintf("%s/one-level", name) + } + b.Run(name, func(b *testing.B) { + numPoolWorkers := runtime.NumCPU() + dupFactor := test.dupFactor + avgDelay := test.workerDelay / time.Duration(numPoolWorkers) + + handler := makeTestTxHandlerOrphaned(txBacklogSize) + if test.firstLevelOnly { + handler.cacheConfig = txHandlerConfig{enableFilteringRawMsg: true, enableFilteringCanonical: false} + } else if !test.dedup { + handler.cacheConfig = txHandlerConfig{} + } + + // prepare tx groups + blobs := make([][]byte, b.N) + stxns := make([][]transactions.SignedTxn, b.N) + for i := 0; i < b.N; i += dupFactor { + stxns[i], blobs[i] = makeRandomTransactions(numTxnsPerGroup) + if b.N >= dupFactor { // skip trivial runs + for j := 1; j < dupFactor; j++ { + if i+j < b.N { + stxns[i+j], blobs[i+j] = stxns[i], blobs[i] + } + } + } + } + + statsCh := make(chan [4]int, 1) + defer close(statsCh) + + finConsume := benchTxHandlerProcessIncomingTxnConsume(b, handler, numTxnsPerGroup, avgDelay, statsCh) + + // submit tx groups + b.ResetTimer() + finalizeSubmit := benchTxHandlerProcessIncomingTxnSubmit(b, handler, blobs, numSendThreads) + + finalizeSubmit() + finConsume() + + stats := <-statsCh + unique := b.N / dupFactor + dropped := stats[0] + received := stats[1] + dups := stats[2] + cdups := stats[3] + b.ReportMetric(float64(received)/float64(unique)*100, "ack,%") + b.ReportMetric(float64(dropped)/float64(b.N)*100, "drop,%") + if test.dedup { + b.ReportMetric(float64(dups)/float64(b.N)*100, "trap,%") + } + if b.N > 1 && os.Getenv("DEBUG") != "" { + b.Logf("unique %d, dropped %d, received %d, dups %d", unique, dropped, received, dups) + if cdups > 0 { + b.Logf("canonical dups %d vs %d recv", cdups, received) + } + } + }) + } +} + +func TestTxHandlerProcessIncomingGroup(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + type T struct { + inputSize int + numDecoded int + action network.ForwardingPolicy + } + var checks = []T{} + for i := 1; i <= config.MaxTxGroupSize; i++ { + checks = append(checks, T{i, i, network.Ignore}) + } + for i := 1; i < 10; i++ { + checks = append(checks, T{config.MaxTxGroupSize + i, 0, network.Disconnect}) + } + + for _, check := range checks { + t.Run(fmt.Sprintf("%d-%d", check.inputSize, check.numDecoded), func(t *testing.T) { + handler := TxHandler{ + backlogQueue: make(chan *txBacklogMsg, 1), + } + stxns, blob := makeRandomTransactions(check.inputSize) + action := handler.processIncomingTxn(network.IncomingMessage{Data: blob}) + require.Equal(t, network.OutgoingMessage{Action: check.action}, action) + if check.numDecoded > 0 { + msg := <-handler.backlogQueue + require.Equal(t, check.numDecoded, len(msg.unverifiedTxGroup)) + for i := 0; i < check.numDecoded; i++ { + require.Equal(t, stxns[i], msg.unverifiedTxGroup[i]) + } + } else { + require.Len(t, handler.backlogQueue, 0) + } + }) + } +} + +func TestTxHandlerProcessIncomingCensoring(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + craftNonCanonical := func(t *testing.T, stxn *transactions.SignedTxn, blobStxn []byte) []byte { + // make non-canonical encoding and ensure it is not accepted + stxnNonCanTxn := transactions.SignedTxn{Txn: stxn.Txn} + blobTxn := protocol.Encode(&stxnNonCanTxn) + stxnNonCanAuthAddr := transactions.SignedTxn{AuthAddr: stxn.AuthAddr} + blobAuthAddr := protocol.Encode(&stxnNonCanAuthAddr) + stxnNonCanAuthSig := transactions.SignedTxn{Sig: stxn.Sig} + blobSig := protocol.Encode(&stxnNonCanAuthSig) + + if blobStxn == nil { + blobStxn = protocol.Encode(stxn) + } + + // double check our skills for transactions.SignedTxn creation by creating a new canonical encoding and comparing to the original + blobValidation := make([]byte, 0, len(blobTxn)+len(blobAuthAddr)+len(blobSig)) + blobValidation = append(blobValidation[:], blobAuthAddr...) + blobValidation = append(blobValidation[:], blobSig[1:]...) // cut transactions.SignedTxn's field count + blobValidation = append(blobValidation[:], blobTxn[1:]...) // cut transactions.SignedTxn's field count + blobValidation[0] += 2 // increase field count + require.Equal(t, blobStxn, blobValidation) + + // craft non-canonical + blobNonCan := make([]byte, 0, len(blobTxn)+len(blobAuthAddr)+len(blobSig)) + blobNonCan = append(blobNonCan[:], blobTxn...) + blobNonCan = append(blobNonCan[:], blobAuthAddr[1:]...) // cut transactions.SignedTxn's field count + blobNonCan = append(blobNonCan[:], blobSig[1:]...) // cut transactions.SignedTxn's field count + blobNonCan[0] += 2 // increase field count + require.Len(t, blobNonCan, len(blobStxn)) + require.NotEqual(t, blobStxn, blobNonCan) + return blobNonCan + } + + forgeSig := func(t *testing.T, stxn *transactions.SignedTxn, blobStxn []byte) (transactions.SignedTxn, []byte) { + stxnForged := *stxn + crypto.RandBytes(stxnForged.Sig[:]) + blobForged := protocol.Encode(&stxnForged) + require.NotEqual(t, blobStxn, blobForged) + return stxnForged, blobForged + } + + encodeGroup := func(t *testing.T, g []transactions.SignedTxn, blobRef []byte) []byte { + result := make([]byte, 0, len(blobRef)) + for i := 0; i < len(g); i++ { + enc := protocol.Encode(&g[i]) + result = append(result, enc...) + } + require.NotEqual(t, blobRef, result) + return result + } + + t.Run("single", func(t *testing.T) { + handler := makeTestTxHandlerOrphaned(txBacklogSize) + stxns, blob := makeRandomTransactions(1) + stxn := stxns[0] + action := handler.processIncomingTxn(network.IncomingMessage{Data: blob}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + msg := <-handler.backlogQueue + require.Equal(t, 1, len(msg.unverifiedTxGroup)) + require.Equal(t, stxn, msg.unverifiedTxGroup[0]) + + // forge signature, ensure accepted + stxnForged, blobForged := forgeSig(t, &stxn, blob) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blobForged}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + msg = <-handler.backlogQueue + require.Equal(t, 1, len(msg.unverifiedTxGroup)) + require.Equal(t, stxnForged, msg.unverifiedTxGroup[0]) + + // make non-canonical encoding and ensure it is not accepted + blobNonCan := craftNonCanonical(t, &stxn, blob) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blobNonCan}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Len(t, handler.backlogQueue, 0) + }) + + t.Run("group", func(t *testing.T) { + handler := makeTestTxHandlerOrphaned(txBacklogSize) + num := rand.Intn(config.MaxTxGroupSize-1) + 2 // 2..config.MaxTxGroupSize + require.LessOrEqual(t, num, config.MaxTxGroupSize) + stxns, blob := makeRandomTransactions(num) + action := handler.processIncomingTxn(network.IncomingMessage{Data: blob}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + msg := <-handler.backlogQueue + require.Equal(t, num, len(msg.unverifiedTxGroup)) + for i := 0; i < num; i++ { + require.Equal(t, stxns[i], msg.unverifiedTxGroup[i]) + } + + // swap two txns + i := rand.Intn(num / 2) + j := rand.Intn(num-num/2) + num/2 + require.Less(t, i, j) + swapped := make([]transactions.SignedTxn, num) + copied := copy(swapped, stxns) + require.Equal(t, num, copied) + swapped[i], swapped[j] = swapped[j], swapped[i] + blobSwapped := encodeGroup(t, swapped, blob) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blobSwapped}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Len(t, handler.backlogQueue, 1) + msg = <-handler.backlogQueue + require.Equal(t, num, len(msg.unverifiedTxGroup)) + for i := 0; i < num; i++ { + require.Equal(t, swapped[i], msg.unverifiedTxGroup[i]) + } + + // forge signature, ensure accepted + i = rand.Intn(num) + forged := make([]transactions.SignedTxn, num) + copied = copy(forged, stxns) + require.Equal(t, num, copied) + crypto.RandBytes(forged[i].Sig[:]) + blobForged := encodeGroup(t, forged, blob) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blobForged}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Len(t, handler.backlogQueue, 1) + msg = <-handler.backlogQueue + require.Equal(t, num, len(msg.unverifiedTxGroup)) + for i := 0; i < num; i++ { + require.Equal(t, forged[i], msg.unverifiedTxGroup[i]) + } + + // make non-canonical encoding and ensure it is not accepted + i = rand.Intn(num) + nonCan := make([]transactions.SignedTxn, num) + copied = copy(nonCan, stxns) + require.Equal(t, num, copied) + blobNonCan := make([]byte, 0, len(blob)) + for j := 0; j < num; j++ { + enc := protocol.Encode(&nonCan[j]) + if j == i { + enc = craftNonCanonical(t, &stxns[j], enc) + } + blobNonCan = append(blobNonCan, enc...) + } + require.Len(t, blobNonCan, len(blob)) + require.NotEqual(t, blob, blobNonCan) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blobNonCan}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Len(t, handler.backlogQueue, 0) + }) +} + +// makeTestTxHandlerOrphaned creates a tx handler without any backlog consumer. +// It is caller responsibility to run a consumer thread. +func makeTestTxHandlerOrphaned(backlogSize int) *TxHandler { + return makeTestTxHandlerOrphanedWithContext(context.Background(), txBacklogSize, txBacklogSize, 0) +} + +func makeTestTxHandlerOrphanedWithContext(ctx context.Context, backlogSize int, cacheSize int, refreshInterval time.Duration) *TxHandler { + if backlogSize <= 0 { + backlogSize = txBacklogSize + } + if cacheSize <= 0 { + cacheSize = txBacklogSize + } + handler := &TxHandler{ + backlogQueue: make(chan *txBacklogMsg, backlogSize), + msgCache: makeSaltedCache(cacheSize), + txCanonicalCache: makeDigestCache(cacheSize), + cacheConfig: txHandlerConfig{true, true}, + } + handler.msgCache.start(ctx, refreshInterval) + return handler +} + +func makeTestTxHandler(dl *Ledger, cfg config.Local) *TxHandler { + tp := pools.MakeTransactionPool(dl.Ledger, cfg, logging.Base()) + backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) + opts := TxHandlerOpts{ + tp, backlogPool, dl, &mocks.MockNetwork{}, "", crypto.Digest{}, cfg, + } + return MakeTxHandler(opts) +} + +func TestTxHandlerProcessIncomingCache(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + handler := makeTestTxHandlerOrphaned(20) + + var action network.OutgoingMessage + var msg *txBacklogMsg + + // double enqueue a single txn message, ensure it discarded + stxns1, blob1 := makeRandomTransactions(1) + require.Equal(t, 1, len(stxns1)) + + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + msg = <-handler.backlogQueue + require.Equal(t, 1, len(msg.unverifiedTxGroup)) + require.Equal(t, stxns1[0], msg.unverifiedTxGroup[0]) + + // double enqueue a two txns message + stxns2, blob2 := makeRandomTransactions(2) + require.Equal(t, 2, len(stxns2)) + + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob2}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob2}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + msg = <-handler.backlogQueue + require.Equal(t, 2, len(msg.unverifiedTxGroup)) + require.Equal(t, stxns2[0], msg.unverifiedTxGroup[0]) + require.Equal(t, stxns2[1], msg.unverifiedTxGroup[1]) + + // now combine seen and not seen txns, ensure the group is still enqueued + stxns3, _ := makeRandomTransactions(2) + require.Equal(t, 2, len(stxns3)) + stxns3[1] = stxns1[0] + + var blob3 []byte + for i := range stxns3 { + encoded := protocol.Encode(&stxns3[i]) + blob3 = append(blob3, encoded...) + } + require.Greater(t, len(blob3), 0) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob3}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + msg = <-handler.backlogQueue + require.Equal(t, 2, len(msg.unverifiedTxGroup)) + require.Equal(t, stxns3[0], msg.unverifiedTxGroup[0]) + require.Equal(t, stxns3[1], msg.unverifiedTxGroup[1]) + + // check a combo from two different seen groups, ensure the group is still enqueued + stxns4 := make([]transactions.SignedTxn, 2) + stxns4[0] = stxns2[0] + stxns4[1] = stxns3[0] + var blob4 []byte + for i := range stxns4 { + encoded := protocol.Encode(&stxns4[i]) + blob4 = append(blob4, encoded...) + } + require.Greater(t, len(blob4), 0) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob4}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + msg = <-handler.backlogQueue + require.Equal(t, 2, len(msg.unverifiedTxGroup)) + require.Equal(t, stxns4[0], msg.unverifiedTxGroup[0]) + require.Equal(t, stxns4[1], msg.unverifiedTxGroup[1]) +} + +func TestTxHandlerProcessIncomingCacheRotation(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + stxns1, blob1 := makeRandomTransactions(1) + require.Equal(t, 1, len(stxns1)) + + resetCanonical := func(handler *TxHandler) { + handler.txCanonicalCache.swap() + handler.txCanonicalCache.swap() + } + + t.Run("scheduled", func(t *testing.T) { + // double enqueue a single txn message, ensure it discarded + ctx, cancelFunc := context.WithCancel(context.Background()) + handler := makeTestTxHandlerOrphanedWithContext(ctx, txBacklogSize, txBacklogSize, 10*time.Millisecond) + + var action network.OutgoingMessage + var msg *txBacklogMsg + + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + resetCanonical(handler) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + msg = <-handler.backlogQueue + require.Equal(t, 1, len(msg.unverifiedTxGroup)) + require.Equal(t, stxns1[0], msg.unverifiedTxGroup[0]) + cancelFunc() + }) + + t.Run("manual", func(t *testing.T) { + // double enqueue a single txn message, ensure it discarded + handler := makeTestTxHandlerOrphaned(txBacklogSize) + var action network.OutgoingMessage + var msg *txBacklogMsg + + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + resetCanonical(handler) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + msg = <-handler.backlogQueue + require.Equal(t, 1, len(msg.unverifiedTxGroup)) + require.Equal(t, stxns1[0], msg.unverifiedTxGroup[0]) + + // rotate once, ensure the txn still there + handler.msgCache.Remix() + resetCanonical(handler) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 0, len(handler.backlogQueue)) + + // rotate twice, ensure the txn done + handler.msgCache.Remix() + resetCanonical(handler) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + resetCanonical(handler) + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + msg = <-handler.backlogQueue + require.Equal(t, 1, len(msg.unverifiedTxGroup)) + require.Equal(t, stxns1[0], msg.unverifiedTxGroup[0]) + }) +} + +// TestTxHandlerProcessIncomingCacheBacklogDrop checks if dropped messages are also removed from caches +func TestTxHandlerProcessIncomingCacheBacklogDrop(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + handler := makeTestTxHandlerOrphanedWithContext(context.Background(), 1, 20, 0) + + stxns1, blob1 := makeRandomTransactions(1) + require.Equal(t, 1, len(stxns1)) + + action := handler.processIncomingTxn(network.IncomingMessage{Data: blob1}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + require.Equal(t, 1, handler.msgCache.Len()) + require.Equal(t, 1, handler.txCanonicalCache.Len()) + + stxns2, blob2 := makeRandomTransactions(1) + require.Equal(t, 1, len(stxns2)) + + initialValue := transactionMessagesDroppedFromBacklog.GetUint64Value() + action = handler.processIncomingTxn(network.IncomingMessage{Data: blob2}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + require.Equal(t, 1, handler.msgCache.Len()) + require.Equal(t, 1, handler.txCanonicalCache.Len()) + currentValue := transactionMessagesDroppedFromBacklog.GetUint64Value() + require.Equal(t, initialValue+1, currentValue) +} + +func TestTxHandlerProcessIncomingCacheTxPoolDrop(t *testing.T) { + partitiontest.PartitionTest(t) + + const numUsers = 100 + log := logging.TestingLog(t) + + // prepare the accounts + addresses, secrets, genesis := makeTestGenesisAccounts(t, numUsers) + genBal := bookkeeping.MakeGenesisBalances(genesis, sinkAddr, poolAddr) + ledgerName := fmt.Sprintf("%s-mem", t.Name()) + const inMem = true + cfg := config.GetDefaultLocal() + cfg.Archival = true + cfg.TxIncomingFilteringFlags = 3 // txFilterRawMsg + txFilterCanonical + ledger, err := LoadLedger(log, ledgerName, inMem, protocol.ConsensusCurrentVersion, genBal, genesisID, genesisHash, nil, cfg) + require.NoError(t, err) + + l := ledger + handler := makeTestTxHandler(l, cfg) + handler.postVerificationQueue = make(chan *txBacklogMsg) + + makeTxns := func(sendIdx, recvIdx int) ([]transactions.SignedTxn, []byte) { + tx := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: addresses[sendIdx], + Fee: basics.MicroAlgos{Raw: proto.MinTxnFee * 2}, + FirstValid: 0, + LastValid: basics.Round(proto.MaxTxnLife), + Note: make([]byte, 2), + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: addresses[recvIdx], + Amount: basics.MicroAlgos{Raw: mockBalancesMinBalance + (rand.Uint64() % 10000)}, + }, + } + signedTx := tx.Sign(secrets[sendIdx]) + blob := protocol.Encode(&signedTx) + return []transactions.SignedTxn{signedTx}, blob + } + + stxns, blob := makeTxns(1, 2) + + action := handler.processIncomingTxn(network.IncomingMessage{Data: blob}) + require.Equal(t, network.OutgoingMessage{Action: network.Ignore}, action) + require.Equal(t, 1, len(handler.backlogQueue)) + require.Equal(t, 1, handler.msgCache.Len()) + require.Equal(t, 1, handler.txCanonicalCache.Len()) + + msg := <-handler.backlogQueue + require.Equal(t, 1, len(msg.unverifiedTxGroup)) + require.Equal(t, stxns, msg.unverifiedTxGroup) + + initialCount := transactionMessagesDroppedFromPool.GetUint64Value() + handler.asyncVerifySignature(msg) + currentCount := transactionMessagesDroppedFromPool.GetUint64Value() + require.Equal(t, initialCount+1, currentCount) + require.Equal(t, 0, handler.msgCache.Len()) + require.Equal(t, 0, handler.txCanonicalCache.Len()) +} + const benchTxnNum = 25_000 func BenchmarkTxHandlerDecoder(b *testing.B) { @@ -255,42 +1027,51 @@ func BenchmarkTxHandlerDecoderMsgp(b *testing.B) { } } -func TestIncomingTxHandle(t *testing.T) { - incomingTxHandlerProcessing(1, t) +// TestTxHandlerIncomingTxHandle checks the correctness with single txns +func TestTxHandlerIncomingTxHandle(t *testing.T) { + partitiontest.PartitionTest(t) + + numberOfTransactionGroups := 1000 + incomingTxHandlerProcessing(1, numberOfTransactionGroups, t) } -func TestIncomingTxGroupHandle(t *testing.T) { - incomingTxHandlerProcessing(proto.MaxTxGroupSize, t) +// TestTxHandlerIncomingTxGroupHandle checks the correctness with txn groups +func TestTxHandlerIncomingTxGroupHandle(t *testing.T) { + partitiontest.PartitionTest(t) + + numberOfTransactionGroups := 1000 / proto.MaxTxGroupSize + incomingTxHandlerProcessing(proto.MaxTxGroupSize, numberOfTransactionGroups, t) +} + +// TestTxHandlerIncomingTxHandleDrops accounts for the dropped txns when the verifier/exec pool is saturated +func TestTxHandlerIncomingTxHandleDrops(t *testing.T) { + partitiontest.PartitionTest(t) + + // use smaller backlog size to test the message drops + origValue := txBacklogSize + defer func() { + txBacklogSize = origValue + }() + txBacklogSize = 10 + + numberOfTransactionGroups := 1000 + incomingTxHandlerProcessing(1, numberOfTransactionGroups, t) } // incomingTxHandlerProcessing is a comprehensive transaction handling test // It handles the singed transactions by passing them to the backlog for verification -func incomingTxHandlerProcessing(maxGroupSize int, t *testing.T) { +func incomingTxHandlerProcessing(maxGroupSize, numberOfTransactionGroups int, t *testing.T) { + defer func() { + // reset the counters + transactionMessagesDroppedFromBacklog = metrics.MakeCounter(metrics.TransactionMessagesDroppedFromBacklog) + transactionMessagesDroppedFromPool = metrics.MakeCounter(metrics.TransactionMessagesDroppedFromPool) + }() + const numUsers = 100 - numberOfTransactionGroups := 1000 log := logging.TestingLog(t) - log.SetLevel(logging.Warn) - addresses := make([]basics.Address, numUsers) - secrets := make([]*crypto.SignatureSecrets, numUsers) // prepare the accounts - genesis := make(map[basics.Address]basics.AccountData) - for i := 0; i < numUsers; i++ { - secret := keypair() - addr := basics.Address(secret.SignatureVerifier) - secrets[i] = secret - addresses[i] = addr - genesis[addr] = basics.AccountData{ - Status: basics.Online, - MicroAlgos: basics.MicroAlgos{Raw: 10000000000000}, - } - } - genesis[poolAddr] = basics.AccountData{ - Status: basics.NotParticipating, - MicroAlgos: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinBalance}, - } - - require.Equal(t, len(genesis), numUsers+1) + addresses, secrets, genesis := makeTestGenesisAccounts(t, numUsers) genBal := bookkeeping.MakeGenesisBalances(genesis, sinkAddr, poolAddr) ledgerName := fmt.Sprintf("%s-mem-%d", t.Name(), numberOfTransactionGroups) const inMem = true @@ -300,9 +1081,8 @@ func incomingTxHandlerProcessing(maxGroupSize int, t *testing.T) { require.NoError(t, err) l := ledger - tp := pools.MakeTransactionPool(l.Ledger, cfg, logging.Base()) - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - handler := MakeTxHandler(tp, l, &mocks.MockNetwork{}, "", crypto.Digest{}, backlogPool) + handler := makeTestTxHandler(l, cfg) + // since Start is not called, set the context here handler.ctx, handler.ctxCancel = context.WithCancel(context.Background()) defer handler.ctxCancel() @@ -310,8 +1090,8 @@ func incomingTxHandlerProcessing(maxGroupSize int, t *testing.T) { outChan := make(chan *txBacklogMsg, 10) wg := sync.WaitGroup{} wg.Add(1) - // Make a test backlog worker, which is simiar to backlogWorker, but sends the results - // through the outChan instead of passing it to postprocessCheckedTxn + // Make a test backlog worker, which is similar to backlogWorker, but sends the results + // through the outChan instead of passing it to postProcessCheckedTxn go func() { defer wg.Done() defer close(outChan) @@ -376,47 +1156,58 @@ func incomingTxHandlerProcessing(maxGroupSize int, t *testing.T) { } // Process the results and make sure they are correct + initDroppedBacklog, initDroppedPool := getDropped() wg.Add(1) go func() { defer wg.Done() - groupCounter := 0 + var groupCounter uint64 txnCounter := 0 invalidCounter := 0 + var droppedBacklog, droppedPool uint64 defer func() { + t.Logf("Txn groups with invalid sigs: %d\n", invalidCounter) + t.Logf("dropped: [%d backlog] [%d pool]\n", droppedBacklog, droppedPool) + // release the backlog worker t.Logf("processed %d txn groups (%d txns)\n", groupCounter, txnCounter) + handler.Stop() // cancel the handler ctx }() - for wi := range outChan { - txnCounter = txnCounter + len(wi.unverifiedTxGroup) - groupCounter++ - u, _ := binary.Uvarint(wi.unverifiedTxGroup[0].Txn.Note) - _, inBad := badTxnGroups[u] - if wi.verificationErr == nil { - require.False(t, inBad, "No error for invalid signature") - } else { - invalidCounter++ - require.True(t, inBad, "Error for good signature") + timer := time.NewTicker(250 * time.Millisecond) + for { + select { + case wi := <-outChan: + txnCounter = txnCounter + len(wi.unverifiedTxGroup) + groupCounter++ + u, _ := binary.Uvarint(wi.unverifiedTxGroup[0].Txn.Note) + _, inBad := badTxnGroups[u] + if wi.verificationErr == nil { + require.False(t, inBad, "No error for invalid signature") + } else { + invalidCounter++ + require.True(t, inBad, "Error for good signature") + } + case <-timer.C: + droppedBacklog, droppedPool = getDropped() + if int(groupCounter+(droppedBacklog-initDroppedBacklog)+(droppedPool-initDroppedPool)) == len(signedTransactionGroups) { + // all the benchmark txns processed + return + } + time.Sleep(250 * time.Millisecond) + timer.Reset(250 * time.Millisecond) } } - t.Logf("Txn groups with invalid sigs: %d\n", invalidCounter) }() // Send the transactions to the verifier for _, tg := range encodedSignedTransactionGroups { handler.processIncomingTxn(tg) - randduration := time.Duration(uint64(((1 + rand.Float32()) * 3))) - time.Sleep(randduration * time.Microsecond) } - close(handler.backlogQueue) wg.Wait() +} - // Report the number of transactions dropped because the backlog was busy - var buf strings.Builder - metrics.DefaultRegistry().WriteMetrics(&buf, "") - str := buf.String() - x := strings.Index(str, "\nalgod_transaction_messages_dropped_backlog") - str = str[x+44 : x+44+strings.Index(str[x+44:], "\n")] - str = strings.TrimSpace(strings.ReplaceAll(str, "}", " ")) - t.Logf("dropped %s txn gropus\n", str) +func getDropped() (droppedBacklog, droppedPool uint64) { + droppedBacklog = transactionMessagesDroppedFromBacklog.GetUint64Value() + droppedPool = transactionMessagesDroppedFromPool.GetUint64Value() + return } // makeSignedTxnGroups prepares N transaction groups of random (maxGroupSize) sizes with random @@ -454,13 +1245,17 @@ func makeSignedTxnGroups(N, numUsers, maxGroupSize int, invalidProb float32, add Amount: basics.MicroAlgos{Raw: mockBalancesMinBalance + (rand.Uint64() % 10000)}, }, } - txGroup.TxGroupHashes = append(txGroup.TxGroupHashes, crypto.Digest(tx.ID())) + if grpSize > 1 { + txGroup.TxGroupHashes = append(txGroup.TxGroupHashes, crypto.Digest(tx.ID())) + } txns = append(txns, tx) } groupHash := crypto.HashObj(txGroup) signedTxGroup := make([]transactions.SignedTxn, 0, grpSize) for g, txn := range txns { - txn.Group = groupHash + if grpSize > 1 { + txn.Group = groupHash + } signedTx := txn.Sign(secrets[(u+g)%numUsers]) signedTx.Txn = txn signedTxGroup = append(signedTxGroup, signedTx) @@ -476,46 +1271,81 @@ func makeSignedTxnGroups(N, numUsers, maxGroupSize int, invalidProb float32, add return } -// BenchmarkHandler sends singed transactions the the verifier +// BenchmarkHandleTxns sends signed transactions directly to the verifier func BenchmarkHandleTxns(b *testing.B) { - b.N = b.N * proto.MaxTxGroupSize / 2 - runHandlerBenchmark(1, b) + maxGroupSize := 1 + tpss := []int{6000000, 600000, 60000, 6000} + invalidRates := []float32{0.5, 0.001} + for _, tps := range tpss { + for _, ivr := range invalidRates { + b.Run(fmt.Sprintf("tps_%d_inv_%.3f", tps, ivr), func(b *testing.B) { + runHandlerBenchmarkWithBacklog(maxGroupSize, tps, ivr, b, false) + }) + } + } } -// BenchmarkHandler sends singed transaction groups to the verifier +// BenchmarkHandleTxnGroups sends signed transaction groups directly to the verifier func BenchmarkHandleTxnGroups(b *testing.B) { - runHandlerBenchmark(proto.MaxTxGroupSize, b) + maxGroupSize := proto.MaxTxGroupSize / 2 + tpss := []int{6000000, 600000, 60000, 6000} + invalidRates := []float32{0.5, 0.001} + for _, tps := range tpss { + for _, ivr := range invalidRates { + b.Run(fmt.Sprintf("tps_%d_inv_%.3f", tps, ivr), func(b *testing.B) { + runHandlerBenchmarkWithBacklog(maxGroupSize, tps, ivr, b, false) + }) + } + } } -// runHandlerBenchmark has a similar workflow to incomingTxHandlerProcessing, -// but bypasses the backlog, and sends the transactions directly to the verifier -func runHandlerBenchmark(maxGroupSize int, b *testing.B) { - const numUsers = 100 - log := logging.TestingLog(b) - log.SetLevel(logging.Warn) - addresses := make([]basics.Address, numUsers) - secrets := make([]*crypto.SignatureSecrets, numUsers) - - // prepare the accounts - genesis := make(map[basics.Address]basics.AccountData) - for i := 0; i < numUsers; i++ { - secret := keypair() - addr := basics.Address(secret.SignatureVerifier) - secrets[i] = secret - addresses[i] = addr - genesis[addr] = basics.AccountData{ - Status: basics.Online, - MicroAlgos: basics.MicroAlgos{Raw: 10000000000000}, +// BenchmarkBacklogWorkerHandleTxns sends signed transactions to the verifier +// using a backlog worker replica +func BenchmarkHandleBLWTxns(b *testing.B) { + maxGroupSize := 1 + tpss := []int{6000000, 600000, 60000, 6000} + invalidRates := []float32{0.5, 0.001} + for _, tps := range tpss { + for _, ivr := range invalidRates { + b.Run(fmt.Sprintf("tps_%d_inv_%.3f", tps, ivr), func(b *testing.B) { + runHandlerBenchmarkWithBacklog(maxGroupSize, tps, ivr, b, true) + }) } } - genesis[poolAddr] = basics.AccountData{ - Status: basics.NotParticipating, - MicroAlgos: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinBalance}, +} + +// BenchmarkBacklogWorkerHandleTxnGroups sends signed transaction groups to the verifier +// using a backlog worker replica +func BenchmarkHandleBLWTxnGroups(b *testing.B) { + maxGroupSize := proto.MaxTxGroupSize / 2 + tpss := []int{6000000, 600000, 60000, 6000} + invalidRates := []float32{0.5, 0.001} + for _, tps := range tpss { + for _, ivr := range invalidRates { + b.Run(fmt.Sprintf("tps_%d_inv_%.3f", tps, ivr), func(b *testing.B) { + runHandlerBenchmarkWithBacklog(maxGroupSize, tps, ivr, b, true) + }) + } } +} + +// runHandlerBenchmarkWithBacklog benchmarks the number of transactions verfied or dropped +func runHandlerBenchmarkWithBacklog(maxGroupSize, tps int, invalidRate float32, b *testing.B, useBacklogWorker bool) { + defer func() { + // reset the counters + transactionMessagesDroppedFromBacklog = metrics.MakeCounter(metrics.TransactionMessagesDroppedFromBacklog) + transactionMessagesDroppedFromPool = metrics.MakeCounter(metrics.TransactionMessagesDroppedFromPool) + }() - require.Equal(b, len(genesis), numUsers+1) + const numUsers = 100 + log := logging.TestingLog(b) + log.SetLevel(logging.Warn) + + addresses, secrets, genesis := makeTestGenesisAccounts(b, numUsers) genBal := bookkeeping.MakeGenesisBalances(genesis, sinkAddr, poolAddr) - ledgerName := fmt.Sprintf("%s-mem-%d", b.Name(), b.N) + ivrString := strings.IndexAny(fmt.Sprintf("%f", invalidRate), "1") + ledgerName := fmt.Sprintf("%s-mem-%d-%d", b.Name(), b.N, ivrString) + ledgerName = strings.Replace(ledgerName, "#", "-", 1) const inMem = true cfg := config.GetDefaultLocal() cfg.Archival = true @@ -523,56 +1353,147 @@ func runHandlerBenchmark(maxGroupSize int, b *testing.B) { require.NoError(b, err) l := ledger - tp := pools.MakeTransactionPool(l.Ledger, cfg, logging.Base()) - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - handler := MakeTxHandler(tp, l, &mocks.MockNetwork{}, "", crypto.Digest{}, backlogPool) + handler := makeTestTxHandler(l, cfg) // since Start is not called, set the context here handler.ctx, handler.ctxCancel = context.WithCancel(context.Background()) defer handler.ctxCancel() - // Prepare the transactions - signedTransactionGroups, badTxnGroups := makeSignedTxnGroups(b.N, numUsers, maxGroupSize, 0.001, addresses, secrets) - outChan := handler.postVerificationQueue + testResultChan := handler.postVerificationQueue wg := sync.WaitGroup{} + if useBacklogWorker { + wg.Add(1) + testResultChan = make(chan *txBacklogMsg, 10) + // Make a test backlog worker, which is similar to backlogWorker, but sends the results + // through the testResultChan instead of passing it to postProcessCheckedTxn + go func() { + defer wg.Done() + for { + // prioritize the postVerificationQueue + select { + case wi, ok := <-handler.postVerificationQueue: + if !ok { + return + } + testResultChan <- wi + + // restart the loop so that we could empty out the post verification queue. + continue + default: + } + + // we have no more post verification items. wait for either backlog queue item or post verification item. + select { + case wi, ok := <-handler.backlogQueue: + if !ok { + return + } + if handler.checkAlreadyCommitted(wi) { + // this is not expected during the test + continue + } + handler.txVerificationPool.EnqueueBacklog(handler.ctx, handler.asyncVerifySignature, wi, nil) + + case wi, ok := <-handler.postVerificationQueue: + if !ok { + return + } + testResultChan <- wi + + case <-handler.ctx.Done(): + return + } + } + }() + } + + // Prepare the transactions + signedTransactionGroups, badTxnGroups := makeSignedTxnGroups(b.N, numUsers, maxGroupSize, invalidRate, addresses, secrets) + var encodedSignedTransactionGroups []network.IncomingMessage + if useBacklogWorker { + encodedSignedTransactionGroups = make([]network.IncomingMessage, 0, b.N) + for _, stxngrp := range signedTransactionGroups { + data := make([]byte, 0) + for _, stxn := range stxngrp { + data = append(data, protocol.Encode(&stxn)...) + } + encodedSignedTransactionGroups = + append(encodedSignedTransactionGroups, network.IncomingMessage{Data: data}) + } + } + var tt time.Time // Process the results and make sure they are correct + rateAdjuster := time.Second / time.Duration(tps) wg.Add(1) go func() { defer wg.Done() - groupCounter := 0 + groupCounter := uint64(0) var txnCounter uint64 invalidCounter := 0 - for wi := range outChan { - txnCounter = txnCounter + uint64(len(wi.unverifiedTxGroup)) - groupCounter++ - u, _ := binary.Uvarint(wi.unverifiedTxGroup[0].Txn.Note) - _, inBad := badTxnGroups[u] - if wi.verificationErr == nil { - require.False(b, inBad, "No error for invalid signature") - } else { - invalidCounter++ - require.True(b, inBad, "Error for good signature") + defer func() { + if groupCounter > 1 { + droppedBacklog, droppedPool := getDropped() + b.Logf("Input T(grp)PS: %d (delay %f microsec)", tps, float64(rateAdjuster)/float64(time.Microsecond)) + b.Logf("Verified TPS: %d", uint64(txnCounter)*uint64(time.Second)/uint64(time.Since(tt))) + b.Logf("Time/txn: %d(microsec)", uint64((time.Since(tt)/time.Microsecond))/txnCounter) + b.Logf("processed total: [%d groups (%d invalid)] [%d txns]", groupCounter, invalidCounter, txnCounter) + b.Logf("dropped: [%d backlog] [%d pool]\n", droppedBacklog, droppedPool) + } + handler.Stop() // cancel the handler ctx + }() + stopChan := make(chan interface{}) + go func() { + for { + time.Sleep(200 * time.Millisecond) + droppedBacklog, droppedPool := getDropped() + if int(groupCounter+droppedBacklog+droppedPool) == len(signedTransactionGroups) { + // all the benchmark txns processed + close(stopChan) + return + } + } + }() + + for { + select { + case wi := <-testResultChan: + txnCounter = txnCounter + uint64(len(wi.unverifiedTxGroup)) + groupCounter++ + u, _ := binary.Uvarint(wi.unverifiedTxGroup[0].Txn.Note) + _, inBad := badTxnGroups[u] + if wi.verificationErr == nil { + require.False(b, inBad, "No error for invalid signature") + } else { + invalidCounter++ + require.True(b, inBad, "Error for good signature") + } + if groupCounter == uint64(len(signedTransactionGroups)) { + // all the benchmark txns processed + return + } + case <-stopChan: + return } - } - if txnCounter > 0 { - b.Logf("TPS: %d\n", uint64(txnCounter)*1000000000/uint64(time.Since(tt))) - b.Logf("Time/txn: %d(microsec)\n", uint64((time.Since(tt)/time.Microsecond))/txnCounter) - b.Logf("processed total: [%d groups (%d invalid)] [%d txns]\n", groupCounter, invalidCounter, txnCounter) } }() b.ResetTimer() tt = time.Now() - for _, stxngrp := range signedTransactionGroups { - blm := txBacklogMsg{rawmsg: nil, unverifiedTxGroup: stxngrp} - handler.txVerificationPool.EnqueueBacklog(handler.ctx, handler.asyncVerifySignature, &blm, nil) + if useBacklogWorker { + for _, tg := range encodedSignedTransactionGroups { + handler.processIncomingTxn(tg) + time.Sleep(rateAdjuster) + } + } else { + for _, stxngrp := range signedTransactionGroups { + blm := txBacklogMsg{rawmsg: nil, unverifiedTxGroup: stxngrp} + handler.txVerificationPool.EnqueueBacklog(handler.ctx, handler.asyncVerifySignature, &blm, nil) + time.Sleep(rateAdjuster) + } } - // shut down to end the test - handler.txVerificationPool.Shutdown() - close(handler.postVerificationQueue) - close(handler.backlogQueue) wg.Wait() + handler.Stop() // cancel the handler ctx } func TestTxHandlerPostProcessError(t *testing.T) { @@ -609,13 +1530,13 @@ func TestTxHandlerPostProcessError(t *testing.T) { continue } - errTxGroup := &verify.ErrTxGroupError{Reason: i} + errTxGroup := &verify.TxGroupError{Reason: i} txh.postProcessReportErrors(errTxGroup) result = collect() if i == verify.TxGroupErrorReasonSigNotWellFormed { // TxGroupErrorReasonSigNotWellFormed and TxGroupErrorReasonHasNoSig increment the same metric counter-- - require.Equal(t, result[metrics.TransactionMessagesTxnSigNotWellFormed.Name], float64(2)) + require.Equal(t, float64(2), result[metrics.TransactionMessagesTxnSigNotWellFormed.Name]) } require.Len(t, result, counter) counter++ @@ -645,7 +1566,7 @@ func TestTxHandlerPostProcessErrorWithVerify(t *testing.T) { }, } _, err := verify.TxnGroup([]transactions.SignedTxn{stxn}, hdr, nil, nil) - var txGroupErr *verify.ErrTxGroupError + var txGroupErr *verify.TxGroupError require.ErrorAs(t, err, &txGroupErr) result := map[string]float64{} @@ -657,3 +1578,255 @@ func TestTxHandlerPostProcessErrorWithVerify(t *testing.T) { transactionMessagesTxnNotWellFormed.AddMetric(result) require.Len(t, result, 1) } + +// TestTxHandlerRememberReportErrors checks Is and As statements work as expected +func TestTxHandlerRememberReportErrors(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + var txh TxHandler + result := map[string]float64{} + + getMetricName := func(tag string) string { + return strings.ReplaceAll(transactionMessageTxPoolRememberCounter.Name, "{TAG}", tag) + } + getMetricCounter := func(tag string) int { + transactionMessageTxPoolRememberCounter.AddMetric(result) + return int(result[getMetricName(tag)]) + } + + noSpaceErr := ledgercore.ErrNoSpace + txh.rememberReportErrors(noSpaceErr) + transactionMessageTxPoolRememberCounter.AddMetric(result) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagNoSpace)) + + wrapped := fmt.Errorf("wrap: %w", noSpaceErr) // simulate wrapping + txh.rememberReportErrors(wrapped) + + transactionMessageTxPoolRememberCounter.AddMetric(result) + require.Equal(t, 2, getMetricCounter(txPoolRememberTagNoSpace)) + + feeErr := pools.ErrTxPoolFeeError{} + wrapped = fmt.Errorf("wrap: %w", &feeErr) // simulate wrapping + txh.rememberReportErrors(wrapped) + + transactionMessageTxPoolRememberCounter.AddMetric(result) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagFee)) +} + +type blockTicker struct { + cond sync.Cond +} + +func (t *blockTicker) OnNewBlock(block bookkeeping.Block, delta ledgercore.StateDelta) { + t.cond.L.Lock() + defer t.cond.L.Unlock() + t.cond.Broadcast() +} + +func (t *blockTicker) Wait() { + t.cond.L.Lock() + defer t.cond.L.Unlock() + t.cond.Wait() +} + +func TestTxHandlerRememberReportErrorsWithTxPool(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + result := map[string]float64{} + checkResult := map[string]float64{} + getMetricName := func(tag string) string { + return strings.ReplaceAll(transactionMessageTxPoolRememberCounter.Name, "{TAG}", tag) + } + getCheckMetricName := func(tag string) string { + return strings.ReplaceAll(transactionMessageTxPoolCheckCounter.Name, "{TAG}", tag) + } + getMetricCounter := func(tag string) int { + transactionMessageTxPoolRememberCounter.AddMetric(result) + return int(result[getMetricName(tag)]) + } + getCheckMetricCounter := func(tag string) int { + transactionMessageTxPoolCheckCounter.AddMetric(checkResult) + return int(checkResult[getCheckMetricName(tag)]) + } + + log := logging.TestingLog(t) + log.SetLevel(logging.Warn) + + const numAccts = 2 + genesis := make(map[basics.Address]basics.AccountData, numAccts+1) + addresses := make([]basics.Address, numAccts) + secrets := make([]*crypto.SignatureSecrets, numAccts) + + for i := 0; i < numAccts; i++ { + secret := keypair() + addr := basics.Address(secret.SignatureVerifier) + secrets[i] = secret + addresses[i] = addr + genesis[addr] = basics.AccountData{ + Status: basics.Online, + MicroAlgos: basics.MicroAlgos{Raw: 10000000000000}, + } + } + genesis[poolAddr] = basics.AccountData{ + Status: basics.NotParticipating, + MicroAlgos: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinBalance}, + } + + genBal := bookkeeping.MakeGenesisBalances(genesis, sinkAddr, poolAddr) + + ledgerName := fmt.Sprintf("%s-mem-%d", t.Name(), rand.Int()) + const inMem = true + cfg := config.GetDefaultLocal() + cfg.Archival = true + cfg.TxPoolSize = config.MaxTxGroupSize + 1 + ledger, err := LoadLedger(log, ledgerName, inMem, protocol.ConsensusCurrentVersion, genBal, genesisID, genesisHash, nil, cfg) + require.NoError(t, err) + + handler := makeTestTxHandler(ledger, cfg) + // since Start is not called, set the context here + handler.ctx, handler.ctxCancel = context.WithCancel(context.Background()) + defer handler.ctxCancel() + + var wi txBacklogMsg + wi.unverifiedTxGroup = []transactions.SignedTxn{{}} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagTxnDead)) + + // trigger max pool capacity metric + hdr := bookkeeping.BlockHeader{ + Round: 1, + UpgradeState: bookkeeping.UpgradeState{ + CurrentProtocol: protocol.ConsensusCurrentVersion, + }, + } + + txn1 := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: addresses[0], + Fee: basics.MicroAlgos{Raw: proto.MinTxnFee * 2}, + FirstValid: 0, + LastValid: basics.Round(proto.MaxTxnLife), + GenesisHash: genesisHash, + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: poolAddr, + Amount: basics.MicroAlgos{Raw: mockBalancesMinBalance + (rand.Uint64() % 10000)}, + }, + } + + wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0])} + for i := 0; i <= cfg.TxPoolSize; i++ { + txn := txn1 + crypto.RandBytes(txn.Note[:]) + wi.unverifiedTxGroup = append(wi.unverifiedTxGroup, txn.Sign(secrets[0])) + } + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagCap)) + + // trigger not well-formed error + txn2 := txn1 + txn2.Sender = basics.Address{} + wi.unverifiedTxGroup = []transactions.SignedTxn{txn2.Sign(secrets[0])} + handler.checkAlreadyCommitted(&wi) + require.Equal(t, 1, getCheckMetricCounter(txPoolRememberTagTxnNotWellFormed)) + + // trigger group id error + txn2 = txn1 + crypto.RandBytes(txn2.Group[:]) + wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0]), txn2.Sign(secrets[0])} + handler.checkAlreadyCommitted(&wi) + require.Equal(t, 1, getCheckMetricCounter(txPoolRememberTagGroupID)) + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagGroupID)) + + // trigger group too large error + wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0])} + for i := 0; i < config.MaxTxGroupSize; i++ { + txn := txn1 + crypto.RandBytes(txn.Note[:]) + wi.unverifiedTxGroup = append(wi.unverifiedTxGroup, txn.Sign(secrets[0])) + } + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagTooLarge)) + handler.checkAlreadyCommitted(&wi) + require.Equal(t, 1, getCheckMetricCounter(txPoolRememberTagTooLarge)) + + // trigger eval error + secret := keypair() + addr := basics.Address(secret.SignatureVerifier) + txn2 = txn1 + txn2.Sender = addr + wi.unverifiedTxGroup = []transactions.SignedTxn{txn2.Sign(secret)} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagEvalGeneric)) + + // trigger TxnDeadErr from the evaluator + txn2 = txn1 + txn2.FirstValid = ledger.LastRound() + 10 + prevTxnDead := getMetricCounter(txPoolRememberTagTxnDead) + wi.unverifiedTxGroup = []transactions.SignedTxn{txn2.Sign(secrets[0])} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, prevTxnDead+1, getMetricCounter(txPoolRememberTagTxnDead)) + handler.checkAlreadyCommitted(&wi) + require.Equal(t, 1, getCheckMetricCounter(txPoolRememberTagTxnDead)) + + // trigger TransactionInLedgerError (txid) error + wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0])} + wi.rawmsg = &network.IncomingMessage{} + handler.postProcessCheckedTxn(&wi) + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagTxIDEval)) + handler.checkAlreadyCommitted(&wi) + require.Equal(t, 1, getCheckMetricCounter(txPoolRememberTagTxIDEval)) + + // trigger LeaseInLedgerError (lease) error + txn2 = txn1 + crypto.RandBytes(txn2.Lease[:]) + txn3 := txn2 + txn3.Receiver = addr + wi.unverifiedTxGroup = []transactions.SignedTxn{txn2.Sign(secrets[0])} + handler.postProcessCheckedTxn(&wi) + wi.unverifiedTxGroup = []transactions.SignedTxn{txn3.Sign(secrets[0])} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagLeaseEval)) + handler.checkAlreadyCommitted(&wi) + require.Equal(t, 1, getCheckMetricCounter(txPoolRememberTagLeaseEval)) + + // TODO: not sure how to trigger fee error - need to return ErrNoSpace from ledger + // trigger pool fee error + // txn1.Fee = basics.MicroAlgos{Raw: proto.MinTxnFee / 2} + // wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0])} + // handler.postProcessCheckedTxn(&wi) + // require.Equal(t, 1, getMetricCounter(txPoolRememberFee)) + + // make an invalid block to fail recompute pool and expose transactionMessageTxGroupRememberNoPendingEval metric + blockTicker := &blockTicker{cond: *sync.NewCond(&deadlock.Mutex{})} + blockListeners := []realledger.BlockListener{ + handler.txPool, + blockTicker, + } + ledger.RegisterBlockListeners(blockListeners) + + hdr = bookkeeping.BlockHeader{ + Round: 1, + UpgradeState: bookkeeping.UpgradeState{ + CurrentProtocol: "test", + }, + } + + blk := bookkeeping.Block{ + BlockHeader: hdr, + Payset: []transactions.SignedTxnInBlock{{}}, + } + vb := ledgercore.MakeValidatedBlock(blk, ledgercore.StateDelta{}) + err = ledger.AddValidatedBlock(vb, agreement.Certificate{}) + require.NoError(t, err) + blockTicker.Wait() + + wi.unverifiedTxGroup = []transactions.SignedTxn{} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberPendingEval)) +} diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000000..c40a21ed28 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,83 @@ +# Algod Container + +General purpose algod docker container. + + +# Image Configuration + +There are a number of special files and environment variables used to control how a container is started. + +## Default Configuration + +By default the following config.json overrides are applied: + +| Setting | Value | +| ------- | ----- | +| GossipFanout | 1 | +| EndpointAddress | 0.0.0.0:8080 | +| IncomingConnectionsLimit | 0 | +| Archival | false | +| IsIndexerActive | false | +| EnableDeveloperAPI | true | + +## Environment Variables + +The following environment variables can be supplied. Except when noted, it is possible to reconfigure deployments even after the data directory has been initialized. + +| Variable | Description | +| -------- | ----------- | +| NETWORK | Leave blank for a private network, otherwise specify one of mainnet, betanet, testnet, or devnet. Only used during a data directory initialization. | +| FAST_CATCHUP | If set on a public network, attempt to start fast-catchup during initial config. | +| TELEMETRY_NAME| If set on a public network, telemetry is reported with this name. | +| DEV_MODE | If set on a private network, enable dev mode. Only used during data directory initialization. | +| NUM_ROUNDS | If set on a private network, override default of 30000 participation keys. | +| TOKEN | If set, overrides the REST API token. | +| ADMIN_TOKEN | If set, overrides the REST API admin token. | + + +## Special Files + +Configuration can be modified by specifying certian files. These can be changed each time you start the container if the data directory is a mounted volume. + +| File | Description | +| ---- | ----------- | +| /etc/config.json | Override default configurations by providing your own file. | +| /etc/algod.token | Override default randomized REST API token. | +| /etc/algod.admin.token | Override default randomized REST API admin token. | + +TODO: `/etc/template.json` for overriding the private network topology. + +# Example Configuration + +The following command launches a container configured with one of the public networks: +``` +docker run --rm -it \ + -p 4190:8080 \ + -e NETWORK=mainnet \ + -e FAST_CATCHUP=1 \ + -e TELEMETRY_NAME=name \ + -e TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ + -v ${PWD}/data:/algod/data/ \ + --name mainnet-container \ + algorand/algod:latest +``` + +Explanation of parts: +* `-p 4190:8080` maps the internal algod REST API to local port 4190 +* `-e NETWORK=` can be set to any of the supported public networks. +* `-e FAST_CATCHUP=` causes fast catchup to start shortly after launching the network. +* `-e TELEMETRY_NAME=` enables telemetry reporting to Algorand for network health analysis. +* `-e TOKEN=` sets the REST API token to use. +* `-v ${PWD}/data:/algod/data/` mounts a local volume to the data directory, which can be used to restart and upgrad the deployment. + + +# Mounting the Data Directory + +The data directory located at `/algod/data`. Mounting a volume at that location will allow you to shutdown and resume the node. + +## Private Network + +Private networks work a little bit differently. They are configured with, potentially, several data directories. The default topology supplied with this container is installed to `/algod/`, and has a single node named `data`. This means the private network has a data directory at `/algod/data`, matching the production configuration. + +Because the root directory contains some metadata, if persistence of the private network is required, you should mount the volume `/algod/` instead of `/algod/data`. This will ensure the extra metadata is included when changing images. + diff --git a/docker/files/build/install.sh b/docker/files/build/install.sh new file mode 100755 index 0000000000..20d5766e47 --- /dev/null +++ b/docker/files/build/install.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Script to install algod in all sorts of different ways. +# +# Parameters: +# -d : Location where binaries will be installed. +# -c : Channel to install. Mutually exclusive with source options. +# -u : Git repository URL. Mutually exclusive with -c. +# -b : Git branch. Mutually exclusive with -c. +# -s : (optional) Git Commit SHA hash. Mutually exclusive with -c. + +set -e + +rootdir=$(dirname "$0") +pushd "$rootdir" + +BINDIR="" +CHANNEL="" +URL="" +BRANCH="" +SHA="" + +while getopts "p:d:c:u:b:s:" opt; do + case "$opt" in + p) BINDIR=$OPTARG; ;; + d) ALGORAND_DATA=$OPTARG; ;; + c) CHANNEL=$OPTARG; ;; + u) URL=$OPTARG; ;; + b) BRANCH=$OPTARG; ;; + s) SHA=$OPTARG; ;; + *) echo "unknown flag"; exit 1;; + esac +done + +if [ -z "$BINDIR" ]; then + echo "-d is required." + exit 1 +fi + +if [ -n "$CHANNEL" ] && [ -n "$BRANCH" ]; then + echo "Set only one of -c or -b " + exit 1 +fi + +if [ -n "$BRANCH" ] && [ -z "$URL" ]; then + echo "If using -b , must also set -u " + exit 1 +fi + +echo "Installing algod with options:" +echo " BINDIR = ${BINDIR}" +echo " DATADIR = ${ALGORAND_DATA}" +echo " CHANNEL = ${CHANNEL}" +echo " URL = ${URL}" +echo " BRANCH = ${BRANCH}" +echo " SHA = ${SHA}" + +if [ -n "$CHANNEL" ] && [ -n "$BRANCH" ]; then + echo "Do not provide CHANNEL and BRANCH." + exit 1 +fi + +# Deploy from release channel. +if [ -n "$CHANNEL" ]; then + ./update.sh -i -c "$CHANNEL" -p "$BINDIR" -d "${ALGORAND_DATA}" -n + exit 0 +fi + +# Build from source. +if [ -n "$BRANCH" ]; then + git clone --single-branch --branch "${BRANCH}" "${URL}" +else + git clone "${URL}" +fi + +cd go-algorand +if [ "${SHA}" != "" ]; then + echo "Checking out ${SHA}" + git checkout "${SHA}" +fi + +git log -n 5 + +./scripts/configure_dev.sh +make build +./scripts/dev_install.sh -p "${BINDIR}" -d "${ALGORAND_DATA}" + +"$BINDIR"/algod -v diff --git a/docker/files/build/kmd_config.json.example b/docker/files/build/kmd_config.json.example new file mode 100644 index 0000000000..0165dd0a63 --- /dev/null +++ b/docker/files/build/kmd_config.json.example @@ -0,0 +1,19 @@ +{ + "drivers": { + "sqlite": { + "wallets_dir": "", + "allow_unsafe_scrypt": false, + "scrypt": { + "scrypt_n": 65536, + "scrypt_r": 1, + "scrypt_p": 32 + } + }, + "ledger": { + "disable": false + } + }, + "session_lifetime_secs": 60, + "address": "", + "allowed_origins": null +} diff --git a/docker/files/run/devmode_template.json b/docker/files/run/devmode_template.json new file mode 100644 index 0000000000..8e756502b5 --- /dev/null +++ b/docker/files/run/devmode_template.json @@ -0,0 +1,46 @@ +{ + "Genesis": { + "ConsensusProtocol": "future", + "NetworkName": "devmodenet", + "FirstPartKeyRound": 0, + "LastPartKeyRound": NUM_ROUNDS, + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 40, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 40, + "Online": true + }, + { + "Name": "Wallet3", + "Stake": 20, + "Online": true + } + ], + "DevMode": true + }, + "Nodes": [ + { + "Name": "data", + "IsRelay": false, + "Wallets": [ + { + "Name": "Wallet1", + "ParticipationOnly": false + }, + { + "Name": "Wallet2", + "ParticipationOnly": false + }, + { + "Name": "Wallet3", + "ParticipationOnly": false + } + ] + } + ] +} diff --git a/docker/files/run/future_template.json b/docker/files/run/future_template.json new file mode 100644 index 0000000000..d80128c580 --- /dev/null +++ b/docker/files/run/future_template.json @@ -0,0 +1,54 @@ +{ + "Genesis": { + "ConsensusProtocol": "future", + "NetworkName": "", + "FirstPartKeyRound": 0, + "LastPartKeyRound": NUM_ROUNDS, + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 10, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 40, + "Online": true + }, + { + "Name": "Wallet3", + "Stake": 40, + "Online": false + }, + { + "Name": "Wallet4", + "Stake": 10, + "Online": false + } + ] + }, + "Nodes": [ + { + "Name": "data", + "IsRelay": true, + "Wallets": [ + { + "Name": "Wallet1", + "ParticipationOnly": false + }, + { + "Name": "Wallet2", + "ParticipationOnly": false + }, + { + "Name": "Wallet3", + "ParticipationOnly": false + }, + { + "Name": "Wallet4", + "ParticipationOnly": false + } + ] + } + ] +} diff --git a/docker/files/run/run.sh b/docker/files/run/run.sh new file mode 100755 index 0000000000..027c0cc7fe --- /dev/null +++ b/docker/files/run/run.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash + +set -ex + +# Script to configure or resume a network. Based on environment settings the +# node will be setup with a private network or connect to a public network. + +#################### +# Helper functions # +#################### + +function apply_configuration() { + cd "$ALGORAND_DATA" + + # check for config file overrides. + if [ -f "/etc/config.json" ]; then + cp /etc/config.json config.json + fi + if [ -f "/etc/algod.token" ]; then + cp /etc/algod.token algod.token + fi + if [ -f "/etc/algod.admin.token" ]; then + cp /etc/algod.admin.token algod.admin.token + fi + + # check for environment variable overrides. + if [ "$TOKEN" != "" ]; then + echo "$TOKEN" > algod.token + fi + if [ "$ADMIN_TOKEN" != "" ]; then + echo "$ADMIN_TOKEN" > algod.admin.token + fi + + # configure telemetry + if [ "$TELEMETRY_NAME" != "" ]; then + diagcfg telemetry name -n "$TELEMETRY_NAME" -d "$ALGORAND_DATA" + diagcfg telemetry enable -d "$ALGORAND_DATA" + else + diagcfg telemetry disable + fi +} + +function catchup() { + local FAST_CATCHUP_URL="https://algorand-catchpoints.s3.us-east-2.amazonaws.com/channel/CHANNEL/latest.catchpoint" + local CATCHPOINT=$(curl -s ${FAST_CATCHUP_URL/CHANNEL/$NETWORK}) + if [[ "$(echo $CATCHPOINT | wc -l | tr -d ' ')" != "1" ]]; then + echo "Problem starting fast catchup." + exit 1 + fi + + sleep 5 + goal node catchup "$CATCHPOINT" +} + +function start_public_network() { + cd "$ALGORAND_DATA" + + apply_configuration + + if [ $FAST_CATCHUP ]; then + catchup& + fi + # redirect output to stdout + algod -o +} + +function configure_data_dir() { + cd "$ALGORAND_DATA" + algocfg -d . set -p GossipFanout -v 1 + algocfg -d . set -p EndpointAddress -v "0.0.0.0:${ALGOD_PORT}" + algocfg -d . set -p IncomingConnectionsLimit -v 0 + algocfg -d . set -p Archival -v false + algocfg -d . set -p IsIndexerActive -v false + algocfg -d . set -p EnableDeveloperAPI -v true +} + +function start_new_public_network() { + cd /node + if [ ! -d "run/genesis/$NETWORK" ]; then + echo "No genesis file for '$NETWORK' is available." + exit 1 + fi + + mkdir -p "$ALGORAND_DATA" + mv dataTemplate/* "$ALGORAND_DATA" + rm -rf dataTemplate + + cp "run/genesis/$NETWORK/genesis.json" "$ALGORAND_DATA/genesis.json" + cd "$ALGORAND_DATA" + + mv config.json.example config.json + configure_data_dir + + local ID + case $NETWORK in + mainnet) ID=".algorand.network";; + testnet) ID=".algorand.network";; + betanet) ID=".algodev.network";; + alphanet) ID=".algodev.network";; + devnet) ID=".algodev.network";; + *) echo "Unknown network"; exit 1;; + esac + set -p DNSBootstrapID -v "$ID" + + start_public_network +} + +function start_private_network() { + apply_configuration + + # TODO: Is there a way to properly exec a private network? + goal network start -r "$ALGORAND_DATA/.." + tail -f "$ALGORAND_DATA/node.log" +} + +function start_new_private_network() { + cd /node + local TEMPLATE="template.json" + if [ "$DEV_MODE" ]; then + TEMPLATE="devmode_template.json" + fi + sed -i "s/NUM_ROUNDS/${NUM_ROUNDS:-30000}/" "run/$TEMPLATE" + goal network create -n dockernet -r "$ALGORAND_DATA/.." -t "run/$TEMPLATE" + configure_data_dir + start_private_network +} + +############## +# Entrypoint # +############## + +echo "Starting Algod Docker Container" +echo " ALGORAND_DATA: $ALGORAND_DATA" +echo " NETWORK: $NETWORK" +echo " ALGOD_PORT: $ALGOD_PORT" +echo " FAST_CATCHUP: $FAST_CATCHUP" +echo " DEV_MODE: $DEV_MODE" +echo " TOKEN: $TOKEN" +echo " TELEMETRY_NAME $TELEMETRY_NAME" + +# If data directory is initialized, start existing environment. +if [ -f "$ALGORAND_DATA/../network.json" ]; then + start_private_network + exit 1 +elif [ -f "$ALGORAND_DATA/genesis.json" ]; then + start_public_network + exit 1 +fi + +# Initialize and start network. +if [ "$NETWORK" == "" ]; then + start_new_private_network +else + start_new_public_network +fi diff --git a/docker/files/run/template.json b/docker/files/run/template.json new file mode 100644 index 0000000000..6e3f3cfe9c --- /dev/null +++ b/docker/files/run/template.json @@ -0,0 +1,52 @@ +{ + "Genesis": { + "NetworkName": "", + "FirstPartKeyRound": 0, + "LastPartKeyRound": NUM_ROUNDS, + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 10, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 40, + "Online": true + }, + { + "Name": "Wallet3", + "Stake": 40, + "Online": false + }, + { + "Name": "Wallet4", + "Stake": 10, + "Online": false + } + ] + }, + "Nodes": [ + { + "Name": "data", + "Wallets": [ + { + "Name": "Wallet1", + "ParticipationOnly": false + }, + { + "Name": "Wallet2", + "ParticipationOnly": false + }, + { + "Name": "Wallet3", + "ParticipationOnly": false + }, + { + "Name": "Wallet4", + "ParticipationOnly": false + } + ] + } + ] +} diff --git a/go.mod b/go.mod index 3ef58a8804..d605367586 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( github.com/DataDog/zstd v1.5.2 - github.com/algorand/avm-abi v0.1.0 + github.com/algorand/avm-abi v0.1.1 github.com/algorand/falcon v0.0.0-20220727072124-02a2a64c4414 github.com/algorand/go-codec/codec v1.1.8 github.com/algorand/go-deadlock v0.2.2 @@ -63,6 +63,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect golang.org/x/net v0.1.0 // indirect diff --git a/go.sum b/go.sum index 8f3ebcf269..300b5f8a1c 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwS github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/algorand/avm-abi v0.1.0 h1:znZFQXpSUVYz37vXbaH5OZG2VK4snTyXwnc/tV9CVr4= github.com/algorand/avm-abi v0.1.0/go.mod h1:+CgwM46dithy850bpTeHh9MC99zpn2Snirb3QTl2O/g= +github.com/algorand/avm-abi v0.1.1 h1:dbyQKzXiyaEbzpmqXFB30yAhyqseBsyqXTyZbNbkh2Y= +github.com/algorand/avm-abi v0.1.1/go.mod h1:+CgwM46dithy850bpTeHh9MC99zpn2Snirb3QTl2O/g= github.com/algorand/falcon v0.0.0-20220727072124-02a2a64c4414 h1:nwYN+GQ7Z5OOfZwqBO1ma7DSlP7S1YrKWICOyjkwqrc= github.com/algorand/falcon v0.0.0-20220727072124-02a2a64c4414/go.mod h1:OkQyHlGvS0kLNcIWbC21/uQcnbfwSOQm+wiqWwBG9pQ= github.com/algorand/go-codec v1.1.8/go.mod h1:XhzVs6VVyWMLu6cApb9/192gBjGRVGm5cX5j203Heg4= @@ -126,6 +128,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/installer/config.json.example b/installer/config.json.example index 84476092e4..9002388088 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -1,5 +1,5 @@ { - "Version": 25, + "Version": 26, "AccountUpdatesStatsInterval": 5000000000, "AccountsRebuildSynchronousMode": 1, "AgreementIncomingBundlesQueueLength": 7, @@ -98,6 +98,7 @@ "TelemetryToLog": true, "TransactionSyncDataExchangeRate": 0, "TransactionSyncSignificantMessageThreshold": 0, + "TxIncomingFilteringFlags": 1, "TxPoolExponentialIncreaseFactor": 2, "TxPoolSize": 75000, "TxSyncIntervalSeconds": 60, diff --git a/ledger/accountdb.go b/ledger/accountdb.go index 9b8d418287..c82a4e5ee8 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -20,271 +20,23 @@ import ( "bytes" "context" "database/sql" - "encoding/hex" "errors" "fmt" "math" - "strings" - "time" - - "github.com/mattn/go-sqlite3" "github.com/algorand/msgp/msgp" "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/crypto/merklesignature" - "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/ledger/ledgercore" - "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-algorand/util/db" ) -// accountsDbQueries is used to cache a prepared SQL statement to look up -// the state of a single account. -type accountsDbQueries struct { - listCreatablesStmt *sql.Stmt - lookupStmt *sql.Stmt - lookupResourcesStmt *sql.Stmt - lookupAllResourcesStmt *sql.Stmt - lookupKvPairStmt *sql.Stmt - lookupKeysByRangeStmt *sql.Stmt - lookupCreatorStmt *sql.Stmt -} - -type onlineAccountsDbQueries struct { - lookupOnlineStmt *sql.Stmt - lookupOnlineHistoryStmt *sql.Stmt - lookupOnlineTotalsStmt *sql.Stmt -} - -var accountsSchema = []string{ - `CREATE TABLE IF NOT EXISTS acctrounds ( - id string primary key, - rnd integer)`, - `CREATE TABLE IF NOT EXISTS accounttotals ( - id string primary key, - online integer, - onlinerewardunits integer, - offline integer, - offlinerewardunits integer, - notparticipating integer, - notparticipatingrewardunits integer, - rewardslevel integer)`, - `CREATE TABLE IF NOT EXISTS accountbase ( - address blob primary key, - data blob)`, - `CREATE TABLE IF NOT EXISTS assetcreators ( - asset integer primary key, - creator blob)`, - `CREATE TABLE IF NOT EXISTS storedcatchpoints ( - round integer primary key, - filename text NOT NULL, - catchpoint text NOT NULL, - filesize size NOT NULL, - pinned integer NOT NULL)`, - `CREATE TABLE IF NOT EXISTS accounthashes ( - id integer primary key, - data blob)`, - `CREATE TABLE IF NOT EXISTS catchpointstate ( - id string primary key, - intval integer, - 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`, -} - -// createNormalizedOnlineBalanceIndex handles accountbase/catchpointbalances tables -func createNormalizedOnlineBalanceIndex(idxname string, tablename string) string { - return fmt.Sprintf(`CREATE INDEX IF NOT EXISTS %s - ON %s ( normalizedonlinebalance, address, data ) WHERE normalizedonlinebalance>0`, idxname, tablename) -} - -// createNormalizedOnlineBalanceIndexOnline handles onlineaccounts/catchpointonlineaccounts tables -func createNormalizedOnlineBalanceIndexOnline(idxname string, tablename string) string { - return fmt.Sprintf(`CREATE INDEX IF NOT EXISTS %s - ON %s ( normalizedonlinebalance, address )`, idxname, tablename) -} - -func createUniqueAddressBalanceIndex(idxname string, tablename string) string { - return fmt.Sprintf(`CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (address)`, idxname, tablename) -} - -var createOnlineAccountIndex = []string{ - `ALTER TABLE accountbase - ADD COLUMN normalizedonlinebalance INTEGER`, - createNormalizedOnlineBalanceIndex("onlineaccountbals", "accountbase"), -} - -var createResourcesTable = []string{ - `CREATE TABLE IF NOT EXISTS resources ( - addrid INTEGER NOT NULL, - aidx INTEGER NOT NULL, - data BLOB NOT NULL, - PRIMARY KEY (addrid, aidx) ) WITHOUT ROWID`, -} - -var createBoxTable = []string{ - `CREATE TABLE IF NOT EXISTS kvstore ( - key blob primary key, - value blob)`, -} - -var createOnlineAccountsTable = []string{ - `CREATE TABLE IF NOT EXISTS onlineaccounts ( - address BLOB NOT NULL, - updround INTEGER NOT NULL, - normalizedonlinebalance INTEGER NOT NULL, - votelastvalid INTEGER NOT NULL, - data BLOB NOT NULL, - PRIMARY KEY (address, updround) )`, - createNormalizedOnlineBalanceIndexOnline("onlineaccountnorm", "onlineaccounts"), -} - -var createTxTailTable = []string{ - `CREATE TABLE IF NOT EXISTS txtail ( - rnd INTEGER PRIMARY KEY NOT NULL, - data BLOB NOT NULL)`, -} - -var createOnlineRoundParamsTable = []string{ - `CREATE TABLE IF NOT EXISTS onlineroundparamstail( - rnd INTEGER NOT NULL PRIMARY KEY, - data BLOB NOT NULL)`, // contains a msgp encoded OnlineRoundParamsData -} - -// Table containing some metadata for a future catchpoint. The `info` column -// contains a serialized object of type catchpointFirstStageInfo. -const createCatchpointFirstStageInfoTable = ` - CREATE TABLE IF NOT EXISTS catchpointfirststageinfo ( - round integer primary key NOT NULL, - info BLOB NOT NULL)` - -const createUnfinishedCatchpointsTable = ` - CREATE TABLE IF NOT EXISTS unfinishedcatchpoints ( - round integer primary key NOT NULL, - blockhash blob NOT NULL)` - -var accountsResetExprs = []string{ - `DROP TABLE IF EXISTS acctrounds`, - `DROP TABLE IF EXISTS accounttotals`, - `DROP TABLE IF EXISTS accountbase`, - `DROP TABLE IF EXISTS kvstore`, - `DROP TABLE IF EXISTS assetcreators`, - `DROP TABLE IF EXISTS storedcatchpoints`, - `DROP TABLE IF EXISTS catchpointstate`, - `DROP TABLE IF EXISTS accounthashes`, - `DROP TABLE IF EXISTS resources`, - `DROP TABLE IF EXISTS onlineaccounts`, - `DROP TABLE IF EXISTS txtail`, - `DROP TABLE IF EXISTS onlineroundparamstail`, - `DROP TABLE IF EXISTS catchpointfirststageinfo`, - `DROP TABLE IF EXISTS unfinishedcatchpoints`, -} - -// 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(9) - -// persistedAccountData is used for representing a single account stored on the disk. In addition to the -// basics.AccountData, it also stores complete referencing information used to maintain the base accounts -// list. -type persistedAccountData struct { - // The address of the account. In contrasts to maps, having this value explicitly here allows us to use this - // data structure in queues directly, without "attaching" the address as the address as the map key. - addr basics.Address - // The underlaying account data - accountData baseAccountData - // The rowid, when available. If the entry was loaded from the disk, then we have the rowid for it. Entries - // that doesn't have rowid ( hence, rowid == 0 ) represent either deleted accounts or non-existing accounts. - rowid int64 - // the round number that is associated with the accountData. This field is needed so that we can maintain a correct - // lruAccounts cache. We use it to ensure that the entries on the lruAccounts.accountsList are the latest ones. - // this becomes an issue since while we attempt to write an update to disk, we might be reading an entry and placing - // it on the lruAccounts.pendingAccounts; The commitRound doesn't attempt to flush the pending accounts, but rather - // just write the latest ( which is correct ) to the lruAccounts.accountsList. later on, during on newBlockImpl, we - // want to ensure that the "real" written value isn't being overridden by the value from the pending accounts. - round basics.Round -} - -type persistedOnlineAccountData struct { - addr basics.Address - accountData baseOnlineAccountData - rowid int64 - // the round number that is associated with the baseOnlineAccountData. This field is the corresponding one to the round field - // in persistedAccountData, and serves the same purpose. This value comes from account rounds table and correspond to - // the last trackers db commit round. - round basics.Round - // the round number that the online account is for, i.e. account state change round. - updRound basics.Round -} - -//msgp:ignore persistedResourcesData -type persistedResourcesData struct { - // addrid is the rowid of the account address that holds this resource. - // it is used in update/delete operations so must be filled for existing records. - // resolution is a multi stage process: - // - baseResources cache might have valid entries - // - baseAccount cache might have an entry for the address with rowid set - // - when loading non-cached resources in resourcesLoadOld - // - when creating new accounts in accountsNewRound - addrid int64 - // creatable index - aidx basics.CreatableIndex - // actual resource data - data resourcesData - // the round number that is associated with the resourcesData. This field is the corresponding one to the round field - // in persistedAccountData, and serves the same purpose. - round basics.Round -} - -func (prd *persistedResourcesData) AccountResource() ledgercore.AccountResource { - var ret ledgercore.AccountResource - if prd.data.IsAsset() { - if prd.data.IsHolding() { - holding := prd.data.GetAssetHolding() - ret.AssetHolding = &holding - } - if prd.data.IsOwning() { - assetParams := prd.data.GetAssetParams() - ret.AssetParams = &assetParams - } - } - if prd.data.IsApp() { - if prd.data.IsHolding() { - localState := prd.data.GetAppLocalState() - ret.AppLocalState = &localState - } - if prd.data.IsOwning() { - appParams := prd.data.GetAppParams() - ret.AppParams = &appParams - } - } - return ret -} - -//msgp:ignore persistedKVData -type persistedKVData struct { - // kv value - value []byte - // the round number that is associated with the kv value. This field is the corresponding one to the round field - // in persistedAccountData, and serves the same purpose. - round basics.Round -} - // resourceDelta is used as part of the compactResourcesDeltas to describe a change to a single resource. type resourceDelta struct { - oldResource persistedResourcesData - newResource resourcesData + oldResource store.PersistedResourcesData + newResource store.ResourcesData nAcctDeltas int address basics.Address } @@ -302,8 +54,8 @@ type compactResourcesDeltas struct { } type accountDelta struct { - oldAcct persistedAccountData - newAcct baseAccountData + oldAcct store.PersistedAccountData + newAcct store.BaseAccountData nAcctDeltas int address basics.Address } @@ -325,8 +77,8 @@ type compactAccountDeltas struct { // oldAcct represents the "old" state of the account in the DB, and compared against newAcct[0] // to determine if the acct became online or went offline. type onlineAccountDelta struct { - oldAcct persistedOnlineAccountData - newAcct []baseOnlineAccountData + oldAcct store.PersistedOnlineAccountData + newAcct []store.BaseOnlineAccountData nOnlineAcctDeltas int address basics.Address updRound []uint64 @@ -342,37 +94,6 @@ type compactOnlineAccountDeltas struct { misses []int } -// catchpointState is used to store catchpoint related variables into the catchpointstate table. -type catchpointState string - -const ( - // catchpointStateLastCatchpoint is written by a node once a catchpoint label is created for a round - catchpointStateLastCatchpoint = catchpointState("lastCatchpoint") - // This state variable is set to 1 if catchpoint's first stage is unfinished, - // and is 0 otherwise. Used to clear / restart the first stage after a crash. - // This key is set in the same db transaction as the account updates, so the - // unfinished first stage corresponds to the current db round. - catchpointStateWritingFirstStageInfo = catchpointState("writingFirstStageInfo") - // If there is an unfinished catchpoint, this state variable is set to - // the catchpoint's round. Otherwise, it is set to 0. - // DEPRECATED. - catchpointStateWritingCatchpoint = catchpointState("writingCatchpoint") - // catchpointCatchupState is the state of the catchup process. The variable is stored only during the catchpoint catchup process, and removed afterward. - catchpointStateCatchupState = catchpointState("catchpointCatchupState") - // catchpointStateCatchupLabel is the label to which the currently catchpoint catchup process is trying to catchup to. - catchpointStateCatchupLabel = catchpointState("catchpointCatchupLabel") - // catchpointCatchupBlockRound is the block round that is associated with the current running catchpoint catchup. - catchpointStateCatchupBlockRound = catchpointState("catchpointCatchupBlockRound") - // catchpointStateCatchupBalancesRound is the balance round that is associated with the current running catchpoint catchup. Typically it would be - // equal to catchpointStateCatchupBlockRound - 320. - catchpointStateCatchupBalancesRound = catchpointState("catchpointCatchupBalancesRound") - // catchpointStateCatchupHashRound is the round that is associated with the hash of the merkle trie. Normally, it's identical to catchpointStateCatchupBalancesRound, - // however, it could differ when we catchup from a catchpoint that was created using a different version : in this case, - // we set it to zero in order to reset the merkle trie. This would force the merkle trie to be re-build on startup ( if needed ). - catchpointStateCatchupHashRound = catchpointState("catchpointCatchupHashRound") - catchpointStateCatchpointLookback = catchpointState("catchpointLookback") -) - // MaxEncodedBaseAccountDataSize is a rough estimate for the worst-case scenario we're going to have of the base account data serialized. // this number is verified by the TestEncodedBaseAccountDataSize function. const MaxEncodedBaseAccountDataSize = 350 @@ -381,109 +102,88 @@ const MaxEncodedBaseAccountDataSize = 350 // this number is verified by the TestEncodedBaseResourceSize function. const MaxEncodedBaseResourceDataSize = 20000 -// normalizedAccountBalance is a staging area for a catchpoint file account information before it's being added to the catchpoint staging tables. -type normalizedAccountBalance struct { - // The public key address to which the account belongs. - address basics.Address - // accountData contains the baseAccountData for that account. - accountData baseAccountData - // resources is a map, where the key is the creatable index, and the value is the resource data. - resources map[basics.CreatableIndex]resourcesData - // encodedAccountData contains the baseAccountData encoded bytes that are going to be written to the accountbase table. - encodedAccountData []byte - // accountHashes contains a list of all the hashes that would need to be added to the merkle trie for that account. - // on V6, we could have multiple hashes, since we have separate account/resource hashes. - accountHashes [][]byte - // normalizedBalance contains the normalized balance for the account. - normalizedBalance uint64 - // encodedResources provides the encoded form of the resources - encodedResources map[basics.CreatableIndex][]byte - // partial balance indicates that the original account balance was split into multiple parts in catchpoint creation time - partialBalance bool -} - // prepareNormalizedBalancesV5 converts an array of encodedBalanceRecordV5 into an equal size array of normalizedAccountBalances. -func prepareNormalizedBalancesV5(bals []encodedBalanceRecordV5, proto config.ConsensusParams) (normalizedAccountBalances []normalizedAccountBalance, err error) { - normalizedAccountBalances = make([]normalizedAccountBalance, len(bals)) +func prepareNormalizedBalancesV5(bals []encodedBalanceRecordV5, proto config.ConsensusParams) (normalizedAccountBalances []store.NormalizedAccountBalance, err error) { + normalizedAccountBalances = make([]store.NormalizedAccountBalance, len(bals)) for i, balance := range bals { - normalizedAccountBalances[i].address = balance.Address + normalizedAccountBalances[i].Address = balance.Address var accountDataV5 basics.AccountData err = protocol.Decode(balance.AccountData, &accountDataV5) if err != nil { return nil, err } - normalizedAccountBalances[i].accountData.SetAccountData(&accountDataV5) - normalizedAccountBalances[i].normalizedBalance = accountDataV5.NormalizedOnlineBalance(proto) + normalizedAccountBalances[i].AccountData.SetAccountData(&accountDataV5) + normalizedAccountBalances[i].NormalizedBalance = accountDataV5.NormalizedOnlineBalance(proto) type resourcesRow struct { aidx basics.CreatableIndex - resourcesData + store.ResourcesData } var resources []resourcesRow - addResourceRow := func(_ context.Context, _ int64, aidx basics.CreatableIndex, rd *resourcesData) error { - resources = append(resources, resourcesRow{aidx: aidx, resourcesData: *rd}) + addResourceRow := func(_ context.Context, _ int64, aidx basics.CreatableIndex, rd *store.ResourcesData) error { + resources = append(resources, resourcesRow{aidx: aidx, ResourcesData: *rd}) return nil } - if err = accountDataResources(context.Background(), &accountDataV5, 0, addResourceRow); err != nil { + if err = store.AccountDataResources(context.Background(), &accountDataV5, 0, addResourceRow); err != nil { return nil, err } - normalizedAccountBalances[i].accountHashes = make([][]byte, 1) - normalizedAccountBalances[i].accountHashes[0] = accountHashBuilder(balance.Address, accountDataV5, balance.AccountData) + normalizedAccountBalances[i].AccountHashes = make([][]byte, 1) + normalizedAccountBalances[i].AccountHashes[0] = store.AccountHashBuilder(balance.Address, accountDataV5, balance.AccountData) if len(resources) > 0 { - normalizedAccountBalances[i].resources = make(map[basics.CreatableIndex]resourcesData, len(resources)) - normalizedAccountBalances[i].encodedResources = make(map[basics.CreatableIndex][]byte, len(resources)) + normalizedAccountBalances[i].Resources = make(map[basics.CreatableIndex]store.ResourcesData, len(resources)) + normalizedAccountBalances[i].EncodedResources = make(map[basics.CreatableIndex][]byte, len(resources)) } for _, resource := range resources { - normalizedAccountBalances[i].resources[resource.aidx] = resource.resourcesData - normalizedAccountBalances[i].encodedResources[resource.aidx] = protocol.Encode(&resource.resourcesData) + normalizedAccountBalances[i].Resources[resource.aidx] = resource.ResourcesData + normalizedAccountBalances[i].EncodedResources[resource.aidx] = protocol.Encode(&resource.ResourcesData) } - normalizedAccountBalances[i].encodedAccountData = protocol.Encode(&normalizedAccountBalances[i].accountData) + normalizedAccountBalances[i].EncodedAccountData = protocol.Encode(&normalizedAccountBalances[i].AccountData) } return } // prepareNormalizedBalancesV6 converts an array of encodedBalanceRecordV6 into an equal size array of normalizedAccountBalances. -func prepareNormalizedBalancesV6(bals []encodedBalanceRecordV6, proto config.ConsensusParams) (normalizedAccountBalances []normalizedAccountBalance, err error) { - normalizedAccountBalances = make([]normalizedAccountBalance, len(bals)) +func prepareNormalizedBalancesV6(bals []encodedBalanceRecordV6, proto config.ConsensusParams) (normalizedAccountBalances []store.NormalizedAccountBalance, err error) { + normalizedAccountBalances = make([]store.NormalizedAccountBalance, len(bals)) for i, balance := range bals { - normalizedAccountBalances[i].address = balance.Address - err = protocol.Decode(balance.AccountData, &(normalizedAccountBalances[i].accountData)) + normalizedAccountBalances[i].Address = balance.Address + err = protocol.Decode(balance.AccountData, &(normalizedAccountBalances[i].AccountData)) if err != nil { return nil, err } - normalizedAccountBalances[i].normalizedBalance = basics.NormalizedOnlineAccountBalance( - normalizedAccountBalances[i].accountData.Status, - normalizedAccountBalances[i].accountData.RewardsBase, - normalizedAccountBalances[i].accountData.MicroAlgos, + normalizedAccountBalances[i].NormalizedBalance = basics.NormalizedOnlineAccountBalance( + normalizedAccountBalances[i].AccountData.Status, + normalizedAccountBalances[i].AccountData.RewardsBase, + normalizedAccountBalances[i].AccountData.MicroAlgos, proto) - normalizedAccountBalances[i].encodedAccountData = balance.AccountData + normalizedAccountBalances[i].EncodedAccountData = balance.AccountData curHashIdx := 0 if balance.ExpectingMoreEntries { // There is a single chunk in the catchpoint file with ExpectingMoreEntries // set to false for this account. There may be multiple chunks with // ExpectingMoreEntries set to true. In this case, we do not have to add the // account's own hash to accountHashes. - normalizedAccountBalances[i].accountHashes = make([][]byte, len(balance.Resources)) - normalizedAccountBalances[i].partialBalance = true + normalizedAccountBalances[i].AccountHashes = make([][]byte, len(balance.Resources)) + normalizedAccountBalances[i].PartialBalance = true } else { - normalizedAccountBalances[i].accountHashes = make([][]byte, 1+len(balance.Resources)) - normalizedAccountBalances[i].accountHashes[0] = accountHashBuilderV6(balance.Address, &normalizedAccountBalances[i].accountData, balance.AccountData) + normalizedAccountBalances[i].AccountHashes = make([][]byte, 1+len(balance.Resources)) + normalizedAccountBalances[i].AccountHashes[0] = store.AccountHashBuilderV6(balance.Address, &normalizedAccountBalances[i].AccountData, balance.AccountData) curHashIdx++ } if len(balance.Resources) > 0 { - normalizedAccountBalances[i].resources = make(map[basics.CreatableIndex]resourcesData, len(balance.Resources)) - normalizedAccountBalances[i].encodedResources = make(map[basics.CreatableIndex][]byte, len(balance.Resources)) + normalizedAccountBalances[i].Resources = make(map[basics.CreatableIndex]store.ResourcesData, len(balance.Resources)) + normalizedAccountBalances[i].EncodedResources = make(map[basics.CreatableIndex][]byte, len(balance.Resources)) for cidx, res := range balance.Resources { - var resData resourcesData + var resData store.ResourcesData err = protocol.Decode(res, &resData) if err != nil { return nil, err } - normalizedAccountBalances[i].accountHashes[curHashIdx], err = resourcesHashBuilderV6(&resData, balance.Address, basics.CreatableIndex(cidx), resData.UpdateRound, res) + normalizedAccountBalances[i].AccountHashes[curHashIdx], err = store.ResourcesHashBuilderV6(&resData, balance.Address, basics.CreatableIndex(cidx), resData.UpdateRound, res) if err != nil { return nil, err } - normalizedAccountBalances[i].resources[basics.CreatableIndex(cidx)] = resData - normalizedAccountBalances[i].encodedResources[basics.CreatableIndex(cidx)] = res + normalizedAccountBalances[i].Resources[basics.CreatableIndex(cidx)] = resData + normalizedAccountBalances[i].EncodedResources[basics.CreatableIndex(cidx)] = res curHashIdx++ } } @@ -491,17 +191,17 @@ func prepareNormalizedBalancesV6(bals []encodedBalanceRecordV6, proto config.Con return } -// makeCompactResourceDeltas takes an array of AccountDeltas ( one array entry per round ), and compacts the resource portions of the arrays into a single +// makeCompactResourceDeltas takes an array of StateDeltas containing AccountDeltas ( one array entry per round ), and compacts the resource portions of the AccountDeltas into a single // data structure that contains all the resources deltas changes. While doing that, the function eliminate any intermediate resources changes. // It counts the number of changes each account get modified across the round range by specifying it in the nAcctDeltas field of the resourcesDeltas. -// As an optimization, accountDeltas is passed as a slice and must not be modified. -func makeCompactResourceDeltas(accountDeltas []ledgercore.AccountDeltas, baseRound basics.Round, setUpdateRound bool, baseAccounts lruAccounts, baseResources lruResources) (outResourcesDeltas compactResourcesDeltas) { - if len(accountDeltas) == 0 { +// As an optimization, stateDeltas is passed as a slice and must not be modified. +func makeCompactResourceDeltas(stateDeltas []ledgercore.StateDelta, baseRound basics.Round, setUpdateRound bool, baseAccounts lruAccounts, baseResources lruResources) (outResourcesDeltas compactResourcesDeltas) { + if len(stateDeltas) == 0 { return } // the sizes of the maps here aren't super accurate, but would hopefully be a rough estimate for a reasonable starting point. - size := accountDeltas[0].Len()*len(accountDeltas) + 1 + size := stateDeltas[0].Accts.Len()*len(stateDeltas) + 1 outResourcesDeltas.cache = make(map[accountCreatable]int, size) outResourcesDeltas.deltas = make([]resourceDelta, 0, size) outResourcesDeltas.misses = make([]int, 0, size) @@ -514,7 +214,8 @@ func makeCompactResourceDeltas(accountDeltas []ledgercore.AccountDeltas, baseRou if setUpdateRound { updateRoundMultiplier = 1 } - for _, roundDelta := range accountDeltas { + for _, stateDelta := range stateDeltas { + roundDelta := stateDelta.Accts deltaRound++ // assets for _, res := range roundDelta.GetAllAssetResources() { @@ -534,21 +235,21 @@ func makeCompactResourceDeltas(accountDeltas []ledgercore.AccountDeltas, baseRou newEntry := resourceDelta{ nAcctDeltas: 1, address: res.Addr, - newResource: makeResourcesData(deltaRound * updateRoundMultiplier), + newResource: store.MakeResourcesData(deltaRound * updateRoundMultiplier), } newEntry.newResource.SetAssetData(res.Params, res.Holding) // baseResources caches deleted entries, and they have addrid = 0 // need to handle this and prevent such entries to be treated as fully resolved baseResourceData, has := baseResources.read(res.Addr, basics.CreatableIndex(res.Aidx)) - existingAcctCacheEntry := has && baseResourceData.addrid != 0 + existingAcctCacheEntry := has && baseResourceData.Addrid != 0 if existingAcctCacheEntry { newEntry.oldResource = baseResourceData outResourcesDeltas.insert(newEntry) } else { if pad, has := baseAccounts.read(res.Addr); has { - newEntry.oldResource = persistedResourcesData{addrid: pad.rowid} + newEntry.oldResource = store.PersistedResourcesData{Addrid: pad.Rowid} } - newEntry.oldResource.aidx = basics.CreatableIndex(res.Aidx) + newEntry.oldResource.Aidx = basics.CreatableIndex(res.Aidx) outResourcesDeltas.insertMissing(newEntry) } } @@ -572,19 +273,19 @@ func makeCompactResourceDeltas(accountDeltas []ledgercore.AccountDeltas, baseRou newEntry := resourceDelta{ nAcctDeltas: 1, address: res.Addr, - newResource: makeResourcesData(deltaRound * updateRoundMultiplier), + newResource: store.MakeResourcesData(deltaRound * updateRoundMultiplier), } newEntry.newResource.SetAppData(res.Params, res.State) baseResourceData, has := baseResources.read(res.Addr, basics.CreatableIndex(res.Aidx)) - existingAcctCacheEntry := has && baseResourceData.addrid != 0 + existingAcctCacheEntry := has && baseResourceData.Addrid != 0 if existingAcctCacheEntry { newEntry.oldResource = baseResourceData outResourcesDeltas.insert(newEntry) } else { if pad, has := baseAccounts.read(res.Addr); has { - newEntry.oldResource = persistedResourcesData{addrid: pad.rowid} + newEntry.oldResource = store.PersistedResourcesData{Addrid: pad.Rowid} } - newEntry.oldResource.aidx = basics.CreatableIndex(res.Aidx) + newEntry.oldResource.Aidx = basics.CreatableIndex(res.Aidx) outResourcesDeltas.insertMissing(newEntry) } } @@ -600,17 +301,7 @@ func (a *compactResourcesDeltas) resourcesLoadOld(tx *sql.Tx, knownAddresses map if len(a.misses) == 0 { return nil } - selectStmt, err := tx.Prepare("SELECT data FROM resources WHERE addrid = ? AND aidx = ?") - if err != nil { - return - } - defer selectStmt.Close() - - addrRowidStmt, err := tx.Prepare("SELECT rowid FROM accountbase WHERE address=?") - if err != nil { - return - } - defer addrRowidStmt.Close() + arw := store.NewAccountsSQLReaderWriter(tx) defer func() { a.misses = nil @@ -622,11 +313,11 @@ func (a *compactResourcesDeltas) resourcesLoadOld(tx *sql.Tx, knownAddresses map for _, missIdx := range a.misses { delta := a.deltas[missIdx] addr := delta.address - aidx = delta.oldResource.aidx - if delta.oldResource.addrid != 0 { - addrid = delta.oldResource.addrid + aidx = delta.oldResource.Aidx + if delta.oldResource.Addrid != 0 { + addrid = delta.oldResource.Addrid } else if addrid, ok = knownAddresses[addr]; !ok { - err = addrRowidStmt.QueryRow(addr[:]).Scan(&addrid) + addrid, err = arw.LookupAccountRowID(addr) if err != nil { if err != sql.ErrNoRows { err = fmt.Errorf("base account cannot be read while processing resource for addr=%s, aidx=%d: %w", addr.String(), aidx, err) @@ -639,13 +330,12 @@ func (a *compactResourcesDeltas) resourcesLoadOld(tx *sql.Tx, knownAddresses map continue } } - resDataBuf = nil - err = selectStmt.QueryRow(addrid, aidx).Scan(&resDataBuf) + resDataBuf, err = arw.LookupResourceDataByAddrID(addrid, aidx) switch err { case nil: if len(resDataBuf) > 0 { - persistedResData := persistedResourcesData{addrid: addrid, aidx: aidx} - err = protocol.Decode(resDataBuf, &persistedResData.data) + persistedResData := store.PersistedResourcesData{Addrid: addrid, Aidx: aidx} + err = protocol.Decode(resDataBuf, &persistedResData.Data) if err != nil { return err } @@ -656,7 +346,7 @@ func (a *compactResourcesDeltas) resourcesLoadOld(tx *sql.Tx, knownAddresses map } case sql.ErrNoRows: // we don't have that account, just return an empty record. - a.updateOld(missIdx, persistedResourcesData{addrid: addrid, aidx: aidx}) + a.updateOld(missIdx, store.PersistedResourcesData{Addrid: addrid, Aidx: aidx}) err = nil default: // unexpected error - let the caller know that we couldn't complete the operation. @@ -696,7 +386,7 @@ func (a *compactResourcesDeltas) insert(delta resourceDelta) int { if a.cache == nil { a.cache = make(map[accountCreatable]int) } - a.cache[accountCreatable{address: delta.address, index: delta.oldResource.aidx}] = last + a.cache[accountCreatable{address: delta.address, index: delta.oldResource.Aidx}] = last return last } @@ -705,21 +395,21 @@ func (a *compactResourcesDeltas) insertMissing(delta resourceDelta) { } // updateOld updates existing or inserts a new partial entry with only old field filled -func (a *compactResourcesDeltas) updateOld(idx int, old persistedResourcesData) { +func (a *compactResourcesDeltas) updateOld(idx int, old store.PersistedResourcesData) { a.deltas[idx].oldResource = old } -// makeCompactAccountDeltas takes an array of account AccountDeltas ( one array entry per round ), and compacts the arrays into a single +// makeCompactAccountDeltas takes an array of account StateDeltas with AccountDeltas ( one array entry per round ), and compacts the AccountDeltas into a single // data structure that contains all the account deltas changes. While doing that, the function eliminate any intermediate account changes. // It counts the number of changes each account get modified across the round range by specifying it in the nAcctDeltas field of the accountDeltaCount/modifiedCreatable. -// As an optimization, accountDeltas is passed as a slice and must not be modified. -func makeCompactAccountDeltas(accountDeltas []ledgercore.AccountDeltas, baseRound basics.Round, setUpdateRound bool, baseAccounts lruAccounts) (outAccountDeltas compactAccountDeltas) { - if len(accountDeltas) == 0 { +// As an optimization, stateDeltas is passed as a slice and must not be modified. +func makeCompactAccountDeltas(stateDeltas []ledgercore.StateDelta, baseRound basics.Round, setUpdateRound bool, baseAccounts lruAccounts) (outAccountDeltas compactAccountDeltas) { + if len(stateDeltas) == 0 { return } // the sizes of the maps here aren't super accurate, but would hopefully be a rough estimate for a reasonable starting point. - size := accountDeltas[0].Len()*len(accountDeltas) + 1 + size := stateDeltas[0].Accts.Len()*len(stateDeltas) + 1 outAccountDeltas.cache = make(map[basics.Address]int, size) outAccountDeltas.deltas = make([]accountDelta, 0, size) outAccountDeltas.misses = make([]int, 0, size) @@ -732,7 +422,8 @@ func makeCompactAccountDeltas(accountDeltas []ledgercore.AccountDeltas, baseRoun if setUpdateRound { updateRoundMultiplier = 1 } - for _, roundDelta := range accountDeltas { + for _, stateDelta := range stateDeltas { + roundDelta := stateDelta.Accts deltaRound++ for i := 0; i < roundDelta.Len(); i++ { addr, acctDelta := roundDelta.GetByIdx(i) @@ -749,7 +440,7 @@ func makeCompactAccountDeltas(accountDeltas []ledgercore.AccountDeltas, baseRoun // it's a new entry. newEntry := accountDelta{ nAcctDeltas: 1, - newAcct: baseAccountData{ + newAcct: store.BaseAccountData{ UpdateRound: deltaRound * updateRoundMultiplier, }, address: addr, @@ -774,35 +465,29 @@ func (a *compactAccountDeltas) accountsLoadOld(tx *sql.Tx) (err error) { if len(a.misses) == 0 { return nil } - selectStmt, err := tx.Prepare("SELECT rowid, data FROM accountbase WHERE address=?") - if err != nil { - return - } - defer selectStmt.Close() + arw := store.NewAccountsSQLReaderWriter(tx) defer func() { a.misses = nil }() - var rowid sql.NullInt64 - var acctDataBuf []byte for _, idx := range a.misses { addr := a.deltas[idx].address - err = selectStmt.QueryRow(addr[:]).Scan(&rowid, &acctDataBuf) + rowid, acctDataBuf, err := arw.LookupAccountDataByAddress(addr) switch err { case nil: if len(acctDataBuf) > 0 { - persistedAcctData := &persistedAccountData{addr: addr, rowid: rowid.Int64} - err = protocol.Decode(acctDataBuf, &persistedAcctData.accountData) + persistedAcctData := &store.PersistedAccountData{Addr: addr, Rowid: rowid} + err = protocol.Decode(acctDataBuf, &persistedAcctData.AccountData) if err != nil { return err } a.updateOld(idx, *persistedAcctData) } else { // to retain backward compatibility, we will treat this condition as if we don't have the account. - a.updateOld(idx, persistedAccountData{addr: addr, rowid: rowid.Int64}) + a.updateOld(idx, store.PersistedAccountData{Addr: addr, Rowid: rowid}) } case sql.ErrNoRows: // we don't have that account, just return an empty record. - a.updateOld(idx, persistedAccountData{addr: addr}) + a.updateOld(idx, store.PersistedAccountData{Addr: addr}) err = nil default: // unexpected error - let the caller know that we couldn't complete the operation. @@ -852,12 +537,12 @@ func (a *compactAccountDeltas) insertMissing(delta accountDelta) { } // updateOld updates existing or inserts a new partial entry with only old field filled -func (a *compactAccountDeltas) updateOld(idx int, old persistedAccountData) { +func (a *compactAccountDeltas) updateOld(idx int, old store.PersistedAccountData) { a.deltas[idx].oldAcct = old } func (c *onlineAccountDelta) append(acctDelta ledgercore.AccountData, deltaRound basics.Round) { - var baseEntry baseOnlineAccountData + var baseEntry store.BaseOnlineAccountData baseEntry.SetCoreAccountData(&acctDelta) c.newAcct = append(c.newAcct, baseEntry) c.updRound = append(c.updRound, uint64(deltaRound)) @@ -916,37 +601,29 @@ func (a *compactOnlineAccountDeltas) accountsLoadOld(tx *sql.Tx) (err error) { if len(a.misses) == 0 { return nil } - // fetch the latest entry - selectStmt, err := tx.Prepare("SELECT rowid, data FROM onlineaccounts WHERE address=? ORDER BY updround DESC LIMIT 1") - if err != nil { - return - } - defer selectStmt.Close() + arw := store.NewAccountsSQLReaderWriter(tx) defer func() { a.misses = nil }() - var rowid sql.NullInt64 - var acctDataBuf []byte for _, idx := range a.misses { addr := a.deltas[idx].address - err = selectStmt.QueryRow(addr[:]).Scan(&rowid, &acctDataBuf) + rowid, acctDataBuf, err := arw.LookupOnlineAccountDataByAddress(addr) switch err { case nil: if len(acctDataBuf) > 0 { - persistedAcctData := &persistedOnlineAccountData{addr: addr, rowid: rowid.Int64} - err = protocol.Decode(acctDataBuf, &persistedAcctData.accountData) + persistedAcctData := &store.PersistedOnlineAccountData{Addr: addr, Rowid: rowid} + err = protocol.Decode(acctDataBuf, &persistedAcctData.AccountData) if err != nil { return err } a.updateOld(idx, *persistedAcctData) } else { // empty data means offline account - a.updateOld(idx, persistedOnlineAccountData{addr: addr, rowid: rowid.Int64}) + a.updateOld(idx, store.PersistedOnlineAccountData{Addr: addr, Rowid: rowid}) } case sql.ErrNoRows: // we don't have that account, just return an empty record. - a.updateOld(idx, persistedOnlineAccountData{addr: addr}) - err = nil + a.updateOld(idx, store.PersistedOnlineAccountData{Addr: addr}) default: // unexpected error - let the caller know that we couldn't complete the operation. return err @@ -995,2748 +672,181 @@ func (a *compactOnlineAccountDeltas) insertMissing(delta onlineAccountDelta) { } // updateOld updates existing or inserts a new partial entry with only old field filled -func (a *compactOnlineAccountDeltas) updateOld(idx int, old persistedOnlineAccountData) { +func (a *compactOnlineAccountDeltas) updateOld(idx int, old store.PersistedOnlineAccountData) { a.deltas[idx].oldAcct = old } -// writeCatchpointStagingBalances inserts all the account balances in the provided array into the catchpoint balance staging table catchpointbalances. -func writeCatchpointStagingBalances(ctx context.Context, tx *sql.Tx, bals []normalizedAccountBalance) error { - selectAcctStmt, err := tx.PrepareContext(ctx, "SELECT rowid FROM catchpointbalances WHERE address = ?") - if err != nil { - return err +// accountDataToOnline returns the part of the AccountData that matters +// for online accounts (to answer top-N queries). We store a subset of +// the full AccountData because we need to store a large number of these +// in memory (say, 1M), and storing that many AccountData could easily +// cause us to run out of memory. +func accountDataToOnline(address basics.Address, ad *ledgercore.AccountData, proto config.ConsensusParams) *ledgercore.OnlineAccount { + return &ledgercore.OnlineAccount{ + Address: address, + MicroAlgos: ad.MicroAlgos, + RewardsBase: ad.RewardsBase, + NormalizedOnlineBalance: ad.NormalizedOnlineBalance(proto), + VoteFirstValid: ad.VoteFirstValid, + VoteLastValid: ad.VoteLastValid, + StateProofID: ad.StateProofID, } +} - insertAcctStmt, err := tx.PrepareContext(ctx, "INSERT INTO catchpointbalances(address, normalizedonlinebalance, data) VALUES(?, ?, ?)") - if err != nil { - return err - } +// accountsNewRound is a convenience wrapper for accountsNewRoundImpl +func accountsNewRound( + tx *sql.Tx, + updates compactAccountDeltas, resources compactResourcesDeltas, kvPairs map[string]modifiedKvValue, creatables map[basics.CreatableIndex]ledgercore.ModifiedCreatable, + proto config.ConsensusParams, lastUpdateRound basics.Round, +) (updatedAccounts []store.PersistedAccountData, updatedResources map[basics.Address][]store.PersistedResourcesData, updatedKVs map[string]store.PersistedKVData, err error) { + hasAccounts := updates.len() > 0 + hasResources := resources.len() > 0 + hasKvPairs := len(kvPairs) > 0 + hasCreatables := len(creatables) > 0 - insertRscStmt, err := tx.PrepareContext(ctx, "INSERT INTO catchpointresources(addrid, aidx, data) VALUES(?, ?, ?)") + writer, err := store.MakeAccountsSQLWriter(tx, hasAccounts, hasResources, hasKvPairs, hasCreatables) if err != nil { - return err + return } + defer writer.Close() - var result sql.Result - var rowID int64 - for _, balance := range bals { - result, err = insertAcctStmt.ExecContext(ctx, balance.address[:], balance.normalizedBalance, balance.encodedAccountData) - if err == nil { - var aff int64 - aff, err = result.RowsAffected() - if err != nil { - return err - } - if aff != 1 { - return fmt.Errorf("number of affected record in insert was expected to be one, but was %d", aff) - } - rowID, err = result.LastInsertId() - if err != nil { - return err - } - } else { - var sqliteErr sqlite3.Error - if errors.As(err, &sqliteErr) && sqliteErr.Code == sqlite3.ErrConstraint && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { - // address exists: overflowed account record: find addrid - err = selectAcctStmt.QueryRowContext(ctx, balance.address[:]).Scan(&rowID) - if err != nil { - return err - } - } else { - return err - } - } - - // write resources - for aidx := range balance.resources { - var result sql.Result - result, err = insertRscStmt.ExecContext(ctx, rowID, aidx, balance.encodedResources[aidx]) - if err != nil { - return err - } - var aff int64 - aff, err = result.RowsAffected() - if err != nil { - return err - } - if aff != 1 { - return fmt.Errorf("number of affected record in insert was expected to be one, but was %d", aff) - } - } - } - return nil + return accountsNewRoundImpl(writer, updates, resources, kvPairs, creatables, proto, lastUpdateRound) } -// writeCatchpointStagingHashes inserts all the account hashes in the provided array into the catchpoint pending hashes table catchpointpendinghashes. -func writeCatchpointStagingHashes(ctx context.Context, tx *sql.Tx, bals []normalizedAccountBalance) error { - insertStmt, err := tx.PrepareContext(ctx, "INSERT INTO catchpointpendinghashes(data) VALUES(?)") - if err != nil { - return err - } - - for _, balance := range bals { - for _, hash := range balance.accountHashes { - result, err := insertStmt.ExecContext(ctx, hash[:]) - if err != nil { - return err - } - - aff, err := result.RowsAffected() - if err != nil { - return err - } - if aff != 1 { - return fmt.Errorf("number of affected record in insert was expected to be one, but was %d", aff) - } - } - } - return nil -} +func onlineAccountsNewRound( + tx *sql.Tx, + updates compactOnlineAccountDeltas, + proto config.ConsensusParams, lastUpdateRound basics.Round, +) (updatedAccounts []store.PersistedOnlineAccountData, err error) { + hasAccounts := updates.len() > 0 -// createCatchpointStagingHashesIndex creates an index on catchpointpendinghashes to allow faster scanning according to the hash order -func createCatchpointStagingHashesIndex(ctx context.Context, tx *sql.Tx) (err error) { - _, err = tx.ExecContext(ctx, "CREATE INDEX IF NOT EXISTS catchpointpendinghashesidx ON catchpointpendinghashes(data)") + writer, err := store.MakeOnlineAccountsSQLWriter(tx, hasAccounts) if err != nil { return } + defer writer.Close() + + updatedAccounts, err = onlineAccountsNewRoundImpl(writer, updates, proto, lastUpdateRound) return } -// writeCatchpointStagingCreatable inserts all the creatables in the provided array into the catchpoint asset creator staging table catchpointassetcreators. -// note that we cannot insert the resources here : in order to insert the resources, we need the rowid of the accountbase entry. This is being inserted by -// writeCatchpointStagingBalances via a separate go-routine. -func writeCatchpointStagingCreatable(ctx context.Context, tx *sql.Tx, bals []normalizedAccountBalance) error { - var insertCreatorsStmt *sql.Stmt - var err error - insertCreatorsStmt, err = tx.PrepareContext(ctx, "INSERT INTO catchpointassetcreators(asset, creator, ctype) VALUES(?, ?, ?)") - if err != nil { - return err - } - defer insertCreatorsStmt.Close() - - for _, balance := range bals { - for aidx, resData := range balance.resources { - if resData.IsOwning() { - // determine if it's an asset - if resData.IsAsset() { - _, err := insertCreatorsStmt.ExecContext(ctx, aidx, balance.address[:], basics.AssetCreatable) - if err != nil { - return err +// accountsNewRoundImpl updates the accountbase and assetcreators tables by applying the provided deltas to the accounts / creatables. +// The function returns a persistedAccountData for the modified accounts which can be stored in the base cache. +func accountsNewRoundImpl( + writer store.AccountsWriter, + updates compactAccountDeltas, resources compactResourcesDeltas, kvPairs map[string]modifiedKvValue, creatables map[basics.CreatableIndex]ledgercore.ModifiedCreatable, + proto config.ConsensusParams, lastUpdateRound basics.Round, +) (updatedAccounts []store.PersistedAccountData, updatedResources map[basics.Address][]store.PersistedResourcesData, updatedKVs map[string]store.PersistedKVData, err error) { + updatedAccounts = make([]store.PersistedAccountData, updates.len()) + updatedAccountIdx := 0 + newAddressesRowIDs := make(map[basics.Address]int64) + for i := 0; i < updates.len(); i++ { + data := updates.getByIdx(i) + if data.oldAcct.Rowid == 0 { + // zero rowid means we don't have a previous value. + if data.newAcct.IsEmpty() { + // IsEmpty means we don't have a previous value. Note, can't use newAcct.MsgIsZero + // because of non-zero UpdateRound field in a new delta + // if we didn't had it before, and we don't have anything now, just skip it. + } else { + // create a new entry. + var rowid int64 + normBalance := data.newAcct.NormalizedOnlineBalance(proto) + rowid, err = writer.InsertAccount(data.address, normBalance, data.newAcct) + if err == nil { + updatedAccounts[updatedAccountIdx].Rowid = rowid + updatedAccounts[updatedAccountIdx].AccountData = data.newAcct + newAddressesRowIDs[data.address] = rowid + } + } + } else { + // non-zero rowid means we had a previous value. + if data.newAcct.IsEmpty() { + // new value is zero, which means we need to delete the current value. + var rowsAffected int64 + rowsAffected, err = writer.DeleteAccount(data.oldAcct.Rowid) + if err == nil { + // we deleted the entry successfully. + updatedAccounts[updatedAccountIdx].Rowid = 0 + updatedAccounts[updatedAccountIdx].AccountData = store.BaseAccountData{} + if rowsAffected != 1 { + err = fmt.Errorf("failed to delete accountbase row for account %v, rowid %d", data.address, data.oldAcct.Rowid) } } - // determine if it's an application - if resData.IsApp() { - _, err := insertCreatorsStmt.ExecContext(ctx, aidx, balance.address[:], basics.AppCreatable) - if err != nil { - return err + } else { + var rowsAffected int64 + normBalance := data.newAcct.NormalizedOnlineBalance(proto) + rowsAffected, err = writer.UpdateAccount(data.oldAcct.Rowid, normBalance, data.newAcct) + if err == nil { + // rowid doesn't change on update. + updatedAccounts[updatedAccountIdx].Rowid = data.oldAcct.Rowid + updatedAccounts[updatedAccountIdx].AccountData = data.newAcct + if rowsAffected != 1 { + err = fmt.Errorf("failed to update accountbase row for account %v, rowid %d", data.address, data.oldAcct.Rowid) } } } } - } - return nil -} - -// writeCatchpointStagingKVs inserts all the KVs in the provided array into the -// catchpoint kvstore staging table catchpointkvstore, and their hashes to the pending -func writeCatchpointStagingKVs(ctx context.Context, tx *sql.Tx, kvrs []encodedKVRecordV6) error { - insertKV, err := tx.PrepareContext(ctx, "INSERT INTO catchpointkvstore(key, value) VALUES(?, ?)") - if err != nil { - return err - } - defer insertKV.Close() - - insertHash, err := tx.PrepareContext(ctx, "INSERT INTO catchpointpendinghashes(data) VALUES(?)") - if err != nil { - return err - } - defer insertHash.Close() - for _, kvr := range kvrs { - _, err := insertKV.ExecContext(ctx, kvr.Key, kvr.Value) if err != nil { - return err + return } - hash := kvHashBuilderV6(string(kvr.Key), kvr.Value) - _, err = insertHash.ExecContext(ctx, hash) - if err != nil { - return err - } + // set the returned persisted account states so that we could store that as the baseAccounts in commitRound + updatedAccounts[updatedAccountIdx].Round = lastUpdateRound + updatedAccounts[updatedAccountIdx].Addr = data.address + updatedAccountIdx++ } - return nil -} -func resetCatchpointStagingBalances(ctx context.Context, tx *sql.Tx, newCatchup bool) (err error) { - s := []string{ - "DROP TABLE IF EXISTS catchpointbalances", - "DROP TABLE IF EXISTS catchpointassetcreators", - "DROP TABLE IF EXISTS catchpointaccounthashes", - "DROP TABLE IF EXISTS catchpointpendinghashes", - "DROP TABLE IF EXISTS catchpointresources", - "DROP TABLE IF EXISTS catchpointkvstore", - "DELETE FROM accounttotals where id='catchpointStaging'", - } + updatedResources = make(map[basics.Address][]store.PersistedResourcesData) - if newCatchup { - // SQLite has no way to rename an existing index. So, we need - // to cook up a fresh name for the index, which will be kept - // around after we rename the table from "catchpointbalances" - // to "accountbase". To construct a unique index name, we - // use the current time. - // Apply the same logic to - now := time.Now().UnixNano() - idxnameBalances := fmt.Sprintf("onlineaccountbals_idx_%d", now) - idxnameAddress := fmt.Sprintf("accountbase_address_idx_%d", now) - - s = append(s, - "CREATE TABLE IF NOT EXISTS catchpointassetcreators (asset integer primary key, creator blob, ctype integer)", - "CREATE TABLE IF NOT EXISTS catchpointbalances (addrid INTEGER PRIMARY KEY NOT NULL, address blob NOT NULL, data blob, normalizedonlinebalance INTEGER)", - "CREATE TABLE IF NOT EXISTS catchpointpendinghashes (data blob)", - "CREATE TABLE IF NOT EXISTS catchpointaccounthashes (id integer primary key, data blob)", - "CREATE TABLE IF NOT EXISTS catchpointresources (addrid INTEGER NOT NULL, aidx INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY (addrid, aidx) ) WITHOUT ROWID", - "CREATE TABLE IF NOT EXISTS catchpointkvstore (key blob primary key, value blob)", - - createNormalizedOnlineBalanceIndex(idxnameBalances, "catchpointbalances"), // should this be removed ? - createUniqueAddressBalanceIndex(idxnameAddress, "catchpointbalances"), - ) + // the resources update is going to be made in three parts: + // on the first loop, we will find out all the entries that need to be deleted, and parepare a pendingResourcesDeletion map. + // on the second loop, we will perform update/insertion. when considering inserting, we would test the pendingResourcesDeletion to see + // if the said entry was scheduled to be deleted. If so, we would "upgrade" the insert operation into an update operation. + // on the last loop, we would delete the remainder of the resource entries that were detected in loop #1 and were not upgraded in loop #2. + // the rationale behind this is that addrid might get reused, and we need to ensure + // that at all times there are no two representations of the same entry in the resources table. + // ( which would trigger a constrain violation ) + type resourceKey struct { + addrid int64 + aidx basics.CreatableIndex } - - for _, stmt := range s { - _, err = tx.Exec(stmt) - if err != nil { - return err + var pendingResourcesDeletion map[resourceKey]struct{} // map to indicate which resources need to be deleted + for i := 0; i < resources.len(); i++ { + data := resources.getByIdx(i) + if data.oldResource.Addrid == 0 || data.oldResource.Data.IsEmpty() || !data.newResource.IsEmpty() { + continue } - } - - return nil -} - -// applyCatchpointStagingBalances switches the staged catchpoint catchup tables onto the actual -// tables and update the correct balance round. This is the final step in switching onto the new catchpoint round. -func applyCatchpointStagingBalances(ctx context.Context, tx *sql.Tx, balancesRound basics.Round, merkleRootRound basics.Round) (err error) { - stmts := []string{ - "DROP TABLE IF EXISTS accountbase", - "DROP TABLE IF EXISTS assetcreators", - "DROP TABLE IF EXISTS accounthashes", - "DROP TABLE IF EXISTS resources", - "DROP TABLE IF EXISTS kvstore", - - "ALTER TABLE catchpointbalances RENAME TO accountbase", - "ALTER TABLE catchpointassetcreators RENAME TO assetcreators", - "ALTER TABLE catchpointaccounthashes RENAME TO accounthashes", - "ALTER TABLE catchpointresources RENAME TO resources", - "ALTER TABLE catchpointkvstore RENAME TO kvstore", - } - - for _, stmt := range stmts { - _, err = tx.Exec(stmt) - if err != nil { - return err + if pendingResourcesDeletion == nil { + pendingResourcesDeletion = make(map[resourceKey]struct{}) } - } - - _, err = tx.Exec("INSERT OR REPLACE INTO acctrounds(id, rnd) VALUES('acctbase', ?)", balancesRound) - if err != nil { - return err - } - - _, err = tx.Exec("INSERT OR REPLACE INTO acctrounds(id, rnd) VALUES('hashbase', ?)", merkleRootRound) - if err != nil { - return err - } - - return -} - -func getCatchpoint(ctx context.Context, q db.Queryable, round basics.Round) (fileName string, catchpoint string, fileSize int64, err error) { - err = q.QueryRowContext(ctx, "SELECT filename, catchpoint, filesize FROM storedcatchpoints WHERE round=?", int64(round)).Scan(&fileName, &catchpoint, &fileSize) - return -} + pendingResourcesDeletion[resourceKey{addrid: data.oldResource.Addrid, aidx: data.oldResource.Aidx}] = struct{}{} -// accountsInit fills the database using tx with initAccounts if the -// database has not been initialized yet. -// -// accountsInit returns nil if either it has initialized the database -// correctly, or if the database has already been initialized. -func accountsInit(tx *sql.Tx, initAccounts map[basics.Address]basics.AccountData, proto config.ConsensusParams) (newDatabase bool, err error) { - for _, tableCreate := range accountsSchema { - _, err = tx.Exec(tableCreate) - if err != nil { - return - } - } - - // 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 - } - } - } else if err != nil { - return - } - - _, err = tx.Exec("INSERT INTO acctrounds (id, rnd) VALUES ('acctbase', 0)") - if err == nil { - var ot basics.OverflowTracker - var totals ledgercore.AccountTotals - - for addr, data := range initAccounts { - _, err = tx.Exec("INSERT INTO accountbase (address, data) VALUES (?, ?)", - addr[:], protocol.Encode(&data)) - if err != nil { - return true, err - } - - ad := ledgercore.ToAccountData(data) - totals.AddAccount(proto, ad, &ot) - } - - if ot.Overflowed { - return true, fmt.Errorf("overflow computing totals") - } - - err = accountsPutTotals(tx, totals, false) - if err != nil { - return true, err - } - newDatabase = true - } else { - serr, ok := err.(sqlite3.Error) - // serr.Code is sqlite.ErrConstraint if the database has already been initialized; - // in that case, ignore the error and return nil. - if !ok || serr.Code != sqlite3.ErrConstraint { - return - } - - } - - return newDatabase, nil -} - -// accountsAddNormalizedBalance adds the normalizedonlinebalance column -// to the accountbase table. -func accountsAddNormalizedBalance(tx *sql.Tx, proto config.ConsensusParams) error { - var exists bool - err := tx.QueryRow("SELECT 1 FROM pragma_table_info('accountbase') WHERE name='normalizedonlinebalance'").Scan(&exists) - if err == nil { - // Already exists. - return nil - } - if err != sql.ErrNoRows { - return err - } - - for _, stmt := range createOnlineAccountIndex { - _, err := tx.Exec(stmt) - if err != nil { - return err - } - } - - rows, err := tx.Query("SELECT address, data FROM accountbase") - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var addrbuf []byte - var buf []byte - err = rows.Scan(&addrbuf, &buf) - if err != nil { - return err - } - - var data basics.AccountData - err = protocol.Decode(buf, &data) - if err != nil { - return err - } - - normBalance := data.NormalizedOnlineBalance(proto) - if normBalance > 0 { - _, err = tx.Exec("UPDATE accountbase SET normalizedonlinebalance=? WHERE address=?", normBalance, addrbuf) - if err != nil { - return err - } - } - } - - return rows.Err() -} - -// accountsCreateResourceTable creates the resource table in the database. -func accountsCreateResourceTable(ctx context.Context, tx *sql.Tx) error { - var exists bool - err := tx.QueryRowContext(ctx, "SELECT 1 FROM pragma_table_info('resources') WHERE name='addrid'").Scan(&exists) - if err == nil { - // Already exists. - return nil - } - if err != sql.ErrNoRows { - return err - } - for _, stmt := range createResourcesTable { - _, err = tx.ExecContext(ctx, stmt) - if err != nil { - return err - } - } - return nil -} - -func accountsCreateOnlineAccountsTable(ctx context.Context, tx *sql.Tx) error { - var exists bool - err := tx.QueryRowContext(ctx, "SELECT 1 FROM pragma_table_info('onlineaccounts') WHERE name='address'").Scan(&exists) - if err == nil { - // Already exists. - return nil - } - if err != sql.ErrNoRows { - return err - } - for _, stmt := range createOnlineAccountsTable { - _, err = tx.ExecContext(ctx, stmt) - if err != nil { - return err - } - } - return nil -} - -// accountsCreateBoxTable creates the KVStore table for box-storage in the database. -func accountsCreateBoxTable(ctx context.Context, tx *sql.Tx) error { - var exists bool - err := tx.QueryRow("SELECT 1 FROM pragma_table_info('kvstore') WHERE name='key'").Scan(&exists) - if err == nil { - // already exists - return nil - } - if err != sql.ErrNoRows { - return err - } - for _, stmt := range createBoxTable { - _, err = tx.ExecContext(ctx, stmt) - if err != nil { - return err - } - } - return nil -} - -func accountsCreateTxTailTable(ctx context.Context, tx *sql.Tx) (err error) { - for _, stmt := range createTxTailTable { - _, err = tx.ExecContext(ctx, stmt) - if err != nil { - return - } - } - return nil -} - -func accountsCreateOnlineRoundParamsTable(ctx context.Context, tx *sql.Tx) (err error) { - for _, stmt := range createOnlineRoundParamsTable { - _, err = tx.ExecContext(ctx, stmt) - if err != nil { - return - } - } - return nil -} - -func accountsCreateCatchpointFirstStageInfoTable(ctx context.Context, e db.Executable) error { - _, err := e.ExecContext(ctx, createCatchpointFirstStageInfoTable) - return err -} - -func accountsCreateUnfinishedCatchpointsTable(ctx context.Context, e db.Executable) error { - _, err := e.ExecContext(ctx, createUnfinishedCatchpointsTable) - return err -} - -type baseVotingData struct { - _struct struct{} `codec:",omitempty,omitemptyarray"` - - VoteID crypto.OneTimeSignatureVerifier `codec:"A"` - SelectionID crypto.VRFVerifier `codec:"B"` - VoteFirstValid basics.Round `codec:"C"` - VoteLastValid basics.Round `codec:"D"` - VoteKeyDilution uint64 `codec:"E"` - StateProofID merklesignature.Commitment `codec:"F"` -} - -type baseOnlineAccountData struct { - _struct struct{} `codec:",omitempty,omitemptyarray"` - - baseVotingData - - MicroAlgos basics.MicroAlgos `codec:"Y"` - RewardsBase uint64 `codec:"Z"` -} - -type baseAccountData struct { - _struct struct{} `codec:",omitempty,omitemptyarray"` - - Status basics.Status `codec:"a"` - MicroAlgos basics.MicroAlgos `codec:"b"` - RewardsBase uint64 `codec:"c"` - RewardedMicroAlgos basics.MicroAlgos `codec:"d"` - AuthAddr basics.Address `codec:"e"` - TotalAppSchemaNumUint uint64 `codec:"f"` - TotalAppSchemaNumByteSlice uint64 `codec:"g"` - TotalExtraAppPages uint32 `codec:"h"` - TotalAssetParams uint64 `codec:"i"` - TotalAssets uint64 `codec:"j"` - TotalAppParams uint64 `codec:"k"` - TotalAppLocalStates uint64 `codec:"l"` - TotalBoxes uint64 `codec:"m"` - TotalBoxBytes uint64 `codec:"n"` - - baseVotingData - - // UpdateRound is the round that modified this account data last. Since we want all the nodes to have the exact same - // value for this field, we'll be setting the value of this field to zero *before* the EnableAccountDataResourceSeparation - // consensus parameter is being set. Once the above consensus takes place, this field would be populated with the - // correct round number. - UpdateRound uint64 `codec:"z"` -} - -// IsEmpty return true if any of the fields other then the UpdateRound are non-zero. -func (ba *baseAccountData) IsEmpty() bool { - return ba.Status == 0 && - ba.MicroAlgos.Raw == 0 && - ba.RewardsBase == 0 && - ba.RewardedMicroAlgos.Raw == 0 && - ba.AuthAddr.IsZero() && - ba.TotalAppSchemaNumUint == 0 && - ba.TotalAppSchemaNumByteSlice == 0 && - ba.TotalExtraAppPages == 0 && - ba.TotalAssetParams == 0 && - ba.TotalAssets == 0 && - ba.TotalAppParams == 0 && - ba.TotalAppLocalStates == 0 && - ba.TotalBoxes == 0 && - ba.TotalBoxBytes == 0 && - ba.baseVotingData.IsEmpty() -} - -func (ba *baseAccountData) NormalizedOnlineBalance(proto config.ConsensusParams) uint64 { - return basics.NormalizedOnlineAccountBalance(ba.Status, ba.RewardsBase, ba.MicroAlgos, proto) -} - -func (ba *baseAccountData) SetCoreAccountData(ad *ledgercore.AccountData) { - ba.Status = ad.Status - ba.MicroAlgos = ad.MicroAlgos - ba.RewardsBase = ad.RewardsBase - ba.RewardedMicroAlgos = ad.RewardedMicroAlgos - ba.AuthAddr = ad.AuthAddr - ba.TotalAppSchemaNumUint = ad.TotalAppSchema.NumUint - ba.TotalAppSchemaNumByteSlice = ad.TotalAppSchema.NumByteSlice - ba.TotalExtraAppPages = ad.TotalExtraAppPages - ba.TotalAssetParams = ad.TotalAssetParams - ba.TotalAssets = ad.TotalAssets - ba.TotalAppParams = ad.TotalAppParams - ba.TotalAppLocalStates = ad.TotalAppLocalStates - ba.TotalBoxes = ad.TotalBoxes - ba.TotalBoxBytes = ad.TotalBoxBytes - - ba.baseVotingData.SetCoreAccountData(ad) -} - -func (ba *baseAccountData) SetAccountData(ad *basics.AccountData) { - ba.Status = ad.Status - ba.MicroAlgos = ad.MicroAlgos - ba.RewardsBase = ad.RewardsBase - ba.RewardedMicroAlgos = ad.RewardedMicroAlgos - ba.AuthAddr = ad.AuthAddr - ba.TotalAppSchemaNumUint = ad.TotalAppSchema.NumUint - ba.TotalAppSchemaNumByteSlice = ad.TotalAppSchema.NumByteSlice - ba.TotalExtraAppPages = ad.TotalExtraAppPages - ba.TotalAssetParams = uint64(len(ad.AssetParams)) - ba.TotalAssets = uint64(len(ad.Assets)) - ba.TotalAppParams = uint64(len(ad.AppParams)) - ba.TotalAppLocalStates = uint64(len(ad.AppLocalStates)) - ba.TotalBoxes = ad.TotalBoxes - ba.TotalBoxBytes = ad.TotalBoxBytes - - ba.baseVotingData.VoteID = ad.VoteID - ba.baseVotingData.SelectionID = ad.SelectionID - ba.baseVotingData.StateProofID = ad.StateProofID - ba.baseVotingData.VoteFirstValid = ad.VoteFirstValid - ba.baseVotingData.VoteLastValid = ad.VoteLastValid - ba.baseVotingData.VoteKeyDilution = ad.VoteKeyDilution -} - -func (ba *baseAccountData) GetLedgerCoreAccountData() ledgercore.AccountData { - return ledgercore.AccountData{ - AccountBaseData: ba.GetLedgerCoreAccountBaseData(), - VotingData: ba.GetLedgerCoreVotingData(), - } -} - -func (ba *baseAccountData) GetLedgerCoreAccountBaseData() ledgercore.AccountBaseData { - return ledgercore.AccountBaseData{ - Status: ba.Status, - MicroAlgos: ba.MicroAlgos, - RewardsBase: ba.RewardsBase, - RewardedMicroAlgos: ba.RewardedMicroAlgos, - AuthAddr: ba.AuthAddr, - TotalAppSchema: basics.StateSchema{ - NumUint: ba.TotalAppSchemaNumUint, - NumByteSlice: ba.TotalAppSchemaNumByteSlice, - }, - TotalExtraAppPages: ba.TotalExtraAppPages, - TotalAppParams: ba.TotalAppParams, - TotalAppLocalStates: ba.TotalAppLocalStates, - TotalAssetParams: ba.TotalAssetParams, - TotalAssets: ba.TotalAssets, - TotalBoxes: ba.TotalBoxes, - TotalBoxBytes: ba.TotalBoxBytes, - } -} - -func (ba *baseAccountData) GetLedgerCoreVotingData() ledgercore.VotingData { - return ledgercore.VotingData{ - VoteID: ba.VoteID, - SelectionID: ba.SelectionID, - StateProofID: ba.StateProofID, - VoteFirstValid: ba.VoteFirstValid, - VoteLastValid: ba.VoteLastValid, - VoteKeyDilution: ba.VoteKeyDilution, - } -} - -func (ba *baseAccountData) GetAccountData() basics.AccountData { - return basics.AccountData{ - Status: ba.Status, - MicroAlgos: ba.MicroAlgos, - RewardsBase: ba.RewardsBase, - RewardedMicroAlgos: ba.RewardedMicroAlgos, - AuthAddr: ba.AuthAddr, - TotalAppSchema: basics.StateSchema{ - NumUint: ba.TotalAppSchemaNumUint, - NumByteSlice: ba.TotalAppSchemaNumByteSlice, - }, - TotalExtraAppPages: ba.TotalExtraAppPages, - TotalBoxes: ba.TotalBoxes, - TotalBoxBytes: ba.TotalBoxBytes, - - VoteID: ba.VoteID, - SelectionID: ba.SelectionID, - StateProofID: ba.StateProofID, - VoteFirstValid: ba.VoteFirstValid, - VoteLastValid: ba.VoteLastValid, - VoteKeyDilution: ba.VoteKeyDilution, - } -} - -// IsEmpty returns true if all of the fields are zero. -func (bv baseVotingData) IsEmpty() bool { - return bv == baseVotingData{} -} - -// SetCoreAccountData initializes baseVotingData from ledgercore.AccountData -func (bv *baseVotingData) SetCoreAccountData(ad *ledgercore.AccountData) { - bv.VoteID = ad.VoteID - bv.SelectionID = ad.SelectionID - bv.StateProofID = ad.StateProofID - bv.VoteFirstValid = ad.VoteFirstValid - bv.VoteLastValid = ad.VoteLastValid - bv.VoteKeyDilution = ad.VoteKeyDilution -} - -// IsVotingEmpty checks if voting data fields are empty -func (bo *baseOnlineAccountData) IsVotingEmpty() bool { - return bo.baseVotingData.IsEmpty() -} - -// IsEmpty return true if any of the fields are non-zero. -func (bo *baseOnlineAccountData) IsEmpty() bool { - return bo.IsVotingEmpty() && - bo.MicroAlgos.Raw == 0 && - bo.RewardsBase == 0 -} - -// GetOnlineAccount returns ledgercore.OnlineAccount for top online accounts / voters -// TODO: unify -func (bo *baseOnlineAccountData) GetOnlineAccount(addr basics.Address, normBalance uint64) ledgercore.OnlineAccount { - return ledgercore.OnlineAccount{ - Address: addr, - MicroAlgos: bo.MicroAlgos, - RewardsBase: bo.RewardsBase, - NormalizedOnlineBalance: normBalance, - VoteFirstValid: bo.VoteFirstValid, - VoteLastValid: bo.VoteLastValid, - StateProofID: bo.StateProofID, - } -} - -// GetOnlineAccountData returns basics.OnlineAccountData for lookup agreement -// TODO: unify with GetOnlineAccount/ledgercore.OnlineAccount -func (bo *baseOnlineAccountData) GetOnlineAccountData(proto config.ConsensusParams, rewardsLevel uint64) ledgercore.OnlineAccountData { - microAlgos, _, _ := basics.WithUpdatedRewards( - proto, basics.Online, bo.MicroAlgos, basics.MicroAlgos{}, bo.RewardsBase, rewardsLevel, - ) - - return ledgercore.OnlineAccountData{ - MicroAlgosWithRewards: microAlgos, - VotingData: ledgercore.VotingData{ - VoteID: bo.VoteID, - SelectionID: bo.SelectionID, - StateProofID: bo.StateProofID, - VoteFirstValid: bo.VoteFirstValid, - VoteLastValid: bo.VoteLastValid, - VoteKeyDilution: bo.VoteKeyDilution, - }, - } -} - -func (bo *baseOnlineAccountData) NormalizedOnlineBalance(proto config.ConsensusParams) uint64 { - return basics.NormalizedOnlineAccountBalance(basics.Online, bo.RewardsBase, bo.MicroAlgos, proto) -} - -func (bo *baseOnlineAccountData) SetCoreAccountData(ad *ledgercore.AccountData) { - bo.baseVotingData.SetCoreAccountData(ad) - - // MicroAlgos/RewardsBase are updated by the evaluator when accounts are touched - bo.MicroAlgos = ad.MicroAlgos - bo.RewardsBase = ad.RewardsBase -} - -type resourceFlags uint8 - -const ( - resourceFlagsHolding resourceFlags = 0 - resourceFlagsNotHolding resourceFlags = 1 - resourceFlagsOwnership resourceFlags = 2 - resourceFlagsEmptyAsset resourceFlags = 4 - resourceFlagsEmptyApp resourceFlags = 8 -) - -// -// Resource flags interpretation: -// -// resourceFlagsHolding - the resource contains the holding of asset/app. -// resourceFlagsNotHolding - the resource is completely empty. This state should not be persisted. -// resourceFlagsOwnership - the resource contains the asset parameter or application parameters. -// resourceFlagsEmptyAsset - this is an asset resource, and it is empty. -// resourceFlagsEmptyApp - this is an app resource, and it is empty. - -type resourcesData struct { - _struct struct{} `codec:",omitempty,omitemptyarray"` - - // asset parameters ( basics.AssetParams ) - Total uint64 `codec:"a"` - Decimals uint32 `codec:"b"` - DefaultFrozen bool `codec:"c"` - UnitName string `codec:"d"` - AssetName string `codec:"e"` - URL string `codec:"f"` - MetadataHash [32]byte `codec:"g"` - Manager basics.Address `codec:"h"` - Reserve basics.Address `codec:"i"` - Freeze basics.Address `codec:"j"` - Clawback basics.Address `codec:"k"` - - // asset holding ( basics.AssetHolding ) - Amount uint64 `codec:"l"` - Frozen bool `codec:"m"` - - // application local state ( basics.AppLocalState ) - SchemaNumUint uint64 `codec:"n"` - SchemaNumByteSlice uint64 `codec:"o"` - KeyValue basics.TealKeyValue `codec:"p"` - - // application global params ( basics.AppParams ) - ApprovalProgram []byte `codec:"q,allocbound=config.MaxAvailableAppProgramLen"` - ClearStateProgram []byte `codec:"r,allocbound=config.MaxAvailableAppProgramLen"` - GlobalState basics.TealKeyValue `codec:"s"` - LocalStateSchemaNumUint uint64 `codec:"t"` - LocalStateSchemaNumByteSlice uint64 `codec:"u"` - GlobalStateSchemaNumUint uint64 `codec:"v"` - GlobalStateSchemaNumByteSlice uint64 `codec:"w"` - ExtraProgramPages uint32 `codec:"x"` - - // ResourceFlags helps to identify which portions of this structure should be used; in particular, it - // helps to provide a marker - i.e. whether the account was, for instance, opted-in for the asset compared - // to just being the owner of the asset. A comparison against the empty structure doesn't work here - - // since both the holdings and the parameters are allowed to be all at their default values. - ResourceFlags resourceFlags `codec:"y"` - - // UpdateRound is the round that modified this resource last. Since we want all the nodes to have the exact same - // value for this field, we'll be setting the value of this field to zero *before* the EnableAccountDataResourceSeparation - // consensus parameter is being set. Once the above consensus takes place, this field would be populated with the - // correct round number. - UpdateRound uint64 `codec:"z"` -} - -// makeResourcesData returns a new empty instance of resourcesData. -// Using this constructor method is necessary because of the ResourceFlags field. -// An optional rnd args sets UpdateRound -func makeResourcesData(rnd uint64) resourcesData { - return resourcesData{ResourceFlags: resourceFlagsNotHolding, UpdateRound: rnd} -} - -func (rd *resourcesData) IsHolding() bool { - return (rd.ResourceFlags & resourceFlagsNotHolding) == resourceFlagsHolding -} - -func (rd *resourcesData) IsOwning() bool { - return (rd.ResourceFlags & resourceFlagsOwnership) == resourceFlagsOwnership -} - -func (rd *resourcesData) IsEmpty() bool { - return !rd.IsApp() && !rd.IsAsset() -} - -func (rd *resourcesData) IsEmptyAppFields() bool { - return rd.SchemaNumUint == 0 && - rd.SchemaNumByteSlice == 0 && - len(rd.KeyValue) == 0 && - len(rd.ApprovalProgram) == 0 && - len(rd.ClearStateProgram) == 0 && - len(rd.GlobalState) == 0 && - rd.LocalStateSchemaNumUint == 0 && - rd.LocalStateSchemaNumByteSlice == 0 && - rd.GlobalStateSchemaNumUint == 0 && - rd.GlobalStateSchemaNumByteSlice == 0 && - rd.ExtraProgramPages == 0 -} - -func (rd *resourcesData) IsApp() bool { - if (rd.ResourceFlags & resourceFlagsEmptyApp) == resourceFlagsEmptyApp { - return true - } - return !rd.IsEmptyAppFields() -} - -func (rd *resourcesData) IsEmptyAssetFields() bool { - return rd.Amount == 0 && - !rd.Frozen && - rd.Total == 0 && - rd.Decimals == 0 && - !rd.DefaultFrozen && - rd.UnitName == "" && - rd.AssetName == "" && - rd.URL == "" && - rd.MetadataHash == [32]byte{} && - rd.Manager.IsZero() && - rd.Reserve.IsZero() && - rd.Freeze.IsZero() && - rd.Clawback.IsZero() -} - -func (rd *resourcesData) IsAsset() bool { - if (rd.ResourceFlags & resourceFlagsEmptyAsset) == resourceFlagsEmptyAsset { - return true - } - return !rd.IsEmptyAssetFields() -} - -func (rd *resourcesData) ClearAssetParams() { - rd.Total = 0 - rd.Decimals = 0 - rd.DefaultFrozen = false - rd.UnitName = "" - rd.AssetName = "" - rd.URL = "" - rd.MetadataHash = basics.Address{} - rd.Manager = basics.Address{} - rd.Reserve = basics.Address{} - rd.Freeze = basics.Address{} - rd.Clawback = basics.Address{} - hadHolding := (rd.ResourceFlags & resourceFlagsNotHolding) == resourceFlagsHolding - rd.ResourceFlags -= rd.ResourceFlags & resourceFlagsOwnership - rd.ResourceFlags &= ^resourceFlagsEmptyAsset - if rd.IsEmptyAssetFields() && hadHolding { - rd.ResourceFlags |= resourceFlagsEmptyAsset - } -} - -func (rd *resourcesData) SetAssetParams(ap basics.AssetParams, haveHoldings bool) { - rd.Total = ap.Total - rd.Decimals = ap.Decimals - rd.DefaultFrozen = ap.DefaultFrozen - rd.UnitName = ap.UnitName - rd.AssetName = ap.AssetName - rd.URL = ap.URL - rd.MetadataHash = ap.MetadataHash - rd.Manager = ap.Manager - rd.Reserve = ap.Reserve - rd.Freeze = ap.Freeze - rd.Clawback = ap.Clawback - rd.ResourceFlags |= resourceFlagsOwnership - if !haveHoldings { - rd.ResourceFlags |= resourceFlagsNotHolding - } - rd.ResourceFlags &= ^resourceFlagsEmptyAsset - if rd.IsEmptyAssetFields() { - rd.ResourceFlags |= resourceFlagsEmptyAsset - } -} - -func (rd *resourcesData) GetAssetParams() basics.AssetParams { - ap := basics.AssetParams{ - Total: rd.Total, - Decimals: rd.Decimals, - DefaultFrozen: rd.DefaultFrozen, - UnitName: rd.UnitName, - AssetName: rd.AssetName, - URL: rd.URL, - MetadataHash: rd.MetadataHash, - Manager: rd.Manager, - Reserve: rd.Reserve, - Freeze: rd.Freeze, - Clawback: rd.Clawback, - } - return ap -} - -func (rd *resourcesData) ClearAssetHolding() { - rd.Amount = 0 - rd.Frozen = false - - rd.ResourceFlags |= resourceFlagsNotHolding - hadParams := (rd.ResourceFlags & resourceFlagsOwnership) == resourceFlagsOwnership - if hadParams && rd.IsEmptyAssetFields() { - rd.ResourceFlags |= resourceFlagsEmptyAsset - } else { - rd.ResourceFlags &= ^resourceFlagsEmptyAsset - } -} - -func (rd *resourcesData) SetAssetHolding(ah basics.AssetHolding) { - rd.Amount = ah.Amount - rd.Frozen = ah.Frozen - rd.ResourceFlags &= ^(resourceFlagsNotHolding + resourceFlagsEmptyAsset) - // resourceFlagsHolding is set implicitly since it is zero - if rd.IsEmptyAssetFields() { - rd.ResourceFlags |= resourceFlagsEmptyAsset - } -} - -func (rd *resourcesData) GetAssetHolding() basics.AssetHolding { - return basics.AssetHolding{ - Amount: rd.Amount, - Frozen: rd.Frozen, - } -} - -func (rd *resourcesData) ClearAppLocalState() { - rd.SchemaNumUint = 0 - rd.SchemaNumByteSlice = 0 - rd.KeyValue = nil - - rd.ResourceFlags |= resourceFlagsNotHolding - hadParams := (rd.ResourceFlags & resourceFlagsOwnership) == resourceFlagsOwnership - if hadParams && rd.IsEmptyAppFields() { - rd.ResourceFlags |= resourceFlagsEmptyApp - } else { - rd.ResourceFlags &= ^resourceFlagsEmptyApp - } -} - -func (rd *resourcesData) SetAppLocalState(als basics.AppLocalState) { - rd.SchemaNumUint = als.Schema.NumUint - rd.SchemaNumByteSlice = als.Schema.NumByteSlice - rd.KeyValue = als.KeyValue - rd.ResourceFlags &= ^(resourceFlagsEmptyApp + resourceFlagsNotHolding) - if rd.IsEmptyAppFields() { - rd.ResourceFlags |= resourceFlagsEmptyApp - } -} - -func (rd *resourcesData) GetAppLocalState() basics.AppLocalState { - return basics.AppLocalState{ - Schema: basics.StateSchema{ - NumUint: rd.SchemaNumUint, - NumByteSlice: rd.SchemaNumByteSlice, - }, - KeyValue: rd.KeyValue, - } -} - -func (rd *resourcesData) ClearAppParams() { - rd.ApprovalProgram = nil - rd.ClearStateProgram = nil - rd.GlobalState = nil - rd.LocalStateSchemaNumUint = 0 - rd.LocalStateSchemaNumByteSlice = 0 - rd.GlobalStateSchemaNumUint = 0 - rd.GlobalStateSchemaNumByteSlice = 0 - rd.ExtraProgramPages = 0 - hadHolding := (rd.ResourceFlags & resourceFlagsNotHolding) == resourceFlagsHolding - rd.ResourceFlags -= rd.ResourceFlags & resourceFlagsOwnership - rd.ResourceFlags &= ^resourceFlagsEmptyApp - if rd.IsEmptyAppFields() && hadHolding { - rd.ResourceFlags |= resourceFlagsEmptyApp - } -} - -func (rd *resourcesData) SetAppParams(ap basics.AppParams, haveHoldings bool) { - rd.ApprovalProgram = ap.ApprovalProgram - rd.ClearStateProgram = ap.ClearStateProgram - rd.GlobalState = ap.GlobalState - rd.LocalStateSchemaNumUint = ap.LocalStateSchema.NumUint - rd.LocalStateSchemaNumByteSlice = ap.LocalStateSchema.NumByteSlice - rd.GlobalStateSchemaNumUint = ap.GlobalStateSchema.NumUint - rd.GlobalStateSchemaNumByteSlice = ap.GlobalStateSchema.NumByteSlice - rd.ExtraProgramPages = ap.ExtraProgramPages - rd.ResourceFlags |= resourceFlagsOwnership - if !haveHoldings { - rd.ResourceFlags |= resourceFlagsNotHolding - } - rd.ResourceFlags &= ^resourceFlagsEmptyApp - if rd.IsEmptyAppFields() { - rd.ResourceFlags |= resourceFlagsEmptyApp - } -} - -func (rd *resourcesData) GetAppParams() basics.AppParams { - return basics.AppParams{ - ApprovalProgram: rd.ApprovalProgram, - ClearStateProgram: rd.ClearStateProgram, - GlobalState: rd.GlobalState, - StateSchemas: basics.StateSchemas{ - LocalStateSchema: basics.StateSchema{ - NumUint: rd.LocalStateSchemaNumUint, - NumByteSlice: rd.LocalStateSchemaNumByteSlice, - }, - GlobalStateSchema: basics.StateSchema{ - NumUint: rd.GlobalStateSchemaNumUint, - NumByteSlice: rd.GlobalStateSchemaNumByteSlice, - }, - }, - ExtraProgramPages: rd.ExtraProgramPages, - } -} - -func (rd *resourcesData) SetAssetData(ap ledgercore.AssetParamsDelta, ah ledgercore.AssetHoldingDelta) { - if ah.Holding != nil { - rd.SetAssetHolding(*ah.Holding) - } else if ah.Deleted { - rd.ClearAssetHolding() - } - if ap.Params != nil { - rd.SetAssetParams(*ap.Params, rd.IsHolding()) - } else if ap.Deleted { - rd.ClearAssetParams() - } -} - -func (rd *resourcesData) SetAppData(ap ledgercore.AppParamsDelta, al ledgercore.AppLocalStateDelta) { - if al.LocalState != nil { - rd.SetAppLocalState(*al.LocalState) - } else if al.Deleted { - rd.ClearAppLocalState() - } - if ap.Params != nil { - rd.SetAppParams(*ap.Params, rd.IsHolding()) - } else if ap.Deleted { - rd.ClearAppParams() - } -} - -func accountDataResources( - ctx context.Context, - accountData *basics.AccountData, rowid int64, - outputResourceCb func(ctx context.Context, rowid int64, cidx basics.CreatableIndex, rd *resourcesData) error, -) error { - // handle all the assets we can find: - for aidx, holding := range accountData.Assets { - var rd resourcesData - rd.SetAssetHolding(holding) - if ap, has := accountData.AssetParams[aidx]; has { - rd.SetAssetParams(ap, true) - delete(accountData.AssetParams, aidx) - } - err := outputResourceCb(ctx, rowid, basics.CreatableIndex(aidx), &rd) - if err != nil { - return err - } - } - for aidx, aparams := range accountData.AssetParams { - var rd resourcesData - rd.SetAssetParams(aparams, false) - err := outputResourceCb(ctx, rowid, basics.CreatableIndex(aidx), &rd) - if err != nil { - return err - } - } - - // handle all the applications we can find: - for aidx, localState := range accountData.AppLocalStates { - var rd resourcesData - rd.SetAppLocalState(localState) - if ap, has := accountData.AppParams[aidx]; has { - rd.SetAppParams(ap, true) - delete(accountData.AppParams, aidx) - } - err := outputResourceCb(ctx, rowid, basics.CreatableIndex(aidx), &rd) - if err != nil { - return err - } - } - for aidx, aparams := range accountData.AppParams { - var rd resourcesData - rd.SetAppParams(aparams, false) - err := outputResourceCb(ctx, rowid, basics.CreatableIndex(aidx), &rd) - if err != nil { - return err - } - } - - return nil -} - -// performResourceTableMigration migrate the database to use the resources table. -func performResourceTableMigration(ctx context.Context, tx *sql.Tx, log func(processed, total uint64)) (err error) { - now := time.Now().UnixNano() - idxnameBalances := fmt.Sprintf("onlineaccountbals_idx_%d", now) - idxnameAddress := fmt.Sprintf("accountbase_address_idx_%d", now) - - createNewAcctBase := []string{ - `CREATE TABLE IF NOT EXISTS accountbase_resources_migration ( - addrid INTEGER PRIMARY KEY NOT NULL, - address blob NOT NULL, - data blob, - normalizedonlinebalance INTEGER )`, - createNormalizedOnlineBalanceIndex(idxnameBalances, "accountbase_resources_migration"), - createUniqueAddressBalanceIndex(idxnameAddress, "accountbase_resources_migration"), - } - - applyNewAcctBase := []string{ - `ALTER TABLE accountbase RENAME TO accountbase_old`, - `ALTER TABLE accountbase_resources_migration RENAME TO accountbase`, - `DROP TABLE IF EXISTS accountbase_old`, - } - - for _, stmt := range createNewAcctBase { - _, err = tx.ExecContext(ctx, stmt) - if err != nil { - return err - } - } - var insertNewAcctBase *sql.Stmt - var insertResources *sql.Stmt - var insertNewAcctBaseNormBal *sql.Stmt - insertNewAcctBase, err = tx.PrepareContext(ctx, "INSERT INTO accountbase_resources_migration(address, data) VALUES(?, ?)") - if err != nil { - return err - } - defer insertNewAcctBase.Close() - - insertNewAcctBaseNormBal, err = tx.PrepareContext(ctx, "INSERT INTO accountbase_resources_migration(address, data, normalizedonlinebalance) VALUES(?, ?, ?)") - if err != nil { - return err - } - defer insertNewAcctBaseNormBal.Close() - - insertResources, err = tx.PrepareContext(ctx, "INSERT INTO resources(addrid, aidx, data) VALUES(?, ?, ?)") - if err != nil { - return err - } - defer insertResources.Close() - - var rows *sql.Rows - rows, err = tx.QueryContext(ctx, "SELECT address, data, normalizedonlinebalance FROM accountbase ORDER BY address") - if err != nil { - return err - } - defer rows.Close() - - var insertRes sql.Result - var rowID int64 - var rowsAffected int64 - var processedAccounts uint64 - var totalBaseAccounts uint64 - - totalBaseAccounts, err = totalAccounts(ctx, tx) - if err != nil { - return err - } - for rows.Next() { - var addrbuf []byte - var encodedAcctData []byte - var normBal sql.NullInt64 - err = rows.Scan(&addrbuf, &encodedAcctData, &normBal) - if err != nil { - return err - } - - var accountData basics.AccountData - err = protocol.Decode(encodedAcctData, &accountData) - if err != nil { - return err - } - var newAccountData baseAccountData - newAccountData.SetAccountData(&accountData) - encodedAcctData = protocol.Encode(&newAccountData) - - if normBal.Valid { - insertRes, err = insertNewAcctBaseNormBal.ExecContext(ctx, addrbuf, encodedAcctData, normBal.Int64) - } else { - insertRes, err = insertNewAcctBase.ExecContext(ctx, addrbuf, encodedAcctData) - } - - if err != nil { - return err - } - rowsAffected, err = insertRes.RowsAffected() - if err != nil { - return err - } - if rowsAffected != 1 { - return fmt.Errorf("number of affected rows is not 1 - %d", rowsAffected) - } - rowID, err = insertRes.LastInsertId() - if err != nil { - return err - } - insertResourceCallback := func(ctx context.Context, rowID int64, cidx basics.CreatableIndex, rd *resourcesData) error { - var err error - if rd != nil { - encodedData := protocol.Encode(rd) - _, err = insertResources.ExecContext(ctx, rowID, cidx, encodedData) - } - return err - } - err = accountDataResources(ctx, &accountData, rowID, insertResourceCallback) - if err != nil { - return err - } - processedAccounts++ - if log != nil { - log(processedAccounts, totalBaseAccounts) - } - } - - // if the above loop was abrupt by an error, test it now. - if err = rows.Err(); err != nil { - return err - } - - for _, stmt := range applyNewAcctBase { - _, err = tx.Exec(stmt) - if err != nil { - return err - } - } - return nil -} - -func performTxTailTableMigration(ctx context.Context, tx *sql.Tx, blockDb db.Accessor) (err error) { - if tx == nil { - return nil - } - - dbRound, err := accountsRound(tx) - if err != nil { - return fmt.Errorf("latest block number cannot be retrieved : %w", err) - } - - // load the latest MaxTxnLife rounds in the txtail and store these in the txtail. - // when migrating there is only MaxTxnLife blocks in the block DB - // since the original txTail.commmittedUpTo preserved only (rnd+1)-MaxTxnLife = 1000 blocks back - err = blockDb.Atomic(func(ctx context.Context, blockTx *sql.Tx) error { - latestBlockRound, err := blockLatest(blockTx) - if err != nil { - return fmt.Errorf("latest block number cannot be retrieved : %w", err) - } - latestHdr, err := blockGetHdr(blockTx, dbRound) - if err != nil { - return fmt.Errorf("latest block header %d cannot be retrieved : %w", dbRound, err) - } - - proto := config.Consensus[latestHdr.CurrentProtocol] - maxTxnLife := basics.Round(proto.MaxTxnLife) - deeperBlockHistory := basics.Round(proto.DeeperBlockHeaderHistory) - // firstRound is either maxTxnLife + deeperBlockHistory back from the latest for regular init - // or maxTxnLife + deeperBlockHistory + CatchpointLookback back for catchpoint apply. - // Try to check the earliest available and start from there. - firstRound := (latestBlockRound + 1).SubSaturate(maxTxnLife + deeperBlockHistory + basics.Round(proto.CatchpointLookback)) - // we don't need to have the txtail for round 0. - if firstRound == basics.Round(0) { - firstRound++ - } - if _, err := blockGet(blockTx, firstRound); err != nil { - // looks like not catchpoint but a regular migration, start from maxTxnLife + deeperBlockHistory back - firstRound = (latestBlockRound + 1).SubSaturate(maxTxnLife + deeperBlockHistory) - if firstRound == basics.Round(0) { - firstRound++ - } - } - tailRounds := make([][]byte, 0, maxTxnLife) - for rnd := firstRound; rnd <= dbRound; rnd++ { - blk, err := blockGet(blockTx, rnd) - if err != nil { - return fmt.Errorf("block for round %d ( %d - %d ) cannot be retrieved : %w", rnd, firstRound, dbRound, err) - } - - tail, err := txTailRoundFromBlock(blk) - if err != nil { - return err - } - - encodedTail, _ := tail.encode() - tailRounds = append(tailRounds, encodedTail) - } - - return txtailNewRound(ctx, tx, firstRound, tailRounds, firstRound) - }) - - return err -} - -func performOnlineRoundParamsTailMigration(ctx context.Context, tx *sql.Tx, blockDb db.Accessor, newDatabase bool, initProto protocol.ConsensusVersion) (err error) { - totals, err := accountsTotals(ctx, tx, false) - if err != nil { - return err - } - rnd, err := accountsRound(tx) - if err != nil { - return err - } - var currentProto protocol.ConsensusVersion - if newDatabase { - currentProto = initProto - } else { - err = blockDb.Atomic(func(ctx context.Context, blockTx *sql.Tx) error { - hdr, err := blockGetHdr(blockTx, rnd) - if err != nil { - return err - } - currentProto = hdr.CurrentProtocol - return nil - }) - if err != nil { - return err - } - } - onlineRoundParams := []ledgercore.OnlineRoundParamsData{ - { - OnlineSupply: totals.Online.Money.Raw, - RewardsLevel: totals.RewardsLevel, - CurrentProtocol: currentProto, - }, - } - return accountsPutOnlineRoundParams(tx, onlineRoundParams, rnd) -} - -func performOnlineAccountsTableMigration(ctx context.Context, tx *sql.Tx, progress func(processed, total uint64), log logging.Logger) (err error) { - - var insertOnlineAcct *sql.Stmt - insertOnlineAcct, err = tx.PrepareContext(ctx, "INSERT INTO onlineaccounts(address, data, normalizedonlinebalance, updround, votelastvalid) VALUES(?, ?, ?, ?, ?)") - if err != nil { - return err - } - defer insertOnlineAcct.Close() - - var updateAcct *sql.Stmt - updateAcct, err = tx.PrepareContext(ctx, "UPDATE accountbase SET data = ? WHERE addrid = ?") - if err != nil { - return err - } - defer updateAcct.Close() - - var rows *sql.Rows - rows, err = tx.QueryContext(ctx, "SELECT addrid, address, data, normalizedonlinebalance FROM accountbase") - if err != nil { - return err - } - defer rows.Close() - - var insertRes sql.Result - var updateRes sql.Result - var rowsAffected int64 - var processedAccounts uint64 - var totalOnlineBaseAccounts uint64 - - totalOnlineBaseAccounts, err = totalAccounts(ctx, tx) - var total uint64 - err = tx.QueryRowContext(ctx, "SELECT count(1) FROM accountbase").Scan(&total) - if err != nil { - if err != sql.ErrNoRows { - return err - } - total = 0 - err = nil - } - - checkSQLResult := func(e error, res sql.Result) (err error) { - if e != nil { - err = e - return - } - rowsAffected, err = res.RowsAffected() - if err != nil { - return err - } - if rowsAffected != 1 { - return fmt.Errorf("number of affected rows is not 1 - %d", rowsAffected) - } - return nil - } - - type acctState struct { - old baseAccountData - oldEnc []byte - new baseAccountData - newEnc []byte - } - acctRehash := make(map[basics.Address]acctState) - var addr basics.Address - - for rows.Next() { - var addrid sql.NullInt64 - var addrbuf []byte - var encodedAcctData []byte - var normBal sql.NullInt64 - err = rows.Scan(&addrid, &addrbuf, &encodedAcctData, &normBal) - if err != nil { - return err - } - if len(addrbuf) != len(addr) { - err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) - return err - } - var ba baseAccountData - err = protocol.Decode(encodedAcctData, &ba) - if err != nil { - return err - } - - // insert entries into online accounts table - if ba.Status == basics.Online { - if ba.MicroAlgos.Raw > 0 && !normBal.Valid { - copy(addr[:], addrbuf) - return fmt.Errorf("non valid norm balance for online account %s", addr.String()) - } - var baseOnlineAD baseOnlineAccountData - baseOnlineAD.baseVotingData = ba.baseVotingData - baseOnlineAD.MicroAlgos = ba.MicroAlgos - baseOnlineAD.RewardsBase = ba.RewardsBase - encodedOnlineAcctData := protocol.Encode(&baseOnlineAD) - insertRes, err = insertOnlineAcct.ExecContext(ctx, addrbuf, encodedOnlineAcctData, normBal.Int64, ba.UpdateRound, baseOnlineAD.VoteLastValid) - err = checkSQLResult(err, insertRes) - if err != nil { - return err - } - } - - // remove stateproofID field for offline accounts - if ba.Status != basics.Online && !ba.StateProofID.IsEmpty() { - // store old data for account hash update - state := acctState{old: ba, oldEnc: encodedAcctData} - ba.StateProofID = merklesignature.Commitment{} - encodedOnlineAcctData := protocol.Encode(&ba) - copy(addr[:], addrbuf) - state.new = ba - state.newEnc = encodedOnlineAcctData - acctRehash[addr] = state - updateRes, err = updateAcct.ExecContext(ctx, encodedOnlineAcctData, addrid.Int64) - err = checkSQLResult(err, updateRes) - if err != nil { - return err - } - } - - processedAccounts++ - if progress != nil { - progress(processedAccounts, totalOnlineBaseAccounts) - } - } - if err = rows.Err(); err != nil { - return err - } - - // update accounthashes for the modified accounts - if len(acctRehash) > 0 { - var count uint64 - err := tx.QueryRow("SELECT count(1) FROM accounthashes").Scan(&count) - if err != nil { - return err - } - if count == 0 { - // no account hashes, done - return nil - } - - mc, err := MakeMerkleCommitter(tx, false) - if err != nil { - return nil - } - - trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) - if err != nil { - return fmt.Errorf("accountsInitialize was unable to MakeTrie: %v", err) - } - for addr, state := range acctRehash { - deleteHash := accountHashBuilderV6(addr, &state.old, state.oldEnc) - deleted, err := trie.Delete(deleteHash) - if err != nil { - return fmt.Errorf("performOnlineAccountsTableMigration failed to delete hash '%s' from merkle trie for account %v: %w", hex.EncodeToString(deleteHash), addr, err) - } - if !deleted && log != nil { - log.Warnf("performOnlineAccountsTableMigration failed to delete hash '%s' from merkle trie for account %v", hex.EncodeToString(deleteHash), addr) - } - - addHash := accountHashBuilderV6(addr, &state.new, state.newEnc) - added, err := trie.Add(addHash) - if err != nil { - return fmt.Errorf("performOnlineAccountsTableMigration attempted to add duplicate hash '%s' to merkle trie for account %v: %w", hex.EncodeToString(addHash), addr, err) - } - if !added && log != nil { - log.Warnf("performOnlineAccountsTableMigration attempted to add duplicate hash '%s' to merkle trie for account %v", hex.EncodeToString(addHash), addr) - } - } - _, err = trie.Commit() - if err != nil { - return err - } - } - - return nil -} - -// removeEmptyAccountData removes empty AccountData msgp-encoded entries from accountbase table -// and optionally returns list of addresses that were eliminated -func removeEmptyAccountData(tx *sql.Tx, queryAddresses bool) (num int64, addresses []basics.Address, err error) { - if queryAddresses { - rows, err := tx.Query("SELECT address FROM accountbase where length(data) = 1 and data = x'80'") // empty AccountData is 0x80 - if err != nil { - return 0, nil, err - } - defer rows.Close() - - for rows.Next() { - var addrbuf []byte - err = rows.Scan(&addrbuf) - if err != nil { - return 0, nil, err - } - var addr basics.Address - if len(addrbuf) != len(addr) { - err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) - return 0, nil, err - } - copy(addr[:], addrbuf) - addresses = append(addresses, addr) - } - - // if the above loop was abrupted by an error, test it now. - if err = rows.Err(); err != nil { - return 0, nil, err - } - } - - result, err := tx.Exec("DELETE from accountbase where length(data) = 1 and data = x'80'") - if err != nil { - return 0, nil, err - } - num, err = result.RowsAffected() - if err != nil { - // something wrong on getting rows count but data deleted, ignore the error - num = int64(len(addresses)) - err = nil - } - return num, addresses, err -} - -// accountDataToOnline returns the part of the AccountData that matters -// for online accounts (to answer top-N queries). We store a subset of -// the full AccountData because we need to store a large number of these -// in memory (say, 1M), and storing that many AccountData could easily -// cause us to run out of memory. -func accountDataToOnline(address basics.Address, ad *ledgercore.AccountData, proto config.ConsensusParams) *ledgercore.OnlineAccount { - return &ledgercore.OnlineAccount{ - Address: address, - MicroAlgos: ad.MicroAlgos, - RewardsBase: ad.RewardsBase, - NormalizedOnlineBalance: ad.NormalizedOnlineBalance(proto), - VoteFirstValid: ad.VoteFirstValid, - VoteLastValid: ad.VoteLastValid, - StateProofID: ad.StateProofID, - } -} - -func resetAccountHashes(ctx context.Context, tx *sql.Tx) (err error) { - _, err = tx.ExecContext(ctx, `DELETE FROM accounthashes`) - return -} - -func accountsReset(ctx context.Context, tx *sql.Tx) error { - for _, stmt := range accountsResetExprs { - _, err := tx.ExecContext(ctx, stmt) - if err != nil { - return err - } - } - _, err := db.SetUserVersion(ctx, tx, 0) - return err -} - -// accountsRound returns the tracker balances round number -func accountsRound(q db.Queryable) (rnd basics.Round, err error) { - err = q.QueryRow("SELECT rnd FROM acctrounds WHERE id='acctbase'").Scan(&rnd) - if err != nil { - return - } - return -} - -// accountsHashRound returns the round of the hash tree -// if the hash of the tree doesn't exists, it returns zero. -func accountsHashRound(ctx context.Context, tx *sql.Tx) (hashrnd basics.Round, err error) { - err = tx.QueryRowContext(ctx, "SELECT rnd FROM acctrounds WHERE id='hashbase'").Scan(&hashrnd) - if err == sql.ErrNoRows { - hashrnd = basics.Round(0) - err = nil - } - return -} - -func accountsInitDbQueries(q db.Queryable) (*accountsDbQueries, error) { - var err error - qs := &accountsDbQueries{} - - qs.listCreatablesStmt, err = q.Prepare("SELECT acctrounds.rnd, assetcreators.asset, assetcreators.creator FROM acctrounds LEFT JOIN assetcreators ON assetcreators.asset <= ? AND assetcreators.ctype = ? WHERE acctrounds.id='acctbase' ORDER BY assetcreators.asset desc LIMIT ?") - if err != nil { - return nil, err - } - - qs.lookupStmt, err = q.Prepare("SELECT accountbase.rowid, acctrounds.rnd, accountbase.data FROM acctrounds LEFT JOIN accountbase ON address=? WHERE id='acctbase'") - if err != nil { - return nil, err - } - - qs.lookupResourcesStmt, err = q.Prepare("SELECT accountbase.rowid, acctrounds.rnd, resources.data FROM acctrounds LEFT JOIN accountbase ON accountbase.address = ? LEFT JOIN resources ON accountbase.rowid = resources.addrid AND resources.aidx = ? WHERE id='acctbase'") - if err != nil { - return nil, err - } - - qs.lookupAllResourcesStmt, err = q.Prepare("SELECT accountbase.rowid, acctrounds.rnd, resources.aidx, resources.data FROM acctrounds LEFT JOIN accountbase ON accountbase.address = ? LEFT JOIN resources ON accountbase.rowid = resources.addrid WHERE id='acctbase'") - if err != nil { - return nil, err - } - - qs.lookupKvPairStmt, err = q.Prepare("SELECT acctrounds.rnd, kvstore.value FROM acctrounds LEFT JOIN kvstore ON key = ? WHERE id='acctbase';") - if err != nil { - return nil, err - } - - qs.lookupKeysByRangeStmt, err = q.Prepare("SELECT acctrounds.rnd, kvstore.key FROM acctrounds LEFT JOIN kvstore ON kvstore.key >= ? AND kvstore.key < ? WHERE id='acctbase'") - if err != nil { - return nil, err - } - - qs.lookupCreatorStmt, err = q.Prepare("SELECT acctrounds.rnd, assetcreators.creator FROM acctrounds LEFT JOIN assetcreators ON asset = ? AND ctype = ? WHERE id='acctbase'") - if err != nil { - return nil, err - } - - return qs, nil -} - -func onlineAccountsInitDbQueries(r db.Queryable) (*onlineAccountsDbQueries, error) { - var err error - qs := &onlineAccountsDbQueries{} - - qs.lookupOnlineStmt, err = r.Prepare("SELECT onlineaccounts.rowid, onlineaccounts.updround, acctrounds.rnd, onlineaccounts.data FROM acctrounds LEFT JOIN onlineaccounts ON address=? AND updround <= ? WHERE id='acctbase' ORDER BY updround DESC LIMIT 1") - if err != nil { - return nil, err - } - - qs.lookupOnlineHistoryStmt, err = r.Prepare("SELECT onlineaccounts.rowid, onlineaccounts.updround, acctrounds.rnd, onlineaccounts.data FROM acctrounds LEFT JOIN onlineaccounts ON address=? WHERE id='acctbase' ORDER BY updround ASC") - if err != nil { - return nil, err - } - - qs.lookupOnlineTotalsStmt, err = r.Prepare("SELECT data FROM onlineroundparamstail WHERE rnd=?") - if err != nil { - return nil, err - } - return qs, nil -} - -// listCreatables returns an array of CreatableLocator which have CreatableIndex smaller or equal to maxIdx and are of the provided CreatableType. -func (qs *accountsDbQueries) listCreatables(maxIdx basics.CreatableIndex, maxResults uint64, ctype basics.CreatableType) (results []basics.CreatableLocator, dbRound basics.Round, err error) { - err = db.Retry(func() error { - // Query for assets in range - rows, err := qs.listCreatablesStmt.Query(maxIdx, ctype, maxResults) - if err != nil { - return err - } - defer rows.Close() - - // For each row, copy into a new CreatableLocator and append to results - var buf []byte - var cl basics.CreatableLocator - var creatableIndex sql.NullInt64 - for rows.Next() { - err = rows.Scan(&dbRound, &creatableIndex, &buf) - if err != nil { - return err - } - if !creatableIndex.Valid { - // we received an entry without any index. This would happen only on the first entry when there are no creatables of the requested type. - break - } - cl.Index = basics.CreatableIndex(creatableIndex.Int64) - copy(cl.Creator[:], buf) - cl.Type = ctype - results = append(results, cl) - } - return nil - }) - return -} - -// sql.go has the following contradictory comments: - -// Reference types such as []byte are only valid until the next call to Scan -// and should not be retained. Their underlying memory is owned by the driver. -// If retention is necessary, copy their values before the next call to Scan. - -// If a dest argument has type *[]byte, Scan saves in that argument a -// copy of the corresponding data. The copy is owned by the caller and -// can be modified and held indefinitely. The copy can be avoided by -// using an argument of type *RawBytes instead; see the documentation -// for RawBytes for restrictions on its use. - -// After check source code, a []byte slice destination is definitely cloned. - -func (qs *accountsDbQueries) lookupKeyValue(key string) (pv persistedKVData, err error) { - err = db.Retry(func() error { - var val []byte - // Cast to []byte to avoid interpretation as character string, see note in upsertKvPair - err := qs.lookupKvPairStmt.QueryRow([]byte(key)).Scan(&pv.round, &val) - if err != nil { - // this should never happen; it indicates that we don't have a current round in the acctrounds table. - if err == sql.ErrNoRows { - // Return the zero value of data - err = fmt.Errorf("unable to query value for key %v : %w", key, err) - } - return err - } - if val != nil { // We got a non-null value, so it exists - pv.value = val - return nil - } - // we don't have that key, just return pv with the database round (pv.value==nil) - return nil - }) - return -} - -// keyPrefixIntervalPreprocessing is implemented to generate an interval for DB queries that look up keys by prefix. -// Such DB query was designed this way, to trigger the binary search optimization in SQLITE3. -// The DB comparison for blob typed primary key is lexicographic, i.e., byte by byte. -// In this way, we can introduce an interval that a primary key should be >= some prefix, < some prefix increment. -// A corner case to consider is that, the prefix has last byte 0xFF, or the prefix is full of 0xFF. -// - The first case can be solved by carrying, e.g., prefix = 0x1EFF -> interval being >= 0x1EFF and < 0x1F -// - The second case can be solved by disregarding the upper limit, i.e., prefix = 0xFFFF -> interval being >= 0xFFFF -// Another corner case to consider is empty byte, []byte{} or nil. -// - In both cases, the results are interval >= "", i.e., returns []byte{} for prefix, and nil for prefixIncr. -func keyPrefixIntervalPreprocessing(prefix []byte) ([]byte, []byte) { - if prefix == nil { - prefix = []byte{} - } - prefixIncr := make([]byte, len(prefix)) - copy(prefixIncr, prefix) - for i := len(prefix) - 1; i >= 0; i-- { - currentByteIncr := int(prefix[i]) + 1 - if currentByteIncr > 0xFF { - prefixIncr = prefixIncr[:len(prefixIncr)-1] - continue - } - prefixIncr[i] = byte(currentByteIncr) - return prefix, prefixIncr - } - return prefix, nil -} - -func (qs *accountsDbQueries) lookupKeysByPrefix(prefix string, maxKeyNum uint64, results map[string]bool, resultCount uint64) (round basics.Round, err error) { - start, end := keyPrefixIntervalPreprocessing([]byte(prefix)) - if end == nil { - // Not an expected use case, it's asking for all keys, or all keys - // prefixed by some number of 0xFF bytes. - return 0, fmt.Errorf("Lookup by strange prefix %#v", prefix) - } - err = db.Retry(func() error { - var rows *sql.Rows - rows, err = qs.lookupKeysByRangeStmt.Query(start, end) - if err != nil { - return err - } - defer rows.Close() - - var v sql.NullString - - for rows.Next() { - if resultCount == maxKeyNum { - return nil - } - err = rows.Scan(&round, &v) - if err != nil { - return err - } - if v.Valid { - if _, ok := results[v.String]; ok { - continue - } - results[v.String] = true - resultCount++ - } - } - return nil - }) - return -} - -func (qs *accountsDbQueries) lookupCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (addr basics.Address, ok bool, dbRound basics.Round, err error) { - err = db.Retry(func() error { - var buf []byte - err := qs.lookupCreatorStmt.QueryRow(cidx, ctype).Scan(&dbRound, &buf) - - // this shouldn't happen unless we can't figure the round number. - if err == sql.ErrNoRows { - return fmt.Errorf("lookupCreator was unable to retrieve round number") - } - - // Some other database error - if err != nil { - return err - } - - if len(buf) > 0 { - ok = true - copy(addr[:], buf) - } - return nil - }) - return -} - -func (qs *accountsDbQueries) lookupResources(addr basics.Address, aidx basics.CreatableIndex, ctype basics.CreatableType) (data persistedResourcesData, err error) { - err = db.Retry(func() error { - var buf []byte - var rowid sql.NullInt64 - err := qs.lookupResourcesStmt.QueryRow(addr[:], aidx).Scan(&rowid, &data.round, &buf) - if err == nil { - data.aidx = aidx - if len(buf) > 0 && rowid.Valid { - data.addrid = rowid.Int64 - err = protocol.Decode(buf, &data.data) - if err != nil { - return err - } - if ctype == basics.AssetCreatable && !data.data.IsAsset() { - return fmt.Errorf("lookupResources asked for an asset but got %v", data.data) - } - if ctype == basics.AppCreatable && !data.data.IsApp() { - return fmt.Errorf("lookupResources asked for an app but got %v", data.data) - } - return nil - } - data.data = makeResourcesData(0) - // we don't have that account, just return the database round. - return nil - } - - // this should never happen; it indicates that we don't have a current round in the acctrounds table. - if err == sql.ErrNoRows { - // Return the zero value of data - return fmt.Errorf("unable to query resource data for address %v aidx %v ctype %v : %w", addr, aidx, ctype, err) - } - return err - }) - return -} - -func (qs *accountsDbQueries) lookupAllResources(addr basics.Address) (data []persistedResourcesData, rnd basics.Round, err error) { - err = db.Retry(func() error { - // Query for all resources - rows, err := qs.lookupAllResourcesStmt.Query(addr[:]) - if err != nil { - return err - } - defer rows.Close() - - var addrid, aidx sql.NullInt64 - var dbRound basics.Round - data = nil - var buf []byte - for rows.Next() { - err := rows.Scan(&addrid, &dbRound, &aidx, &buf) - if err != nil { - return err - } - if !addrid.Valid || !aidx.Valid { - // we received an entry without any index. This would happen only on the first entry when there are no resources for this address. - // ensure this is the first entry, set the round and return - if len(data) != 0 { - return fmt.Errorf("lookupAllResources: unexpected invalid result on non-first resource record: (%v, %v)", addrid.Valid, aidx.Valid) - } - rnd = dbRound - break - } - var resData resourcesData - err = protocol.Decode(buf, &resData) - if err != nil { - return err - } - data = append(data, persistedResourcesData{ - addrid: addrid.Int64, - aidx: basics.CreatableIndex(aidx.Int64), - data: resData, - round: dbRound, - }) - rnd = dbRound - } - return nil - }) - return -} - -// lookup looks up for a the account data given it's address. It returns the persistedAccountData, which includes the current database round and the matching -// account data, if such was found. If no matching account data could be found for the given address, an empty account data would -// be retrieved. -func (qs *accountsDbQueries) lookup(addr basics.Address) (data persistedAccountData, err error) { - err = db.Retry(func() error { - var buf []byte - var rowid sql.NullInt64 - err := qs.lookupStmt.QueryRow(addr[:]).Scan(&rowid, &data.round, &buf) - if err == nil { - data.addr = addr - if len(buf) > 0 && rowid.Valid { - data.rowid = rowid.Int64 - err = protocol.Decode(buf, &data.accountData) - return err - } - // we don't have that account, just return the database round. - return nil - } - - // this should never happen; it indicates that we don't have a current round in the acctrounds table. - if err == sql.ErrNoRows { - // Return the zero value of data - return fmt.Errorf("unable to query account data for address %v : %w", addr, err) - } - - return err - }) - return -} - -func (qs *onlineAccountsDbQueries) lookupOnline(addr basics.Address, rnd basics.Round) (data persistedOnlineAccountData, err error) { - err = db.Retry(func() error { - var buf []byte - var rowid sql.NullInt64 - var updround sql.NullInt64 - err := qs.lookupOnlineStmt.QueryRow(addr[:], rnd).Scan(&rowid, &updround, &data.round, &buf) - if err == nil { - data.addr = addr - if len(buf) > 0 && rowid.Valid && updround.Valid { - data.rowid = rowid.Int64 - data.updRound = basics.Round(updround.Int64) - err = protocol.Decode(buf, &data.accountData) - return err - } - // we don't have that account, just return the database round. - return nil - } - - // this should never happen; it indicates that we don't have a current round in the acctrounds table. - if err == sql.ErrNoRows { - // Return the zero value of data - return fmt.Errorf("unable to query online account data for address %v : %w", addr, err) - } - - return err - }) - return -} - -func (qs *onlineAccountsDbQueries) lookupOnlineTotalsHistory(round basics.Round) (basics.MicroAlgos, error) { - data := ledgercore.OnlineRoundParamsData{} - err := db.Retry(func() error { - row := qs.lookupOnlineTotalsStmt.QueryRow(round) - var buf []byte - err := row.Scan(&buf) - if err != nil { - return err - } - err = protocol.Decode(buf, &data) - if err != nil { - return err - } - return nil - }) - return basics.MicroAlgos{Raw: data.OnlineSupply}, err -} - -func (qs *onlineAccountsDbQueries) lookupOnlineHistory(addr basics.Address) (result []persistedOnlineAccountData, rnd basics.Round, err error) { - err = db.Retry(func() error { - rows, err := qs.lookupOnlineHistoryStmt.Query(addr[:]) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var buf []byte - data := persistedOnlineAccountData{} - err := rows.Scan(&data.rowid, &data.updRound, &rnd, &buf) - if err != nil { - return err - } - err = protocol.Decode(buf, &data.accountData) - if err != nil { - return err - } - data.addr = addr - result = append(result, data) - } - return err - }) - return -} - -func storeCatchpoint(ctx context.Context, e db.Executable, round basics.Round, fileName string, catchpoint string, fileSize int64) (err error) { - err = db.Retry(func() (err error) { - query := "DELETE FROM storedcatchpoints WHERE round=?" - _, err = e.ExecContext(ctx, query, round) - if err != nil || (fileName == "" && catchpoint == "" && fileSize == 0) { - return err - } - - query = "INSERT INTO storedcatchpoints(round, filename, catchpoint, filesize, pinned) VALUES(?, ?, ?, ?, 0)" - _, err = e.ExecContext(ctx, query, round, fileName, catchpoint, fileSize) - return err - }) - return -} - -func getOldestCatchpointFiles(ctx context.Context, q db.Queryable, fileCount int, filesToKeep int) (fileNames map[basics.Round]string, err error) { - err = db.Retry(func() (err error) { - query := "SELECT round, filename FROM storedcatchpoints WHERE pinned = 0 and round <= COALESCE((SELECT round FROM storedcatchpoints WHERE pinned = 0 ORDER BY round DESC LIMIT ?, 1),0) ORDER BY round ASC LIMIT ?" - rows, err := q.QueryContext(ctx, query, filesToKeep, fileCount) - if err != nil { - return err - } - defer rows.Close() - - fileNames = make(map[basics.Round]string) - for rows.Next() { - var fileName string - var round basics.Round - err = rows.Scan(&round, &fileName) - if err != nil { - return err - } - fileNames[round] = fileName - } - - return rows.Err() - }) - if err != nil { - fileNames = nil - } - return -} - -func readCatchpointStateUint64(ctx context.Context, q db.Queryable, stateName catchpointState) (val uint64, err error) { - err = db.Retry(func() (err error) { - query := "SELECT intval FROM catchpointstate WHERE id=?" - var v sql.NullInt64 - err = q.QueryRowContext(ctx, query, stateName).Scan(&v) - if err == sql.ErrNoRows { - return nil - } - if err != nil { - return err - } - if v.Valid { - val = uint64(v.Int64) - } - return nil - }) - return val, err -} - -func writeCatchpointStateUint64(ctx context.Context, e db.Executable, stateName catchpointState, setValue uint64) (err error) { - err = db.Retry(func() (err error) { - if setValue == 0 { - return deleteCatchpointStateImpl(ctx, e, stateName) - } - - // we don't know if there is an entry in the table for this state, so we'll insert/replace it just in case. - query := "INSERT OR REPLACE INTO catchpointstate(id, intval) VALUES(?, ?)" - _, err = e.ExecContext(ctx, query, stateName, setValue) - return err - }) - return err -} - -func readCatchpointStateString(ctx context.Context, q db.Queryable, stateName catchpointState) (val string, err error) { - err = db.Retry(func() (err error) { - query := "SELECT strval FROM catchpointstate WHERE id=?" - var v sql.NullString - err = q.QueryRowContext(ctx, query, stateName).Scan(&v) - if err == sql.ErrNoRows { - return nil - } - if err != nil { - return err - } - - if v.Valid { - val = v.String - } - return nil - }) - return val, err -} - -func writeCatchpointStateString(ctx context.Context, e db.Executable, stateName catchpointState, setValue string) (err error) { - err = db.Retry(func() (err error) { - if setValue == "" { - return deleteCatchpointStateImpl(ctx, e, stateName) - } - - // we don't know if there is an entry in the table for this state, so we'll insert/replace it just in case. - query := "INSERT OR REPLACE INTO catchpointstate(id, strval) VALUES(?, ?)" - _, err = e.ExecContext(ctx, query, stateName, setValue) - return err - }) - return err -} - -func deleteCatchpointStateImpl(ctx context.Context, e db.Executable, stateName catchpointState) error { - query := "DELETE FROM catchpointstate WHERE id=?" - _, err := e.ExecContext(ctx, query, stateName) - return err -} - -func (qs *accountsDbQueries) close() { - preparedQueries := []**sql.Stmt{ - &qs.listCreatablesStmt, - &qs.lookupStmt, - &qs.lookupResourcesStmt, - &qs.lookupAllResourcesStmt, - &qs.lookupKvPairStmt, - &qs.lookupKeysByRangeStmt, - &qs.lookupCreatorStmt, - } - for _, preparedQuery := range preparedQueries { - if (*preparedQuery) != nil { - (*preparedQuery).Close() - *preparedQuery = nil - } - } -} - -func (qs *onlineAccountsDbQueries) close() { - preparedQueries := []**sql.Stmt{ - &qs.lookupOnlineStmt, - &qs.lookupOnlineHistoryStmt, - } - for _, preparedQuery := range preparedQueries { - if (*preparedQuery) != nil { - (*preparedQuery).Close() - *preparedQuery = nil - } - } -} - -// accountsOnlineTop returns the top n online accounts starting at position offset -// (that is, the top offset'th account through the top offset+n-1'th account). -// -// The accounts are sorted by their normalized balance and address. The normalized -// balance has to do with the reward parts of online account balances. See the -// normalization procedure in AccountData.NormalizedOnlineBalance(). -// -// Note that this does not check if the accounts have a vote key valid for any -// particular round (past, present, or future). -func accountsOnlineTop(tx *sql.Tx, rnd basics.Round, offset uint64, n uint64, proto config.ConsensusParams) (map[basics.Address]*ledgercore.OnlineAccount, error) { - // onlineaccounts has historical data ordered by updround for both online and offline accounts. - // This means some account A might have norm balance != 0 at round N and norm balance == 0 at some round K > N. - // For online top query one needs to find entries not fresher than X with norm balance != 0. - // To do that the query groups row by address and takes the latest updround, and then filters out rows with zero nor balance. - rows, err := tx.Query(`SELECT address, normalizedonlinebalance, data, max(updround) FROM onlineaccounts -WHERE updround <= ? -GROUP BY address HAVING normalizedonlinebalance > 0 -ORDER BY normalizedonlinebalance DESC, address DESC LIMIT ? OFFSET ?`, rnd, n, offset) - - if err != nil { - return nil, err - } - defer rows.Close() - - res := make(map[basics.Address]*ledgercore.OnlineAccount, n) - for rows.Next() { - var addrbuf []byte - var buf []byte - var normBal sql.NullInt64 - var updround sql.NullInt64 - err = rows.Scan(&addrbuf, &normBal, &buf, &updround) - if err != nil { - return nil, err - } - - var data baseOnlineAccountData - err = protocol.Decode(buf, &data) - if err != nil { - return nil, err - } - - var addr basics.Address - if len(addrbuf) != len(addr) { - err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) - return nil, err - } - - if !normBal.Valid { - return nil, fmt.Errorf("non valid norm balance for online account %s", addr.String()) - } - - copy(addr[:], addrbuf) - // TODO: figure out protocol to use for rewards - // The original implementation uses current proto to recalculate norm balance - // In the same time, in accountsNewRound genesis protocol is used to fill norm balance value - // In order to be consistent with the original implementation recalculate the balance with current proto - normBalance := basics.NormalizedOnlineAccountBalance(basics.Online, data.RewardsBase, data.MicroAlgos, proto) - oa := data.GetOnlineAccount(addr, normBalance) - res[addr] = &oa - } - - return res, rows.Err() -} - -func onlineAccountsAll(tx *sql.Tx, maxAccounts uint64) ([]persistedOnlineAccountData, error) { - rows, err := tx.Query("SELECT rowid, address, updround, data FROM onlineaccounts ORDER BY address, updround ASC") - if err != nil { - return nil, err - } - defer rows.Close() - - result := make([]persistedOnlineAccountData, 0, maxAccounts) - var numAccounts uint64 - seenAddr := make([]byte, len(basics.Address{})) - for rows.Next() { - var addrbuf []byte - var buf []byte - data := persistedOnlineAccountData{} - err := rows.Scan(&data.rowid, &addrbuf, &data.updRound, &buf) - if err != nil { - return nil, err - } - if len(addrbuf) != len(data.addr) { - err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(data.addr)) - return nil, err - } - if maxAccounts > 0 { - if !bytes.Equal(seenAddr, addrbuf) { - numAccounts++ - if numAccounts > maxAccounts { - break - } - copy(seenAddr, addrbuf) - } - } - copy(data.addr[:], addrbuf) - err = protocol.Decode(buf, &data.accountData) - if err != nil { - return nil, err - } - result = append(result, data) - } - return result, nil -} - -func accountsTotals(ctx context.Context, q db.Queryable, catchpointStaging bool) (totals ledgercore.AccountTotals, err error) { - id := "" - if catchpointStaging { - id = "catchpointStaging" - } - row := q.QueryRowContext(ctx, "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) - - return -} - -func accountsPutTotals(tx *sql.Tx, totals ledgercore.AccountTotals, catchpointStaging bool) error { - id := "" - if catchpointStaging { - id = "catchpointStaging" - } - _, err := tx.Exec("REPLACE INTO accounttotals (id, online, onlinerewardunits, offline, offlinerewardunits, notparticipating, notparticipatingrewardunits, rewardslevel) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - id, - totals.Online.Money.Raw, totals.Online.RewardUnits, - totals.Offline.Money.Raw, totals.Offline.RewardUnits, - totals.NotParticipating.Money.Raw, totals.NotParticipating.RewardUnits, - totals.RewardsLevel) - return err -} - -func accountsOnlineRoundParams(tx *sql.Tx) (onlineRoundParamsData []ledgercore.OnlineRoundParamsData, endRound basics.Round, err error) { - rows, err := tx.Query("SELECT rnd, data FROM onlineroundparamstail ORDER BY rnd ASC") - if err != nil { - return nil, 0, err - } - defer rows.Close() - - for rows.Next() { - var buf []byte - err = rows.Scan(&endRound, &buf) - if err != nil { - return nil, 0, err - } - - var data ledgercore.OnlineRoundParamsData - err = protocol.Decode(buf, &data) - if err != nil { - return nil, 0, err - } - - onlineRoundParamsData = append(onlineRoundParamsData, data) - } - return -} - -func accountsPutOnlineRoundParams(tx *sql.Tx, onlineRoundParamsData []ledgercore.OnlineRoundParamsData, startRound basics.Round) error { - insertStmt, err := tx.Prepare("INSERT INTO onlineroundparamstail (rnd, data) VALUES (?, ?)") - if err != nil { - return err - } - - for i, onlineRoundParams := range onlineRoundParamsData { - _, err = insertStmt.Exec(startRound+basics.Round(i), protocol.Encode(&onlineRoundParams)) - if err != nil { - return err - } - } - return nil -} - -func accountsPruneOnlineRoundParams(tx *sql.Tx, deleteBeforeRound basics.Round) error { - _, err := tx.Exec("DELETE FROM onlineroundparamstail WHERE rnd 0 - hasResources := resources.len() > 0 - hasKvPairs := len(kvPairs) > 0 - hasCreatables := len(creatables) > 0 - - writer, err := makeAccountsSQLWriter(tx, hasAccounts, hasResources, hasKvPairs, hasCreatables) - if err != nil { - return - } - defer writer.close() - - return accountsNewRoundImpl(writer, updates, resources, kvPairs, creatables, proto, lastUpdateRound) -} - -func onlineAccountsNewRound( - tx *sql.Tx, - updates compactOnlineAccountDeltas, - proto config.ConsensusParams, lastUpdateRound basics.Round, -) (updatedAccounts []persistedOnlineAccountData, err error) { - hasAccounts := updates.len() > 0 - - writer, err := makeOnlineAccountsSQLWriter(tx, hasAccounts) - if err != nil { - return - } - defer writer.close() - - updatedAccounts, err = onlineAccountsNewRoundImpl(writer, updates, proto, lastUpdateRound) - return -} - -// accountsNewRoundImpl updates the accountbase and assetcreators tables by applying the provided deltas to the accounts / creatables. -// The function returns a persistedAccountData for the modified accounts which can be stored in the base cache. -func accountsNewRoundImpl( - writer accountsWriter, - updates compactAccountDeltas, resources compactResourcesDeltas, kvPairs map[string]modifiedKvValue, creatables map[basics.CreatableIndex]ledgercore.ModifiedCreatable, - proto config.ConsensusParams, lastUpdateRound basics.Round, -) (updatedAccounts []persistedAccountData, updatedResources map[basics.Address][]persistedResourcesData, updatedKVs map[string]persistedKVData, err error) { - updatedAccounts = make([]persistedAccountData, updates.len()) - updatedAccountIdx := 0 - newAddressesRowIDs := make(map[basics.Address]int64) - for i := 0; i < updates.len(); i++ { - data := updates.getByIdx(i) - if data.oldAcct.rowid == 0 { - // zero rowid means we don't have a previous value. - if data.newAcct.IsEmpty() { - // IsEmpty means we don't have a previous value. Note, can't use newAcct.MsgIsZero - // because of non-zero UpdateRound field in a new delta - // if we didn't had it before, and we don't have anything now, just skip it. - } else { - // create a new entry. - var rowid int64 - normBalance := data.newAcct.NormalizedOnlineBalance(proto) - rowid, err = writer.insertAccount(data.address, normBalance, data.newAcct) - if err == nil { - updatedAccounts[updatedAccountIdx].rowid = rowid - updatedAccounts[updatedAccountIdx].accountData = data.newAcct - newAddressesRowIDs[data.address] = rowid - } - } - } else { - // non-zero rowid means we had a previous value. - if data.newAcct.IsEmpty() { - // new value is zero, which means we need to delete the current value. - var rowsAffected int64 - rowsAffected, err = writer.deleteAccount(data.oldAcct.rowid) - if err == nil { - // we deleted the entry successfully. - updatedAccounts[updatedAccountIdx].rowid = 0 - updatedAccounts[updatedAccountIdx].accountData = baseAccountData{} - if rowsAffected != 1 { - err = fmt.Errorf("failed to delete accountbase row for account %v, rowid %d", data.address, data.oldAcct.rowid) - } - } - } else { - var rowsAffected int64 - normBalance := data.newAcct.NormalizedOnlineBalance(proto) - rowsAffected, err = writer.updateAccount(data.oldAcct.rowid, normBalance, data.newAcct) - if err == nil { - // rowid doesn't change on update. - updatedAccounts[updatedAccountIdx].rowid = data.oldAcct.rowid - updatedAccounts[updatedAccountIdx].accountData = data.newAcct - if rowsAffected != 1 { - err = fmt.Errorf("failed to update accountbase row for account %v, rowid %d", data.address, data.oldAcct.rowid) - } - } - } - } - - if err != nil { - return - } - - // set the returned persisted account states so that we could store that as the baseAccounts in commitRound - updatedAccounts[updatedAccountIdx].round = lastUpdateRound - updatedAccounts[updatedAccountIdx].addr = data.address - updatedAccountIdx++ - } - - updatedResources = make(map[basics.Address][]persistedResourcesData) - - // the resources update is going to be made in three parts: - // on the first loop, we will find out all the entries that need to be deleted, and parepare a pendingResourcesDeletion map. - // on the second loop, we will perform update/insertion. when considering inserting, we would test the pendingResourcesDeletion to see - // if the said entry was scheduled to be deleted. If so, we would "upgrade" the insert operation into an update operation. - // on the last loop, we would delete the remainder of the resource entries that were detected in loop #1 and were not upgraded in loop #2. - // the rationale behind this is that addrid might get reused, and we need to ensure - // that at all times there are no two representations of the same entry in the resources table. - // ( which would trigger a constrain violation ) - type resourceKey struct { - addrid int64 - aidx basics.CreatableIndex - } - var pendingResourcesDeletion map[resourceKey]struct{} // map to indicate which resources need to be deleted - for i := 0; i < resources.len(); i++ { - data := resources.getByIdx(i) - if data.oldResource.addrid == 0 || data.oldResource.data.IsEmpty() || !data.newResource.IsEmpty() { - continue - } - if pendingResourcesDeletion == nil { - pendingResourcesDeletion = make(map[resourceKey]struct{}) - } - pendingResourcesDeletion[resourceKey{addrid: data.oldResource.addrid, aidx: data.oldResource.aidx}] = struct{}{} - - entry := persistedResourcesData{addrid: 0, aidx: data.oldResource.aidx, data: makeResourcesData(0), round: lastUpdateRound} - deltas := updatedResources[data.address] - deltas = append(deltas, entry) - updatedResources[data.address] = deltas + entry := store.PersistedResourcesData{Addrid: 0, Aidx: data.oldResource.Aidx, Data: store.MakeResourcesData(0), Round: lastUpdateRound} + deltas := updatedResources[data.address] + deltas = append(deltas, entry) + updatedResources[data.address] = deltas } for i := 0; i < resources.len(); i++ { data := resources.getByIdx(i) addr := data.address - aidx := data.oldResource.aidx - addrid := data.oldResource.addrid + aidx := data.oldResource.Aidx + addrid := data.oldResource.Addrid if addrid == 0 { // new entry, data.oldResource does not have addrid // check if this delta is part of in-memory only account // that is created, funded, transferred, and closed within a commit range - inMemEntry := data.oldResource.data.IsEmpty() && data.newResource.IsEmpty() + inMemEntry := data.oldResource.Data.IsEmpty() && data.newResource.IsEmpty() addrid = newAddressesRowIDs[addr] if addrid == 0 && !inMemEntry { err = fmt.Errorf("cannot resolve address %s (%d), aidx %d, data %v", addr.String(), addrid, aidx, data.newResource) return } } - var entry persistedResourcesData - if data.oldResource.data.IsEmpty() { + var entry store.PersistedResourcesData + if data.oldResource.Data.IsEmpty() { // IsEmpty means we don't have a previous value. Note, can't use oldResource.data.MsgIsZero // because of possibility of empty asset holdings or app local state after opting in, // as well as non-zero UpdateRound field in a new delta @@ -3744,7 +854,7 @@ func accountsNewRoundImpl( // if we didn't had it before, and we don't have anything now, just skip it. // set zero addrid to mark this entry invalid for subsequent addr to addrid resolution // because the base account might gone. - entry = persistedResourcesData{addrid: 0, aidx: aidx, data: makeResourcesData(0), round: lastUpdateRound} + entry = store.PersistedResourcesData{Addrid: 0, Aidx: aidx, Data: store.MakeResourcesData(0), Round: lastUpdateRound} } else { // create a new entry. if !data.newResource.IsApp() && !data.newResource.IsAsset() { @@ -3758,19 +868,19 @@ func accountsNewRoundImpl( // update the database entry instead of deleting + inserting. delete(pendingResourcesDeletion, resourceKey{addrid: addrid, aidx: aidx}) var rowsAffected int64 - rowsAffected, err = writer.updateResource(addrid, aidx, data.newResource) + rowsAffected, err = writer.UpdateResource(addrid, aidx, data.newResource) if err == nil { // rowid doesn't change on update. - entry = persistedResourcesData{addrid: addrid, aidx: aidx, data: data.newResource, round: lastUpdateRound} + entry = store.PersistedResourcesData{Addrid: addrid, Aidx: aidx, Data: data.newResource, Round: lastUpdateRound} if rowsAffected != 1 { err = fmt.Errorf("failed to update resources row for addr %s (%d), aidx %d", addr, addrid, aidx) } } } else { - _, err = writer.insertResource(addrid, aidx, data.newResource) + _, err = writer.InsertResource(addrid, aidx, data.newResource) if err == nil { // set the returned persisted account states so that we could store that as the baseResources in commitRound - entry = persistedResourcesData{addrid: addrid, aidx: aidx, data: data.newResource, round: lastUpdateRound} + entry = store.PersistedResourcesData{Addrid: addrid, Aidx: aidx, Data: data.newResource, Round: lastUpdateRound} } } } @@ -3786,10 +896,10 @@ func accountsNewRoundImpl( return } var rowsAffected int64 - rowsAffected, err = writer.updateResource(addrid, aidx, data.newResource) + rowsAffected, err = writer.UpdateResource(addrid, aidx, data.newResource) if err == nil { // rowid doesn't change on update. - entry = persistedResourcesData{addrid: addrid, aidx: aidx, data: data.newResource, round: lastUpdateRound} + entry = store.PersistedResourcesData{Addrid: addrid, Aidx: aidx, Data: data.newResource, Round: lastUpdateRound} if rowsAffected != 1 { err = fmt.Errorf("failed to update resources row for addr %s (%d), aidx %d", addr, addrid, aidx) } @@ -3810,7 +920,7 @@ func accountsNewRoundImpl( for delRes := range pendingResourcesDeletion { // new value is zero, which means we need to delete the current value. var rowsAffected int64 - rowsAffected, err = writer.deleteResource(delRes.addrid, delRes.aidx) + rowsAffected, err = writer.DeleteResource(delRes.addrid, delRes.aidx) if err == nil { // we deleted the entry successfully. // set zero addrid to mark this entry invalid for subsequent addr to addrid resolution @@ -3824,21 +934,21 @@ func accountsNewRoundImpl( } } - updatedKVs = make(map[string]persistedKVData, len(kvPairs)) + updatedKVs = make(map[string]store.PersistedKVData, len(kvPairs)) for key, mv := range kvPairs { if mv.data != nil { // reminder: check oldData for nil here, b/c bytes.Equal conflates nil and "". if mv.oldData != nil && bytes.Equal(mv.oldData, mv.data) { continue // changed back within the delta span } - err = writer.upsertKvPair(key, mv.data) - updatedKVs[key] = persistedKVData{value: mv.data, round: lastUpdateRound} + err = writer.UpsertKvPair(key, mv.data) + updatedKVs[key] = store.PersistedKVData{Value: mv.data, Round: lastUpdateRound} } else { if mv.oldData == nil { // Came and went within the delta span continue } - err = writer.deleteKvPair(key) - updatedKVs[key] = persistedKVData{value: nil, round: lastUpdateRound} + err = writer.DeleteKvPair(key) + updatedKVs[key] = store.PersistedKVData{Value: nil, Round: lastUpdateRound} } if err != nil { return @@ -3847,9 +957,9 @@ func accountsNewRoundImpl( for cidx, cdelta := range creatables { if cdelta.Created { - _, err = writer.insertCreatable(cidx, cdelta.Ctype, cdelta.Creator[:]) + _, err = writer.InsertCreatable(cidx, cdelta.Ctype, cdelta.Creator[:]) } else { - _, err = writer.deleteCreatable(cidx, cdelta.Ctype) + _, err = writer.DeleteCreatable(cidx, cdelta.Ctype) } if err != nil { return @@ -3860,9 +970,9 @@ func accountsNewRoundImpl( } func onlineAccountsNewRoundImpl( - writer onlineAccountsWriter, updates compactOnlineAccountDeltas, + writer store.OnlineAccountsWriter, updates compactOnlineAccountDeltas, proto config.ConsensusParams, lastUpdateRound basics.Round, -) (updatedAccounts []persistedOnlineAccountData, err error) { +) (updatedAccounts []store.PersistedOnlineAccountData, err error) { for i := 0; i < updates.len(); i++ { data := updates.getByIdx(i) @@ -3871,7 +981,7 @@ func onlineAccountsNewRoundImpl( newAcct := data.newAcct[j] updRound := data.updRound[j] newStatus := data.newStatus[j] - if prevAcct.rowid == 0 { + if prevAcct.Rowid == 0 { // zero rowid means we don't have a previous value. if newAcct.IsEmpty() { // IsEmpty means we don't have a previous value. @@ -3882,387 +992,77 @@ func onlineAccountsNewRoundImpl( err = fmt.Errorf("empty voting data for online account %s: %v", data.address.String(), newAcct) } else { // create a new entry. - var rowid int64 - normBalance := newAcct.NormalizedOnlineBalance(proto) - rowid, err = writer.insertOnlineAccount(data.address, normBalance, newAcct, updRound, uint64(newAcct.VoteLastValid)) - if err == nil { - updated := persistedOnlineAccountData{ - addr: data.address, - accountData: newAcct, - round: lastUpdateRound, - rowid: rowid, - updRound: basics.Round(updRound), - } - updatedAccounts = append(updatedAccounts, updated) - prevAcct = updated - } - } - } else if !newAcct.IsVotingEmpty() { - err = fmt.Errorf("non-empty voting data for non-online account %s: %v", data.address.String(), newAcct) - } - } - } else { - // non-zero rowid means we had a previous value. - if newAcct.IsVotingEmpty() { - // new value is zero then go offline - if newStatus == basics.Online { - err = fmt.Errorf("empty voting data but online account %s: %v", data.address.String(), newAcct) - } else { - var rowid int64 - rowid, err = writer.insertOnlineAccount(data.address, 0, baseOnlineAccountData{}, updRound, 0) - if err == nil { - updated := persistedOnlineAccountData{ - addr: data.address, - accountData: baseOnlineAccountData{}, - round: lastUpdateRound, - rowid: rowid, - updRound: basics.Round(updRound), - } - - updatedAccounts = append(updatedAccounts, updated) - prevAcct = updated - } - } - } else { - if prevAcct.accountData != newAcct { - var rowid int64 - normBalance := newAcct.NormalizedOnlineBalance(proto) - rowid, err = writer.insertOnlineAccount(data.address, normBalance, newAcct, updRound, uint64(newAcct.VoteLastValid)) - if err == nil { - updated := persistedOnlineAccountData{ - addr: data.address, - accountData: newAcct, - round: lastUpdateRound, - rowid: rowid, - updRound: basics.Round(updRound), - } - - updatedAccounts = append(updatedAccounts, updated) - prevAcct = updated - } - } - } - } - - if err != nil { - return - } - } - } - - return -} - -func rowidsToChunkedArgs(rowids []int64) [][]interface{} { - const sqliteMaxVariableNumber = 999 - - numChunks := len(rowids)/sqliteMaxVariableNumber + 1 - if len(rowids)%sqliteMaxVariableNumber == 0 { - numChunks-- - } - chunks := make([][]interface{}, numChunks) - if numChunks == 1 { - // optimize memory consumption for the most common case - chunks[0] = make([]interface{}, len(rowids)) - for i, rowid := range rowids { - chunks[0][i] = interface{}(rowid) - } - } else { - for i := 0; i < numChunks; i++ { - chunkSize := sqliteMaxVariableNumber - if i == numChunks-1 { - chunkSize = len(rowids) - (numChunks-1)*sqliteMaxVariableNumber - } - chunks[i] = make([]interface{}, chunkSize) - } - for i, rowid := range rowids { - chunkIndex := i / sqliteMaxVariableNumber - chunks[chunkIndex][i%sqliteMaxVariableNumber] = interface{}(rowid) - } - } - return chunks -} - -func onlineAccountsDeleteByRowIDs(tx *sql.Tx, rowids []int64) (err error) { - if len(rowids) == 0 { - return - } - - // sqlite3 < 3.32.0 allows SQLITE_MAX_VARIABLE_NUMBER = 999 bindings - // see https://www.sqlite.org/limits.html - // rowids might be larger => split to chunks are remove - chunks := rowidsToChunkedArgs(rowids) - for _, chunk := range chunks { - _, err = tx.Exec("DELETE FROM onlineaccounts WHERE rowid IN (?"+strings.Repeat(",?", len(chunk)-1)+")", chunk...) - if err != nil { - return - } - } - return -} - -// onlineAccountsDelete deleted entries with updRound <= expRound -func onlineAccountsDelete(tx *sql.Tx, forgetBefore basics.Round) (err error) { - rows, err := tx.Query("SELECT rowid, address, updRound, data FROM onlineaccounts WHERE updRound < ? ORDER BY address, updRound DESC", forgetBefore) - if err != nil { - return err - } - defer rows.Close() - - var rowids []int64 - var rowid sql.NullInt64 - var updRound sql.NullInt64 - var buf []byte - var addrbuf []byte - - var prevAddr []byte - - for rows.Next() { - err = rows.Scan(&rowid, &addrbuf, &updRound, &buf) - if err != nil { - return err - } - if !rowid.Valid || !updRound.Valid { - return fmt.Errorf("onlineAccountsDelete: invalid rowid or updRound") - } - if len(addrbuf) != len(basics.Address{}) { - err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(basics.Address{})) - return - } - - if !bytes.Equal(addrbuf, prevAddr) { - // new address - // if the first (latest) entry is - // - offline then delete all - // - online then safe to delete all previous except this first (latest) - - // reset the state - prevAddr = addrbuf - - var oad baseOnlineAccountData - err = protocol.Decode(buf, &oad) - if err != nil { - return - } - if oad.IsVotingEmpty() { - // delete this and all subsequent - rowids = append(rowids, rowid.Int64) - } - - // restart the loop - // if there are some subsequent entries, they will deleted on the next iteration - // if no subsequent entries, the loop will reset the state and the latest entry does not get deleted - continue - } - // delete all subsequent entries - rowids = append(rowids, rowid.Int64) - } - - return onlineAccountsDeleteByRowIDs(tx, rowids) -} - -// updates the round number associated with the current account data. -func updateAccountsRound(tx *sql.Tx, rnd basics.Round) (err error) { - res, err := tx.Exec("UPDATE acctrounds SET rnd=? WHERE id='acctbase' AND rnd rnd { - err = fmt.Errorf("newRound %d is not after base %d", rnd, base) - return - } else if base != rnd { - err = fmt.Errorf("updateAccountsRound(acctbase, %d): expected to update 1 row but got %d", rnd, aff) - return - } - } - return -} - -// updates the round number associated with the hash of current account data. -func updateAccountsHashRound(ctx context.Context, tx *sql.Tx, hashRound basics.Round) (err error) { - res, err := tx.ExecContext(ctx, "INSERT OR REPLACE INTO acctrounds(id,rnd) VALUES('hashbase',?)", hashRound) - if err != nil { - return - } - - aff, err := res.RowsAffected() - if err != nil { - return - } - - if aff != 1 { - err = fmt.Errorf("updateAccountsHashRound(hashbase,%d): expected to update 1 row but got %d", hashRound, aff) - return - } - return -} - -// totalAccounts returns the total number of accounts -func totalAccounts(ctx context.Context, tx *sql.Tx) (total uint64, err error) { - err = tx.QueryRowContext(ctx, "SELECT count(1) FROM accountbase").Scan(&total) - if err == sql.ErrNoRows { - total = 0 - err = nil - return - } - return -} - -func totalKVs(ctx context.Context, tx *sql.Tx) (total uint64, err error) { - err = tx.QueryRowContext(ctx, "SELECT count(1) FROM kvstore").Scan(&total) - if err == sql.ErrNoRows { - total = 0 - err = nil - return - } - 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 warning - // 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 - } + var rowid int64 + normBalance := newAcct.NormalizedOnlineBalance(proto) + rowid, err = writer.InsertOnlineAccount(data.address, normBalance, newAcct, updRound, uint64(newAcct.VoteLastValid)) + if err == nil { + updated := store.PersistedOnlineAccountData{ + Addr: data.address, + AccountData: newAcct, + Round: lastUpdateRound, + Rowid: rowid, + UpdRound: basics.Round(updRound), + } + updatedAccounts = append(updatedAccounts, updated) + prevAcct = updated + } + } + } else if !newAcct.IsVotingEmpty() { + err = fmt.Errorf("non-empty voting data for non-online account %s: %v", data.address.String(), newAcct) + } + } + } else { + // non-zero rowid means we had a previous value. + if newAcct.IsVotingEmpty() { + // new value is zero then go offline + if newStatus == basics.Online { + err = fmt.Errorf("empty voting data but online account %s: %v", data.address.String(), newAcct) + } else { + var rowid int64 + rowid, err = writer.InsertOnlineAccount(data.address, 0, store.BaseOnlineAccountData{}, updRound, 0) + if err == nil { + updated := store.PersistedOnlineAccountData{ + Addr: data.address, + AccountData: store.BaseOnlineAccountData{}, + Round: lastUpdateRound, + Rowid: rowid, + UpdRound: basics.Round(updRound), + } - if len(addrbuf) != len(addr) { - err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) - return - } - copy(addr[:], addrbuf[:]) - scannedAccounts++ + updatedAccounts = append(updatedAccounts, updated) + prevAcct = updated + } + } + } else { + if prevAcct.AccountData != newAcct { + var rowid int64 + normBalance := newAcct.NormalizedOnlineBalance(proto) + rowid, err = writer.InsertOnlineAccount(data.address, normBalance, newAcct, updRound, uint64(newAcct.VoteLastValid)) + if err == nil { + updated := store.PersistedOnlineAccountData{ + Addr: data.address, + AccountData: newAcct, + Round: lastUpdateRound, + Rowid: rowid, + UpdRound: basics.Round(updRound), + } - // decode and re-encode: - var decodedAccountData basics.AccountData - err = protocol.Decode(preencodedAccountData, &decodedAccountData) - if err != nil { - return - } - reencodedAccountData := protocol.Encode(&decodedAccountData) - if bytes.Equal(preencodedAccountData, reencodedAccountData) { - // these are identical, no need to store re-encoded account data - continue - } + updatedAccounts = append(updatedAccounts, updated) + prevAcct = updated + } + } + } + } - // 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) + if err != nil { + return + } } - modifiedAccounts++ } - err = rows.Err() - updateStmt.Close() return } -// MerkleCommitter allows storing and loading merkletrie pages from a sqlite database. -// -//msgp:ignore MerkleCommitter -type MerkleCommitter struct { - tx *sql.Tx - deleteStmt *sql.Stmt - insertStmt *sql.Stmt - selectStmt *sql.Stmt -} - -// MakeMerkleCommitter creates a MerkleCommitter object that implements the merkletrie.Committer interface allowing storing and loading -// merkletrie pages from a sqlite database. -func MakeMerkleCommitter(tx *sql.Tx, staging bool) (mc *MerkleCommitter, err error) { - mc = &MerkleCommitter{tx: tx} - accountHashesTable := "accounthashes" - if staging { - accountHashesTable = "catchpointaccounthashes" - } - mc.deleteStmt, err = tx.Prepare("DELETE FROM " + accountHashesTable + " WHERE id=?") - if err != nil { - return nil, err - } - mc.insertStmt, err = tx.Prepare("INSERT OR REPLACE INTO " + accountHashesTable + "(id, data) VALUES(?, ?)") - if err != nil { - return nil, err - } - mc.selectStmt, err = tx.Prepare("SELECT data FROM " + accountHashesTable + " WHERE id = ?") - if err != nil { - return nil, err - } - return mc, nil -} - -// StorePage is the merkletrie.Committer interface implementation, stores a single page in a sqlite database table. -func (mc *MerkleCommitter) StorePage(page uint64, content []byte) error { - if len(content) == 0 { - _, err := mc.deleteStmt.Exec(page) - return err - } - _, err := mc.insertStmt.Exec(page, content) - return err -} - -// LoadPage is the merkletrie.Committer interface implementation, load a single page from a sqlite database table. -func (mc *MerkleCommitter) LoadPage(page uint64) (content []byte, err error) { - err = mc.selectStmt.QueryRow(page).Scan(&content) - if err == sql.ErrNoRows { - content = nil - err = nil - return - } else if err != nil { - return nil, err - } - return content, nil -} - // catchpointAccountResourceCounter keeps track of the resources processed for the current account type catchpointAccountResourceCounter struct { totalAppParams uint64 @@ -4299,9 +1099,9 @@ func (iterator *encodedAccountsBatchIter) Next(ctx context.Context, tx *sql.Tx, // gather up to accountCount encoded accounts. bals = make([]encodedBalanceRecordV6, 0, accountCount) var encodedRecord encodedBalanceRecordV6 - var baseAcct baseAccountData + var baseAcct store.BaseAccountData var numAcct int - baseCb := func(addr basics.Address, rowid int64, accountData *baseAccountData, encodedAccountData []byte) (err error) { + baseCb := func(addr basics.Address, rowid int64, accountData *store.BaseAccountData, encodedAccountData []byte) (err error) { encodedRecord = encodedBalanceRecordV6{Address: addr, AccountData: encodedAccountData} baseAcct = *accountData numAcct++ @@ -4311,7 +1111,7 @@ func (iterator *encodedAccountsBatchIter) Next(ctx context.Context, tx *sql.Tx, var totalResources int // emptyCount := 0 - resCb := func(addr basics.Address, cidx basics.CreatableIndex, resData *resourcesData, encodedResourceData []byte, lastResource bool) error { + resCb := func(addr basics.Address, cidx basics.CreatableIndex, resData *store.ResourcesData, encodedResourceData []byte, lastResource bool) error { emptyBaseAcct := baseAcct.TotalAppParams == 0 && baseAcct.TotalAppLocalStates == 0 && baseAcct.TotalAssetParams == 0 && baseAcct.TotalAssets == 0 if !emptyBaseAcct && resData != nil { @@ -4451,7 +1251,7 @@ func makeOrderedAccountsIter(tx *sql.Tx, accountCount int) *orderedAccountsIter type pendingBaseRow struct { addr basics.Address rowid int64 - accountData *baseAccountData + accountData *store.BaseAccountData encodedAccountData []byte } @@ -4463,8 +1263,8 @@ type pendingResourceRow struct { func processAllResources( resRows *sql.Rows, - addr basics.Address, accountData *baseAccountData, acctRowid int64, pr pendingResourceRow, resourceCount int, - callback func(addr basics.Address, creatableIdx basics.CreatableIndex, resData *resourcesData, encodedResourceData []byte, lastResource bool) error, + addr basics.Address, accountData *store.BaseAccountData, acctRowid int64, pr pendingResourceRow, resourceCount int, + callback func(addr basics.Address, creatableIdx basics.CreatableIndex, resData *store.ResourcesData, encodedResourceData []byte, lastResource bool) error, ) (pendingResourceRow, int, error) { var err error count := 0 @@ -4474,7 +1274,7 @@ func processAllResources( var buf []byte var addrid int64 var aidx basics.CreatableIndex - var resData resourcesData + var resData store.ResourcesData for { if pr.addrid != 0 { // some accounts may not have resources, consider the following case: @@ -4513,7 +1313,7 @@ func processAllResources( return pendingResourceRow{addrid, aidx, buf}, count, err } } - resData = resourcesData{} + resData = store.ResourcesData{} err = protocol.Decode(buf, &resData) if err != nil { return pendingResourceRow{}, count, err @@ -4535,8 +1335,8 @@ func processAllResources( func processAllBaseAccountRecords( baseRows *sql.Rows, resRows *sql.Rows, - baseCb func(addr basics.Address, rowid int64, accountData *baseAccountData, encodedAccountData []byte) error, - resCb func(addr basics.Address, creatableIdx basics.CreatableIndex, resData *resourcesData, encodedResourceData []byte, lastResource bool) error, + baseCb func(addr basics.Address, rowid int64, accountData *store.BaseAccountData, encodedAccountData []byte) error, + resCb func(addr basics.Address, creatableIdx basics.CreatableIndex, resData *store.ResourcesData, encodedResourceData []byte, lastResource bool) error, pendingBase pendingBaseRow, pendingResource pendingResourceRow, accountCount int, resourceCount int, ) (int, pendingBaseRow, pendingResourceRow, error) { var addr basics.Address @@ -4544,7 +1344,7 @@ func processAllBaseAccountRecords( var err error count := 0 - var accountData baseAccountData + var accountData store.BaseAccountData var addrbuf []byte var buf []byte var rowid int64 @@ -4572,7 +1372,7 @@ func processAllBaseAccountRecords( copy(addr[:], addrbuf) - accountData = baseAccountData{} + accountData = store.BaseAccountData{} err = protocol.Decode(buf, &accountData) if err != nil { return 0, pendingBaseRow{}, pendingResourceRow{}, err @@ -4614,147 +1414,6 @@ func processAllBaseAccountRecords( return count, pendingBaseRow{}, pendingResource, nil } -// loadFullAccount converts baseAccountData into basics.AccountData and loads all resources as needed -func loadFullAccount(ctx context.Context, tx *sql.Tx, resourcesTable string, addr basics.Address, addrid int64, data baseAccountData) (ad basics.AccountData, err error) { - ad = data.GetAccountData() - - hasResources := false - if data.TotalAppParams > 0 { - ad.AppParams = make(map[basics.AppIndex]basics.AppParams, data.TotalAppParams) - hasResources = true - } - if data.TotalAppLocalStates > 0 { - ad.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState, data.TotalAppLocalStates) - hasResources = true - } - if data.TotalAssetParams > 0 { - ad.AssetParams = make(map[basics.AssetIndex]basics.AssetParams, data.TotalAssetParams) - hasResources = true - } - if data.TotalAssets > 0 { - ad.Assets = make(map[basics.AssetIndex]basics.AssetHolding, data.TotalAssets) - hasResources = true - } - - if !hasResources { - return - } - - var resRows *sql.Rows - query := fmt.Sprintf("SELECT aidx, data FROM %s where addrid = ?", resourcesTable) - resRows, err = tx.QueryContext(ctx, query, addrid) - if err != nil { - return - } - defer resRows.Close() - - for resRows.Next() { - var buf []byte - var aidx int64 - err = resRows.Scan(&aidx, &buf) - if err != nil { - return - } - var resData resourcesData - err = protocol.Decode(buf, &resData) - if err != nil { - return - } - if resData.ResourceFlags == resourceFlagsNotHolding { - err = fmt.Errorf("addr %s (%d) aidx = %d resourceFlagsNotHolding should not be persisted", addr.String(), addrid, aidx) - return - } - if resData.IsApp() { - if resData.IsOwning() { - ad.AppParams[basics.AppIndex(aidx)] = resData.GetAppParams() - } - if resData.IsHolding() { - ad.AppLocalStates[basics.AppIndex(aidx)] = resData.GetAppLocalState() - } - } else if resData.IsAsset() { - if resData.IsOwning() { - ad.AssetParams[basics.AssetIndex(aidx)] = resData.GetAssetParams() - } - if resData.IsHolding() { - ad.Assets[basics.AssetIndex(aidx)] = resData.GetAssetHolding() - } - } else { - err = fmt.Errorf("unknown resource data: %v", resData) - return - } - } - - if uint64(len(ad.AssetParams)) != data.TotalAssetParams { - err = fmt.Errorf("%s assets params mismatch: %d != %d", addr.String(), len(ad.AssetParams), data.TotalAssetParams) - } - if err == nil && uint64(len(ad.Assets)) != data.TotalAssets { - err = fmt.Errorf("%s assets mismatch: %d != %d", addr.String(), len(ad.Assets), data.TotalAssets) - } - if err == nil && uint64(len(ad.AppParams)) != data.TotalAppParams { - err = fmt.Errorf("%s app params mismatch: %d != %d", addr.String(), len(ad.AppParams), data.TotalAppParams) - } - if err == nil && uint64(len(ad.AppLocalStates)) != data.TotalAppLocalStates { - err = fmt.Errorf("%s app local states mismatch: %d != %d", addr.String(), len(ad.AppLocalStates), data.TotalAppLocalStates) - } - if err != nil { - return - } - - return -} - -// LoadAllFullAccounts loads all accounts from balancesTable and resourcesTable. -// On every account full load it invokes acctCb callback to report progress and data. -func LoadAllFullAccounts( - ctx context.Context, tx *sql.Tx, - balancesTable string, resourcesTable string, - acctCb func(basics.Address, basics.AccountData), -) (count int, err error) { - baseRows, err := tx.QueryContext(ctx, fmt.Sprintf("SELECT rowid, address, data FROM %s ORDER BY address", balancesTable)) - if err != nil { - return - } - defer baseRows.Close() - - for baseRows.Next() { - var addrbuf []byte - var buf []byte - var rowid sql.NullInt64 - err = baseRows.Scan(&rowid, &addrbuf, &buf) - if err != nil { - return - } - if !rowid.Valid { - err = fmt.Errorf("invalid rowid in %s", balancesTable) - return - } - - var data baseAccountData - 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) - - var ad basics.AccountData - ad, err = loadFullAccount(ctx, tx, resourcesTable, addr, rowid.Int64, data) - if err != nil { - return - } - - acctCb(addr, ad) - - count++ - } - return -} - // accountAddressHash is used by Next to return a single account address and the associated hash. type accountAddressHash struct { addrid int64 @@ -4810,8 +1469,8 @@ func (iterator *orderedAccountsIter) Next(ctx context.Context) (acct []accountAd } if iterator.step == oaiStepInsertAccountData { var lastAddrID int64 - baseCb := func(addr basics.Address, rowid int64, accountData *baseAccountData, encodedAccountData []byte) (err error) { - hash := accountHashBuilderV6(addr, accountData, encodedAccountData) + baseCb := func(addr basics.Address, rowid int64, accountData *store.BaseAccountData, encodedAccountData []byte) (err error) { + hash := store.AccountHashBuilderV6(addr, accountData, encodedAccountData) _, err = iterator.insertStmt.ExecContext(ctx, rowid, hash) if err != nil { return @@ -4820,9 +1479,9 @@ func (iterator *orderedAccountsIter) Next(ctx context.Context) (acct []accountAd return nil } - resCb := func(addr basics.Address, cidx basics.CreatableIndex, resData *resourcesData, encodedResourceData []byte, lastResource bool) error { + resCb := func(addr basics.Address, cidx basics.CreatableIndex, resData *store.ResourcesData, encodedResourceData []byte, lastResource bool) error { if resData != nil { - hash, err := resourcesHashBuilderV6(resData, addr, cidx, resData.UpdateRound, encodedResourceData) + hash, err := store.ResourcesHashBuilderV6(resData, addr, cidx, resData.UpdateRound, encodedResourceData) if err != nil { return err } @@ -4943,24 +1602,6 @@ func (iterator *orderedAccountsIter) Close(ctx context.Context) (err error) { return } -// createCatchpointStagingHashesIndex creates an index on catchpointpendinghashes to allow faster scanning according to the hash order -func lookupAccountAddressFromAddressID(ctx context.Context, tx *sql.Tx, addrid int64) (address basics.Address, err error) { - var addrbuf []byte - err = tx.QueryRowContext(ctx, "SELECT address FROM accountbase WHERE rowid = ?", addrid).Scan(&addrbuf) - if err != nil { - if err == sql.ErrNoRows { - err = fmt.Errorf("no matching address could be found for rowid %d: %w", addrid, err) - } - return - } - if len(addrbuf) != len(address) { - err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(address)) - return - } - copy(address[:], addrbuf) - return -} - // catchpointPendingHashesIterator allows us to iterate over the hashes in the catchpointpendinghashes table in their order. type catchpointPendingHashesIterator struct { hashCount int @@ -5019,294 +1660,3 @@ func (iterator *catchpointPendingHashesIterator) Close() { iterator.rows = nil } } - -// before compares the round numbers of two persistedAccountData and determines if the current persistedAccountData -// happened before the other. -func (pac *persistedAccountData) before(other *persistedAccountData) bool { - return pac.round < other.round -} - -// before compares the round numbers of two persistedResourcesData and determines if the current persistedResourcesData -// happened before the other. -func (prd *persistedResourcesData) before(other *persistedResourcesData) bool { - return prd.round < other.round -} - -// before compares the round numbers of two persistedKVData and determines if the current persistedKVData -// happened before the other. -func (prd persistedKVData) before(other *persistedKVData) bool { - return prd.round < other.round -} - -// before compares the round numbers of two persistedAccountData and determines if the current persistedAccountData -// happened before the other. -func (pac *persistedOnlineAccountData) before(other *persistedOnlineAccountData) bool { - return pac.updRound < other.updRound -} - -// txTailRoundLease is used as part of txTailRound for storing -// a single lease. -type txTailRoundLease struct { - _struct struct{} `codec:",omitempty,omitemptyarray"` - - Sender basics.Address `codec:"s"` - Lease [32]byte `codec:"l,allocbound=-"` - TxnIdx uint64 `code:"i"` //!-- index of the entry in TxnIDs/LastValid -} - -// TxTailRound contains the information about a single round of transactions. -// The TxnIDs and LastValid would both be of the same length, and are stored -// in that way for efficient message=pack encoding. The Leases would point to the -// respective transaction index. Note that this isn’t optimized for storing -// leases, as leases are extremely rare. -type txTailRound struct { - _struct struct{} `codec:",omitempty,omitemptyarray"` - - TxnIDs []transactions.Txid `codec:"i,allocbound=-"` - LastValid []basics.Round `codec:"v,allocbound=-"` - Leases []txTailRoundLease `codec:"l,allocbound=-"` - Hdr bookkeeping.BlockHeader `codec:"h,allocbound=-"` -} - -// encode the transaction tail data into a serialized form, and return the serialized data -// as well as the hash of the data. -func (t *txTailRound) encode() ([]byte, crypto.Digest) { - tailData := protocol.Encode(t) - hash := crypto.Hash(tailData) - return tailData, hash -} - -func txTailRoundFromBlock(blk bookkeeping.Block) (*txTailRound, error) { - payset, err := blk.DecodePaysetFlat() - if err != nil { - return nil, err - } - - tail := &txTailRound{} - - tail.TxnIDs = make([]transactions.Txid, len(payset)) - tail.LastValid = make([]basics.Round, len(payset)) - tail.Hdr = blk.BlockHeader - - for txIdxtxid, txn := range payset { - tail.TxnIDs[txIdxtxid] = txn.ID() - tail.LastValid[txIdxtxid] = txn.Txn.LastValid - if txn.Txn.Lease != [32]byte{} { - tail.Leases = append(tail.Leases, txTailRoundLease{ - Sender: txn.Txn.Sender, - Lease: txn.Txn.Lease, - TxnIdx: uint64(txIdxtxid), - }) - } - } - return tail, nil -} - -func txtailNewRound(ctx context.Context, tx *sql.Tx, baseRound basics.Round, roundData [][]byte, forgetBeforeRound basics.Round) error { - insertStmt, err := tx.PrepareContext(ctx, "INSERT INTO txtail(rnd, data) VALUES(?, ?)") - if err != nil { - return err - } - defer insertStmt.Close() - - for i, data := range roundData { - _, err = insertStmt.ExecContext(ctx, int(baseRound)+i, data[:]) - if err != nil { - return err - } - } - - _, err = tx.ExecContext(ctx, "DELETE FROM txtail WHERE rnd < ?", forgetBeforeRound) - return err -} - -func loadTxTail(ctx context.Context, tx *sql.Tx, dbRound basics.Round) (roundData []*txTailRound, roundHash []crypto.Digest, baseRound basics.Round, err error) { - rows, err := tx.QueryContext(ctx, "SELECT rnd, data FROM txtail ORDER BY rnd DESC") - if err != nil { - return nil, nil, 0, err - } - defer rows.Close() - - expectedRound := dbRound - for rows.Next() { - var round basics.Round - var data []byte - err = rows.Scan(&round, &data) - if err != nil { - return nil, nil, 0, err - } - if round != expectedRound { - return nil, nil, 0, fmt.Errorf("txtail table contain unexpected round %d; round %d was expected", round, expectedRound) - } - tail := &txTailRound{} - err = protocol.Decode(data, tail) - if err != nil { - return nil, nil, 0, err - } - roundData = append(roundData, tail) - roundHash = append(roundHash, crypto.Hash(data)) - expectedRound-- - } - // reverse the array ordering in-place so that it would be incremental order. - for i := 0; i < len(roundData)/2; i++ { - roundData[i], roundData[len(roundData)-i-1] = roundData[len(roundData)-i-1], roundData[i] - roundHash[i], roundHash[len(roundHash)-i-1] = roundHash[len(roundHash)-i-1], roundHash[i] - } - return roundData, roundHash, expectedRound + 1, nil -} - -// For the `catchpointfirststageinfo` table. -type catchpointFirstStageInfo struct { - _struct struct{} `codec:",omitempty,omitemptyarray"` - - Totals ledgercore.AccountTotals `codec:"accountTotals"` - TrieBalancesHash crypto.Digest `codec:"trieBalancesHash"` - // Total number of accounts in the catchpoint data file. Only set when catchpoint - // data files are generated. - TotalAccounts uint64 `codec:"accountsCount"` - - // Total number of accounts in the catchpoint data file. Only set when catchpoint - // data files are generated. - TotalKVs uint64 `codec:"kvsCount"` - - // Total number of chunks in the catchpoint data file. Only set when catchpoint - // data files are generated. - TotalChunks uint64 `codec:"chunksCount"` - // BiggestChunkLen is the size in the bytes of the largest chunk, used when re-packing. - BiggestChunkLen uint64 `codec:"biggestChunk"` -} - -func insertOrReplaceCatchpointFirstStageInfo(ctx context.Context, e db.Executable, round basics.Round, info *catchpointFirstStageInfo) error { - infoSerialized := protocol.Encode(info) - f := func() error { - query := "INSERT OR REPLACE INTO catchpointfirststageinfo(round, info) VALUES(?, ?)" - _, err := e.ExecContext(ctx, query, round, infoSerialized) - return err - } - return db.Retry(f) -} - -func selectCatchpointFirstStageInfo(ctx context.Context, q db.Queryable, round basics.Round) (catchpointFirstStageInfo, bool /*exists*/, error) { - var data []byte - f := func() error { - query := "SELECT info FROM catchpointfirststageinfo WHERE round=?" - err := q.QueryRowContext(ctx, query, round).Scan(&data) - if err == sql.ErrNoRows { - data = nil - return nil - } - return err - } - err := db.Retry(f) - if err != nil { - return catchpointFirstStageInfo{}, false, err - } - - if data == nil { - return catchpointFirstStageInfo{}, false, nil - } - - var res catchpointFirstStageInfo - err = protocol.Decode(data, &res) - if err != nil { - return catchpointFirstStageInfo{}, false, err - } - - return res, true, nil -} - -func selectOldCatchpointFirstStageInfoRounds(ctx context.Context, q db.Queryable, maxRound basics.Round) ([]basics.Round, error) { - var res []basics.Round - - f := func() error { - query := "SELECT round FROM catchpointfirststageinfo WHERE round <= ?" - rows, err := q.QueryContext(ctx, query, maxRound) - if err != nil { - return err - } - - // Clear `res` in case this function is repeated. - res = res[:0] - for rows.Next() { - var r basics.Round - err = rows.Scan(&r) - if err != nil { - return err - } - res = append(res, r) - } - - return nil - } - err := db.Retry(f) - if err != nil { - return nil, err - } - - return res, nil -} - -func deleteOldCatchpointFirstStageInfo(ctx context.Context, e db.Executable, maxRoundToDelete basics.Round) error { - f := func() error { - query := "DELETE FROM catchpointfirststageinfo WHERE round <= ?" - _, err := e.ExecContext(ctx, query, maxRoundToDelete) - return err - } - return db.Retry(f) -} - -func insertUnfinishedCatchpoint(ctx context.Context, e db.Executable, round basics.Round, blockHash crypto.Digest) error { - f := func() error { - query := "INSERT INTO unfinishedcatchpoints(round, blockhash) VALUES(?, ?)" - _, err := e.ExecContext(ctx, query, round, blockHash[:]) - return err - } - return db.Retry(f) -} - -type unfinishedCatchpointRecord struct { - round basics.Round - blockHash crypto.Digest -} - -func selectUnfinishedCatchpoints(ctx context.Context, q db.Queryable) ([]unfinishedCatchpointRecord, error) { - var res []unfinishedCatchpointRecord - - f := func() error { - query := "SELECT round, blockhash FROM unfinishedcatchpoints ORDER BY round" - rows, err := q.QueryContext(ctx, query) - if err != nil { - return err - } - - // Clear `res` in case this function is repeated. - res = res[:0] - for rows.Next() { - var record unfinishedCatchpointRecord - var blockHash []byte - err = rows.Scan(&record.round, &blockHash) - if err != nil { - return err - } - copy(record.blockHash[:], blockHash) - res = append(res, record) - } - - return nil - } - err := db.Retry(f) - if err != nil { - return nil, err - } - - return res, nil -} - -func deleteUnfinishedCatchpoint(ctx context.Context, e db.Executable, round basics.Round) error { - f := func() error { - query := "DELETE FROM unfinishedcatchpoints WHERE round = ?" - _, err := e.ExecContext(ctx, query, round) - return err - } - return db.Retry(f) -} diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index 781c7300c8..c290ed827c 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -21,13 +21,11 @@ import ( "context" "database/sql" "encoding/binary" - "encoding/json" "errors" "fmt" "math" "math/rand" "os" - "reflect" "sort" "strconv" "strings" @@ -41,10 +39,10 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" - "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/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" + storetesting "github.com/algorand/go-algorand/ledger/store/testing" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -52,62 +50,24 @@ import ( "github.com/algorand/go-algorand/util/db" ) -func accountsInitTest(tb testing.TB, tx *sql.Tx, initAccounts map[basics.Address]basics.AccountData, proto protocol.ConsensusVersion) (newDatabase bool) { - newDB, err := accountsInit(tx, initAccounts, config.Consensus[proto]) - require.NoError(tb, err) - - err = accountsAddNormalizedBalance(tx, config.Consensus[proto]) - require.NoError(tb, err) - - err = accountsCreateResourceTable(context.Background(), tx) - require.NoError(tb, err) - - err = performResourceTableMigration(context.Background(), tx, nil) - require.NoError(tb, err) - - err = accountsCreateOnlineAccountsTable(context.Background(), tx) - require.NoError(tb, err) - - err = accountsCreateTxTailTable(context.Background(), tx) - require.NoError(tb, err) - - err = performOnlineAccountsTableMigration(context.Background(), tx, nil, nil) - require.NoError(tb, err) - - // since this is a test that starts from genesis, there is no tail that needs to be migrated. - // we'll pass a nil here in order to ensure we still call this method, although it would - // be a noop. - err = performTxTailTableMigration(context.Background(), nil, db.Accessor{}) - require.NoError(tb, err) - - err = accountsCreateOnlineRoundParamsTable(context.Background(), tx) - require.NoError(tb, err) - - err = performOnlineRoundParamsTailMigration(context.Background(), tx, db.Accessor{}, true, proto) - require.NoError(tb, err) - - err = accountsCreateBoxTable(context.Background(), tx) - require.NoError(tb, err) - - return newDB -} - func checkAccounts(t *testing.T, tx *sql.Tx, rnd basics.Round, accts map[basics.Address]basics.AccountData) { - r, err := accountsRound(tx) + arw := store.NewAccountsSQLReaderWriter(tx) + + r, err := arw.AccountsRound() require.NoError(t, err) require.Equal(t, r, rnd) - aq, err := accountsInitDbQueries(tx) + aq, err := store.AccountsInitDbQueries(tx) require.NoError(t, err) - defer aq.close() + defer aq.Close() var totalOnline, totalOffline, totalNotPart uint64 for addr, data := range accts { expected := ledgercore.ToAccountData(data) - pad, err := aq.lookup(addr) + pad, err := aq.LookupAccount(addr) require.NoError(t, err) - d := pad.accountData.GetLedgerCoreAccountData() + d := pad.AccountData.GetLedgerCoreAccountData() require.Equal(t, expected, d) switch d.Status { @@ -126,7 +86,7 @@ func checkAccounts(t *testing.T, tx *sql.Tx, rnd basics.Round, accts map[basics. require.NoError(t, err) require.Equal(t, all, accts) - totals, err := accountsTotals(context.Background(), tx, false) + totals, err := arw.AccountsTotals(context.Background(), false) require.NoError(t, err) require.Equal(t, totalOnline, totals.Online.Money.Raw, "mismatching total online money") require.Equal(t, totalOffline, totals.Offline.Money.Raw) @@ -134,10 +94,10 @@ func checkAccounts(t *testing.T, tx *sql.Tx, rnd basics.Round, accts map[basics. require.Equal(t, totalOnline+totalOffline, totals.Participating().Raw) require.Equal(t, totalOnline+totalOffline+totalNotPart, totals.All().Raw) - d, err := aq.lookup(ledgertesting.RandomAddress()) + d, err := aq.LookupAccount(ledgertesting.RandomAddress()) require.NoError(t, err) - require.Equal(t, rnd, d.round) - require.Equal(t, d.accountData, baseAccountData{}) + require.Equal(t, rnd, d.Round) + require.Equal(t, d.AccountData, store.BaseAccountData{}) proto := config.Consensus[protocol.ConsensusCurrentVersion] @@ -168,7 +128,7 @@ func checkAccounts(t *testing.T, tx *sql.Tx, rnd basics.Round, accts map[basics. }) for i := 0; i < len(onlineAccounts); i++ { - dbtop, err := accountsOnlineTop(tx, rnd, 0, uint64(i), proto) + dbtop, err := arw.AccountsOnlineTop(rnd, 0, uint64(i), proto) require.NoError(t, err) require.Equal(t, i, len(dbtop)) @@ -178,7 +138,7 @@ func checkAccounts(t *testing.T, tx *sql.Tx, rnd basics.Round, accts map[basics. } } - top, err := accountsOnlineTop(tx, rnd, 0, uint64(len(onlineAccounts)+1), proto) + top, err := arw.AccountsOnlineTop(rnd, 0, uint64(len(onlineAccounts)+1), proto) require.NoError(t, err) require.Equal(t, len(top), len(onlineAccounts)) } @@ -188,8 +148,8 @@ func TestAccountDBInit(t *testing.T) { proto := config.Consensus[protocol.ConsensusCurrentVersion] - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() @@ -197,12 +157,12 @@ func TestAccountDBInit(t *testing.T) { defer tx.Rollback() accts := ledgertesting.RandomAccounts(20, true) - newDB := accountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) + newDB := store.AccountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) require.True(t, newDB) checkAccounts(t, tx, 0, accts) - newDB, err = accountsInit(tx, accts, proto) + newDB, err = store.AccountsInitLightTest(t, tx, accts, proto) require.NoError(t, err) require.False(t, newDB) checkAccounts(t, tx, 0, accts) @@ -249,20 +209,22 @@ func TestAccountDBRound(t *testing.T) { proto := config.Consensus[protocol.ConsensusCurrentVersion] - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() require.NoError(t, err) defer tx.Rollback() + arw := store.NewAccountsSQLReaderWriter(tx) + accts := ledgertesting.RandomAccounts(20, true) - accountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) + store.AccountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) checkAccounts(t, tx, 0, accts) - totals, err := accountsTotals(context.Background(), tx, false) + totals, err := arw.AccountsTotals(context.Background(), false) require.NoError(t, err) - expectedOnlineRoundParams, endRound, err := accountsOnlineRoundParams(tx) + expectedOnlineRoundParams, endRound, err := arw.AccountsOnlineRoundParams() require.NoError(t, err) require.Equal(t, 1, len(expectedOnlineRoundParams)) require.Equal(t, 0, int(endRound)) @@ -290,8 +252,8 @@ func TestAccountDBRound(t *testing.T) { expectedDbImage, numElementsPerSegment) oldBase := i - 1 - updatesCnt := makeCompactAccountDeltas([]ledgercore.AccountDeltas{updates}, basics.Round(oldBase), true, baseAccounts) - resourceUpdatesCnt := makeCompactResourceDeltas([]ledgercore.AccountDeltas{updates}, basics.Round(oldBase), true, baseAccounts, baseResources) + updatesCnt := makeCompactAccountDeltas([]ledgercore.StateDelta{{Accts: updates}}, basics.Round(oldBase), true, baseAccounts) + resourceUpdatesCnt := makeCompactResourceDeltas([]ledgercore.StateDelta{{Accts: updates}}, basics.Round(oldBase), true, baseAccounts, baseResources) updatesOnlineCnt := makeCompactOnlineAccountDeltas([]ledgercore.AccountDeltas{updates}, basics.Round(oldBase), baseOnlineAccounts) err = updatesCnt.accountsLoadOld(tx) @@ -302,16 +264,16 @@ func TestAccountDBRound(t *testing.T) { knownAddresses := make(map[basics.Address]int64) for _, delta := range updatesCnt.deltas { - knownAddresses[delta.oldAcct.addr] = delta.oldAcct.rowid + knownAddresses[delta.oldAcct.Addr] = delta.oldAcct.Rowid } err = resourceUpdatesCnt.resourcesLoadOld(tx, knownAddresses) require.NoError(t, err) - err = accountsPutTotals(tx, totals, false) + err = arw.AccountsPutTotals(totals, false) require.NoError(t, err) onlineRoundParams := ledgercore.OnlineRoundParamsData{RewardsLevel: totals.RewardsLevel, OnlineSupply: totals.Online.Money.Raw, CurrentProtocol: protocol.ConsensusCurrentVersion} - err = accountsPutOnlineRoundParams(tx, []ledgercore.OnlineRoundParamsData{onlineRoundParams}, basics.Round(i)) + err = arw.AccountsPutOnlineRoundParams([]ledgercore.OnlineRoundParamsData{onlineRoundParams}, basics.Round(i)) require.NoError(t, err) expectedOnlineRoundParams = append(expectedOnlineRoundParams, onlineRoundParams) @@ -328,7 +290,7 @@ func TestAccountDBRound(t *testing.T) { updatedOnlineAccts, err := onlineAccountsNewRound(tx, updatesOnlineCnt, proto, basics.Round(i)) require.NoError(t, err) - err = updateAccountsRound(tx, basics.Round(i)) + err = arw.UpdateAccountsRound(basics.Round(i)) require.NoError(t, err) // TODO: calculate exact number of updates? @@ -346,11 +308,11 @@ func TestAccountDBRound(t *testing.T) { } expectedTotals := ledgertesting.CalculateNewRoundAccountTotals(t, updates, 0, proto, nil, ledgercore.AccountTotals{}) - actualTotals, err := accountsTotals(context.Background(), tx, false) + actualTotals, err := arw.AccountsTotals(context.Background(), false) require.NoError(t, err) require.Equal(t, expectedTotals, actualTotals) - actualOnlineRoundParams, endRound, err := accountsOnlineRoundParams(tx) + actualOnlineRoundParams, endRound, err := arw.AccountsOnlineRoundParams() require.NoError(t, err) require.Equal(t, expectedOnlineRoundParams, actualOnlineRoundParams) require.Equal(t, 9, int(endRound)) @@ -360,7 +322,7 @@ func TestAccountDBRound(t *testing.T) { acctCb := func(addr basics.Address, data basics.AccountData) { loaded[addr] = data } - count, err := LoadAllFullAccounts(context.Background(), tx, "accountbase", "resources", acctCb) + count, err := arw.LoadAllFullAccounts(context.Background(), "accountbase", "resources", acctCb) require.NoError(t, err) require.Equal(t, count, len(accts)) require.Equal(t, count, len(loaded)) @@ -373,38 +335,38 @@ func TestAccountDBInMemoryAcct(t *testing.T) { partitiontest.PartitionTest(t) proto := config.Consensus[protocol.ConsensusCurrentVersion] - type testfunc func(basics.Address) ([]ledgercore.AccountDeltas, int, int) + type testfunc func(basics.Address) ([]ledgercore.StateDelta, int, int) var tests = []testfunc{ - func(addr basics.Address) ([]ledgercore.AccountDeltas, int, int) { + func(addr basics.Address) ([]ledgercore.StateDelta, int, int) { const numRounds = 4 - accountDeltas := make([]ledgercore.AccountDeltas, numRounds) - accountDeltas[0].Upsert(addr, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}}}) - accountDeltas[0].UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 0}}) + stateDeltas := make([]ledgercore.StateDelta, numRounds) + stateDeltas[0].Accts.Upsert(addr, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}}}) + stateDeltas[0].Accts.UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 0}}) // transfer some asset - accountDeltas[1].UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 100}}) + stateDeltas[1].Accts.UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 100}}) // close out the asset - accountDeltas[2].UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Deleted: true}) + stateDeltas[2].Accts.UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Deleted: true}) // close the account - accountDeltas[3].Upsert(addr, ledgercore.AccountData{}) - return accountDeltas, 2, 3 + stateDeltas[3].Accts.Upsert(addr, ledgercore.AccountData{}) + return stateDeltas, 2, 3 }, - func(addr basics.Address) ([]ledgercore.AccountDeltas, int, int) { + func(addr basics.Address) ([]ledgercore.StateDelta, int, int) { const numRounds = 4 - accountDeltas := make([]ledgercore.AccountDeltas, numRounds) - accountDeltas[0].Upsert(addr, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}}}) - accountDeltas[1].UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 0}}) + stateDeltas := make([]ledgercore.StateDelta, numRounds) + stateDeltas[0].Accts.Upsert(addr, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}}}) + stateDeltas[1].Accts.UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 0}}) // close out the asset - accountDeltas[2].UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Deleted: true}) + stateDeltas[2].Accts.UpsertAssetResource(addr, 100, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Deleted: true}) // close the account - accountDeltas[3].Upsert(addr, ledgercore.AccountData{}) - return accountDeltas, 2, 2 + stateDeltas[3].Accts.Upsert(addr, ledgercore.AccountData{}) + return stateDeltas, 2, 2 }, } for i, test := range tests { - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() @@ -412,7 +374,7 @@ func TestAccountDBInMemoryAcct(t *testing.T) { defer tx.Rollback() accts := ledgertesting.RandomAccounts(1, true) - accountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) + store.AccountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) addr := ledgertesting.RandomAddress() // lastCreatableID stores asset or app max used index to get rid of conflicts @@ -423,19 +385,19 @@ func TestAccountDBInMemoryAcct(t *testing.T) { t.Run(fmt.Sprintf("test%d", i), func(t *testing.T) { - accountDeltas, numAcctDeltas, numResDeltas := test(addr) - lastRound := uint64(len(accountDeltas) + 1) + stateDeltas, numAcctDeltas, numResDeltas := test(addr) + lastRound := uint64(len(stateDeltas) + 1) - outAccountDeltas := makeCompactAccountDeltas(accountDeltas, basics.Round(1), true, baseAccounts) + outAccountDeltas := makeCompactAccountDeltas(stateDeltas, basics.Round(1), true, baseAccounts) require.Equal(t, 1, len(outAccountDeltas.deltas)) - require.Equal(t, accountDelta{newAcct: baseAccountData{UpdateRound: lastRound}, nAcctDeltas: numAcctDeltas, address: addr}, outAccountDeltas.deltas[0]) + require.Equal(t, accountDelta{newAcct: store.BaseAccountData{UpdateRound: lastRound}, nAcctDeltas: numAcctDeltas, address: addr}, outAccountDeltas.deltas[0]) require.Equal(t, 1, len(outAccountDeltas.misses)) - outResourcesDeltas := makeCompactResourceDeltas(accountDeltas, basics.Round(1), true, baseAccounts, baseResources) + outResourcesDeltas := makeCompactResourceDeltas(stateDeltas, basics.Round(1), true, baseAccounts, baseResources) require.Equal(t, 1, len(outResourcesDeltas.deltas)) require.Equal(t, resourceDelta{ - oldResource: persistedResourcesData{aidx: 100}, newResource: makeResourcesData(lastRound - 1), + oldResource: store.PersistedResourcesData{Aidx: 100}, newResource: store.MakeResourcesData(lastRound - 1), nAcctDeltas: numResDeltas, address: addr, }, outResourcesDeltas.deltas[0], @@ -447,7 +409,7 @@ func TestAccountDBInMemoryAcct(t *testing.T) { knownAddresses := make(map[basics.Address]int64) for _, delta := range outAccountDeltas.deltas { - knownAddresses[delta.oldAcct.addr] = delta.oldAcct.rowid + knownAddresses[delta.oldAcct.Addr] = delta.oldAcct.Rowid } err = outResourcesDeltas.resourcesLoadOld(tx, knownAddresses) @@ -457,13 +419,13 @@ func TestAccountDBInMemoryAcct(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(updatedAccts)) // we store empty even for deleted accounts require.Equal(t, - persistedAccountData{addr: addr, round: basics.Round(lastRound)}, + store.PersistedAccountData{Addr: addr, Round: basics.Round(lastRound)}, updatedAccts[0], ) require.Equal(t, 1, len(updatesResources[addr])) // we store empty even for deleted resources require.Equal(t, - persistedResourcesData{addrid: 0, aidx: 100, data: makeResourcesData(0), round: basics.Round(lastRound)}, + store.PersistedResourcesData{Addrid: 0, Aidx: 100, Data: store.MakeResourcesData(0), Round: basics.Round(lastRound)}, updatesResources[addr][0], ) @@ -475,8 +437,8 @@ func TestAccountDBInMemoryAcct(t *testing.T) { func TestAccountStorageWithStateProofID(t *testing.T) { partitiontest.PartitionTest(t) - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() @@ -484,7 +446,7 @@ func TestAccountStorageWithStateProofID(t *testing.T) { defer tx.Rollback() accts := ledgertesting.RandomAccounts(20, false) - _ = accountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) + _ = store.AccountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) checkAccounts(t, tx, 0, accts) require.True(t, allAccountsHaveStateProofPKs(accts)) } @@ -675,7 +637,7 @@ func benchmarkInitBalances(b testing.TB, numAccounts int, dbs db.Pair, proto pro updates = generateRandomTestingAccountBalances(numAccounts) - accountsInitTest(b, tx, updates, proto) + store.AccountsInitTest(b, tx, updates, proto) err = tx.Commit() require.NoError(b, err) return @@ -689,8 +651,8 @@ func cleanupTestDb(dbs db.Pair, dbName string, inMemory bool) { } func benchmarkReadingAllBalances(b *testing.B, inMemory bool) { - dbs, fn := dbOpenTest(b, inMemory) - setDbLogging(b, dbs) + dbs, fn := storetesting.DbOpenTest(b, inMemory) + storetesting.SetDbLogging(b, dbs) defer cleanupTestDb(dbs, fn, inMemory) benchmarkInitBalances(b, b.N, dbs, protocol.ConsensusCurrentVersion) @@ -720,15 +682,15 @@ func BenchmarkReadingAllBalancesDisk(b *testing.B) { } func benchmarkReadingRandomBalances(b *testing.B, inMemory bool) { - dbs, fn := dbOpenTest(b, inMemory) - setDbLogging(b, dbs) + dbs, fn := storetesting.DbOpenTest(b, inMemory) + storetesting.SetDbLogging(b, dbs) defer cleanupTestDb(dbs, fn, inMemory) accounts := benchmarkInitBalances(b, b.N, dbs, protocol.ConsensusCurrentVersion) - qs, err := accountsInitDbQueries(dbs.Rdb.Handle) + qs, err := store.AccountsInitDbQueries(dbs.Rdb.Handle) require.NoError(b, err) - defer qs.close() + defer qs.Close() // read all the balances in the database, shuffled addrs := make([]basics.Address, len(accounts)) @@ -742,7 +704,7 @@ func benchmarkReadingRandomBalances(b *testing.B, inMemory bool) { // only measure the actual fetch time b.ResetTimer() for _, addr := range addrs { - _, err = qs.lookup(addr) + _, err = qs.LookupAccount(addr) require.NoError(b, err) } } @@ -759,8 +721,8 @@ func BenchmarkWritingRandomBalancesDisk(b *testing.B) { batchCount := 1000 startupAcct := 5 initDatabase := func() (*sql.Tx, func(), error) { - dbs, fn := dbOpenTest(b, false) - setDbLogging(b, dbs) + dbs, fn := storetesting.DbOpenTest(b, false) + storetesting.SetDbLogging(b, dbs) cleanup := func() { cleanupTestDb(dbs, fn, false) } @@ -877,114 +839,29 @@ func BenchmarkWritingRandomBalancesDisk(b *testing.B) { err = tx.Commit() require.NoError(b, err) } -func TestAccountsReencoding(t *testing.T) { - partitiontest.PartitionTest(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}) - var stateProofID merklesignature.Verifier - crypto.RandBytes(stateProofID.Commitment[:]) - - err := dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - accountsInitTest(t, tx, make(map[basics.Address]basics.AccountData), protocol.ConsensusCurrentVersion) - - for _, oldAccData := range oldEncodedAccountsData { - addr := ledgertesting.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 := ledgertesting.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, - StateProofID: stateProofID.Commitment, - VoteFirstValid: basics.Round(0x000ffffffffffffff), - VoteLastValid: basics.Round(0x000ffffffffffffff), - VoteKeyDilution: 0x000ffffffffffffff, - AssetParams: map[basics.AssetIndex]basics.AssetParams{ - 0x000ffffffffffffff: { - 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: { - 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) { partitiontest.PartitionTest(t) - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() err := dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - accountsInitTest(t, tx, make(map[basics.Address]basics.AccountData), protocol.ConsensusCurrentVersion) + store.AccountsInitTest(t, tx, make(map[basics.Address]basics.AccountData), protocol.ConsensusCurrentVersion) return nil }) require.NoError(t, err) - qs, err := accountsInitDbQueries(dbs.Rdb.Handle) + qs, err := store.AccountsInitDbQueries(dbs.Rdb.Handle) require.NoError(t, err) - require.NotNil(t, qs.listCreatablesStmt) - qs.close() - require.Nil(t, qs.listCreatablesStmt) - qs.close() - require.Nil(t, qs.listCreatablesStmt) + // TODO[store-refactor]: internals are opaque, once we move the the remainder of accountdb we can mvoe this too + // require.NotNil(t, qs.listCreatablesStmt) + qs.Close() + // require.Nil(t, qs.listCreatablesStmt) + qs.Close() + // require.Nil(t, qs.listCreatablesStmt) } func benchmarkWriteCatchpointStagingBalancesSub(b *testing.B, ascendingOrder bool) { @@ -1031,7 +908,7 @@ func benchmarkWriteCatchpointStagingBalancesSub(b *testing.B, ascendingOrder boo chunk.Balances = make([]encodedBalanceRecordV6, chunkSize) for i := uint64(0); i < chunkSize; i++ { var randomAccount encodedBalanceRecordV6 - accountData := baseAccountData{RewardsBase: accountsLoaded + i} + accountData := store.BaseAccountData{RewardsBase: accountsLoaded + i} accountData.MicroAlgos.Raw = crypto.RandUint63() randomAccount.AccountData = protocol.Encode(&accountData) crypto.RandBytes(randomAccount.Address[:]) @@ -1048,7 +925,8 @@ func benchmarkWriteCatchpointStagingBalancesSub(b *testing.B, ascendingOrder boo require.NoError(b, err) b.StartTimer() err = l.trackerDBs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - err = writeCatchpointStagingBalances(ctx, tx, normalizedAccountBalances) + crw := store.NewCatchpointSQLReaderWriter(tx) + err = crw.WriteCatchpointStagingBalances(ctx, normalizedAccountBalances) return }) @@ -1081,51 +959,20 @@ func BenchmarkWriteCatchpointStagingBalances(b *testing.B) { } } -func TestKeyPrefixIntervalPreprocessing(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - testCases := []struct { - input []byte - outputPrefix []byte - outputPrefixIncr []byte - }{ - {input: []byte{0xAB, 0xCD}, outputPrefix: []byte{0xAB, 0xCD}, outputPrefixIncr: []byte{0xAB, 0xCE}}, - {input: []byte{0xFF}, outputPrefix: []byte{0xFF}, outputPrefixIncr: nil}, - {input: []byte{0xFE, 0xFF}, outputPrefix: []byte{0xFE, 0xFF}, outputPrefixIncr: []byte{0xFF}}, - {input: []byte{0xFF, 0xFF}, outputPrefix: []byte{0xFF, 0xFF}, outputPrefixIncr: nil}, - {input: []byte{0xAB, 0xCD}, outputPrefix: []byte{0xAB, 0xCD}, outputPrefixIncr: []byte{0xAB, 0xCE}}, - {input: []byte{0x1E, 0xFF, 0xFF}, outputPrefix: []byte{0x1E, 0xFF, 0xFF}, outputPrefixIncr: []byte{0x1F}}, - {input: []byte{0xFF, 0xFE, 0xFF, 0xFF}, outputPrefix: []byte{0xFF, 0xFE, 0xFF, 0xFF}, outputPrefixIncr: []byte{0xFF, 0xFF}}, - {input: []byte{0x00, 0xFF}, outputPrefix: []byte{0x00, 0xFF}, outputPrefixIncr: []byte{0x01}}, - {input: []byte(string("bx:123")), outputPrefix: []byte(string("bx:123")), outputPrefixIncr: []byte(string("bx:124"))}, - {input: []byte{}, outputPrefix: []byte{}, outputPrefixIncr: nil}, - {input: nil, outputPrefix: []byte{}, outputPrefixIncr: nil}, - {input: []byte{0x1E, 0xFF, 0xFF}, outputPrefix: []byte{0x1E, 0xFF, 0xFF}, outputPrefixIncr: []byte{0x1F}}, - {input: []byte{0xFF, 0xFE, 0xFF, 0xFF}, outputPrefix: []byte{0xFF, 0xFE, 0xFF, 0xFF}, outputPrefixIncr: []byte{0xFF, 0xFF}}, - {input: []byte{0x00, 0xFF}, outputPrefix: []byte{0x00, 0xFF}, outputPrefixIncr: []byte{0x01}}, - } - for _, tc := range testCases { - actualOutputPrefix, actualOutputPrefixIncr := keyPrefixIntervalPreprocessing(tc.input) - require.Equal(t, tc.outputPrefix, actualOutputPrefix) - require.Equal(t, tc.outputPrefixIncr, actualOutputPrefixIncr) - } -} - func TestLookupKeysByPrefix(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - dbs, fn := dbOpenTest(t, false) - setDbLogging(t, dbs) + dbs, fn := storetesting.DbOpenTest(t, false) + storetesting.SetDbLogging(t, dbs) defer cleanupTestDb(dbs, fn, false) - // return account data, initialize DB tables from accountsInitTest + // return account data, initialize DB tables from AccountsInitTest _ = benchmarkInitBalances(t, 1, dbs, protocol.ConsensusCurrentVersion) - qs, err := accountsInitDbQueries(dbs.Rdb.Handle) + qs, err := store.AccountsInitDbQueries(dbs.Rdb.Handle) require.NoError(t, err) - defer qs.close() + defer qs.Close() kvPairDBPrepareSet := []struct { key []byte @@ -1154,19 +1001,19 @@ func TestLookupKeysByPrefix(t *testing.T) { require.NoError(t, err) // writer is only for kvstore - writer, err := makeAccountsSQLWriter(tx, true, true, true, true) + writer, err := store.MakeAccountsSQLWriter(tx, true, true, true, true) if err != nil { return } for i := 0; i < len(kvPairDBPrepareSet); i++ { - err := writer.upsertKvPair(string(kvPairDBPrepareSet[i].key), kvPairDBPrepareSet[i].value) + err := writer.UpsertKvPair(string(kvPairDBPrepareSet[i].key), kvPairDBPrepareSet[i].value) require.NoError(t, err) } err = tx.Commit() require.NoError(t, err) - writer.close() + writer.Close() testCases := []struct { prefix []byte @@ -1278,7 +1125,7 @@ func TestLookupKeysByPrefix(t *testing.T) { for index, testCase := range testCases { t.Run("lookupKVByPrefix-testcase-"+strconv.Itoa(index), func(t *testing.T) { actual := make(map[string]bool) - _, err := qs.lookupKeysByPrefix(string(testCase.prefix), uint64(len(kvPairDBPrepareSet)), actual, 0) + _, err := qs.LookupKeysByPrefix(string(testCase.prefix), uint64(len(kvPairDBPrepareSet)), actual, 0) if err != nil { require.NotEmpty(t, testCase.err, testCase.prefix) require.Contains(t, err.Error(), testCase.err) @@ -1297,16 +1144,16 @@ func TestLookupKeysByPrefix(t *testing.T) { func BenchmarkLookupKeyByPrefix(b *testing.B) { // learn something from BenchmarkWritingRandomBalancesDisk - dbs, fn := dbOpenTest(b, false) - setDbLogging(b, dbs) + dbs, fn := storetesting.DbOpenTest(b, false) + storetesting.SetDbLogging(b, dbs) defer cleanupTestDb(dbs, fn, false) - // return account data, initialize DB tables from accountsInitTest + // return account data, initialize DB tables from AccountsInitTest _ = benchmarkInitBalances(b, 1, dbs, protocol.ConsensusCurrentVersion) - qs, err := accountsInitDbQueries(dbs.Rdb.Handle) + qs, err := store.AccountsInitDbQueries(dbs.Rdb.Handle) require.NoError(b, err) - defer qs.close() + defer qs.Close() currentDBSize := 0 nextDBSize := 2 @@ -1322,7 +1169,7 @@ func BenchmarkLookupKeyByPrefix(b *testing.B) { require.NoError(b, err) // writer is only for kvstore - writer, err := makeAccountsSQLWriter(tx, true, true, true, true) + writer, err := store.MakeAccountsSQLWriter(tx, true, true, true, true) if err != nil { return } @@ -1334,7 +1181,7 @@ func BenchmarkLookupKeyByPrefix(b *testing.B) { crypto.RandBytes(valueBuffer) appID := basics.AppIndex(crypto.RandUint64()) boxKey := logic.MakeBoxKey(appID, string(nameBuffer)) - err = writer.upsertKvPair(boxKey, valueBuffer) + err = writer.UpsertKvPair(boxKey, valueBuffer) require.NoError(b, err) if i == 0 { @@ -1343,7 +1190,7 @@ func BenchmarkLookupKeyByPrefix(b *testing.B) { } err = tx.Commit() require.NoError(b, err) - writer.close() + writer.Close() // benchmark the query against large DB, see if we have O(log N) speed currentDBSize = nextDBSize @@ -1352,7 +1199,7 @@ func BenchmarkLookupKeyByPrefix(b *testing.B) { b.Run("lookupKVByPrefix-DBsize"+strconv.Itoa(currentDBSize), func(b *testing.B) { for i := 0; i < b.N; i++ { results := make(map[string]bool) - _, err := qs.lookupKeysByPrefix(prefix, uint64(currentDBSize), results, 0) + _, err := qs.LookupKeysByPrefix(prefix, uint64(currentDBSize), results, 0) require.NoError(b, err) require.True(b, len(results) >= 1) } @@ -1362,7 +1209,7 @@ func BenchmarkLookupKeyByPrefix(b *testing.B) { // upsert updates existing or inserts a new entry func (a *compactResourcesDeltas) upsert(delta resourceDelta) { - if idx, exist := a.cache[accountCreatable{address: delta.address, index: delta.oldResource.aidx}]; exist { + if idx, exist := a.cache[accountCreatable{address: delta.address, index: delta.oldResource.Aidx}]; exist { a.deltas[idx] = delta return } @@ -1370,13 +1217,13 @@ func (a *compactResourcesDeltas) upsert(delta resourceDelta) { } // upsertOld updates existing or inserts a new partial entry with only old field filled -func (a *compactAccountDeltas) upsertOld(old persistedAccountData) { - addr := old.addr +func (a *compactAccountDeltas) upsertOld(old store.PersistedAccountData) { + addr := old.Addr if idx, exist := a.cache[addr]; exist { a.deltas[idx].oldAcct = old return } - a.insert(accountDelta{oldAcct: old, address: old.addr}) + a.insert(accountDelta{oldAcct: old, address: old.Addr}) } // upsert updates existing or inserts a new entry @@ -1405,7 +1252,7 @@ func TestCompactAccountDeltas(t *testing.T) { a.Zero(ad.len()) a.Panics(func() { ad.getByIdx(0) }) - sample1 := accountDelta{newAcct: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 123}}, address: addr} + sample1 := accountDelta{newAcct: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 123}}, address: addr} ad.upsert(addr, sample1) data, idx = ad.get(addr) a.NotEqual(-1, idx) @@ -1416,7 +1263,7 @@ func TestCompactAccountDeltas(t *testing.T) { a.Equal(addr, data.address) a.Equal(sample1, data) - sample2 := accountDelta{newAcct: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 456}}, address: addr} + sample2 := accountDelta{newAcct: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 456}}, address: addr} ad.upsert(addr, sample2) data, idx = ad.get(addr) a.NotEqual(-1, idx) @@ -1437,7 +1284,7 @@ func TestCompactAccountDeltas(t *testing.T) { a.Equal(addr, data.address) a.Equal(sample2, data) - old1 := persistedAccountData{addr: addr, accountData: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 789}}} + old1 := store.PersistedAccountData{Addr: addr, AccountData: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 789}}} ad.upsertOld(old1) a.Equal(1, ad.len()) data = ad.getByIdx(0) @@ -1445,7 +1292,7 @@ func TestCompactAccountDeltas(t *testing.T) { a.Equal(accountDelta{newAcct: sample2.newAcct, oldAcct: old1, address: addr}, data) addr1 := ledgertesting.RandomAddress() - old2 := persistedAccountData{addr: addr1, accountData: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 789}}} + old2 := store.PersistedAccountData{Addr: addr1, AccountData: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 789}}} ad.upsertOld(old2) a.Equal(2, ad.len()) data = ad.getByIdx(0) @@ -1453,7 +1300,7 @@ func TestCompactAccountDeltas(t *testing.T) { a.Equal(accountDelta{newAcct: sample2.newAcct, oldAcct: old1, address: addr}, data) data = ad.getByIdx(1) - a.Equal(addr1, data.oldAcct.addr) + a.Equal(addr1, data.oldAcct.Addr) a.Equal(accountDelta{oldAcct: old2, address: addr1}, data) // apply old on empty delta object, expect no changes @@ -1474,14 +1321,15 @@ func TestCompactAccountDeltas(t *testing.T) { } // upsertOld updates existing or inserts a new partial entry with only old field filled -func (a *compactResourcesDeltas) upsertOld(addr basics.Address, old persistedResourcesData) { - if idx, exist := a.cache[accountCreatable{address: addr, index: old.aidx}]; exist { +func (a *compactResourcesDeltas) upsertOld(addr basics.Address, old store.PersistedResourcesData) { + if idx, exist := a.cache[accountCreatable{address: addr, index: old.Aidx}]; exist { a.deltas[idx].oldResource = old return } idx := a.insert(resourceDelta{oldResource: old, address: addr}) a.deltas[idx].address = addr } + func TestCompactResourceDeltas(t *testing.T) { partitiontest.PartitionTest(t) @@ -1500,7 +1348,7 @@ func TestCompactResourceDeltas(t *testing.T) { a.Zero(ad.len()) a.Panics(func() { ad.getByIdx(0) }) - sample1 := resourceDelta{newResource: resourcesData{Total: 123}, address: addr, oldResource: persistedResourcesData{aidx: 1}} + sample1 := resourceDelta{newResource: store.ResourcesData{Total: 123}, address: addr, oldResource: store.PersistedResourcesData{Aidx: 1}} ad.upsert(sample1) data, idx = ad.get(addr, 1) a.NotEqual(-1, idx) @@ -1511,7 +1359,7 @@ func TestCompactResourceDeltas(t *testing.T) { a.Equal(addr, data.address) a.Equal(sample1, data) - sample2 := resourceDelta{newResource: resourcesData{Total: 456}, address: addr, oldResource: persistedResourcesData{aidx: 1}} + sample2 := resourceDelta{newResource: store.ResourcesData{Total: 456}, address: addr, oldResource: store.PersistedResourcesData{Aidx: 1}} ad.upsert(sample2) data, idx = ad.get(addr, 1) a.NotEqual(-1, idx) @@ -1532,7 +1380,7 @@ func TestCompactResourceDeltas(t *testing.T) { a.Equal(addr, data.address) a.Equal(sample2, data) - old1 := persistedResourcesData{addrid: 111, aidx: 1, data: resourcesData{Total: 789}} + old1 := store.PersistedResourcesData{Addrid: 111, Aidx: 1, Data: store.ResourcesData{Total: 789}} ad.upsertOld(addr, old1) a.Equal(1, ad.len()) data = ad.getByIdx(0) @@ -1540,7 +1388,7 @@ func TestCompactResourceDeltas(t *testing.T) { a.Equal(resourceDelta{newResource: sample2.newResource, oldResource: old1, address: addr}, data) addr1 := ledgertesting.RandomAddress() - old2 := persistedResourcesData{addrid: 222, aidx: 2, data: resourcesData{Total: 789}} + old2 := store.PersistedResourcesData{Addrid: 222, Aidx: 2, Data: store.ResourcesData{Total: 789}} ad.upsertOld(addr1, old2) a.Equal(2, ad.len()) data = ad.getByIdx(0) @@ -1558,7 +1406,7 @@ func TestCompactResourceDeltas(t *testing.T) { a.Equal(resourceDelta{newResource: sample2.newResource, oldResource: old2, address: addr}, data) addr2 := ledgertesting.RandomAddress() - sample2.oldResource.aidx = 2 + sample2.oldResource.Aidx = 2 sample2.address = addr2 idx = ad.insert(sample2) a.Equal(3, ad.len()) @@ -1571,1231 +1419,11 @@ func TestCompactResourceDeltas(t *testing.T) { a.Equal(sample2, data) } -func TestResourcesDataApp(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - a := require.New(t) - - rd := resourcesData{} - a.False(rd.IsApp()) - a.True(rd.IsEmpty()) - - rd = makeResourcesData(1) - a.False(rd.IsApp()) - a.False(rd.IsHolding()) - a.False(rd.IsOwning()) - a.True(rd.IsEmpty()) - - // check empty - appParamsEmpty := basics.AppParams{} - rd = resourcesData{} - rd.SetAppParams(appParamsEmpty, false) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.True(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - a.Equal(appParamsEmpty, rd.GetAppParams()) - - appLocalEmpty := basics.AppLocalState{} - rd = resourcesData{} - rd.SetAppLocalState(appLocalEmpty) - a.True(rd.IsApp()) - a.True(rd.IsHolding()) - a.True(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - a.Equal(appLocalEmpty, rd.GetAppLocalState()) - - // check both empty - rd = resourcesData{} - rd.SetAppLocalState(appLocalEmpty) - rd.SetAppParams(appParamsEmpty, true) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.True(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - a.Equal(appParamsEmpty, rd.GetAppParams()) - a.Equal(appLocalEmpty, rd.GetAppLocalState()) - - // Since some steps use randomly generated input, the test is run N times - // to cover a larger search space of inputs. - for i := 0; i < 1000; i++ { - // check empty states + non-empty params - appParams := ledgertesting.RandomAppParams() - rd = resourcesData{} - rd.SetAppLocalState(appLocalEmpty) - rd.SetAppParams(appParams, true) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - a.Equal(appParams, rd.GetAppParams()) - a.Equal(appLocalEmpty, rd.GetAppLocalState()) - - appState := ledgertesting.RandomAppLocalState() - rd.SetAppLocalState(appState) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - a.Equal(appParams, rd.GetAppParams()) - a.Equal(appState, rd.GetAppLocalState()) - - // check ClearAppLocalState - rd.ClearAppLocalState() - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.False(rd.IsHolding()) - a.False(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - a.Equal(appParams, rd.GetAppParams()) - a.Equal(appLocalEmpty, rd.GetAppLocalState()) - - // check ClearAppParams - rd.SetAppLocalState(appState) - rd.ClearAppParams() - a.True(rd.IsApp()) - a.False(rd.IsOwning()) - a.True(rd.IsHolding()) - if appState.Schema.NumEntries() == 0 { - a.True(rd.IsEmptyAppFields()) - } else { - a.False(rd.IsEmptyAppFields()) - } - a.False(rd.IsEmpty()) - a.Equal(appParamsEmpty, rd.GetAppParams()) - a.Equal(appState, rd.GetAppLocalState()) - - // check both clear - rd.ClearAppLocalState() - a.False(rd.IsApp()) - a.False(rd.IsOwning()) - a.False(rd.IsHolding()) - a.True(rd.IsEmptyAppFields()) - a.True(rd.IsEmpty()) - a.Equal(appParamsEmpty, rd.GetAppParams()) - a.Equal(appLocalEmpty, rd.GetAppLocalState()) - - // check params clear when non-empty params and empty holding - rd = resourcesData{} - rd.SetAppLocalState(appLocalEmpty) - rd.SetAppParams(appParams, true) - rd.ClearAppParams() - a.True(rd.IsApp()) - a.False(rd.IsOwning()) - a.True(rd.IsHolding()) - a.True(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - a.Equal(appParamsEmpty, rd.GetAppParams()) - a.Equal(appLocalEmpty, rd.GetAppLocalState()) - - rd = resourcesData{} - rd.SetAppLocalState(appLocalEmpty) - a.True(rd.IsEmptyAppFields()) - a.True(rd.IsApp()) - a.False(rd.IsEmpty()) - a.Equal(rd.ResourceFlags, resourceFlagsEmptyApp) - rd.ClearAppLocalState() - a.False(rd.IsApp()) - a.True(rd.IsEmptyAppFields()) - a.True(rd.IsEmpty()) - a.Equal(rd.ResourceFlags, resourceFlagsNotHolding) - - // check migration flow (accountDataResources) - // 1. both exist and empty - rd = makeResourcesData(0) - rd.SetAppLocalState(appLocalEmpty) - rd.SetAppParams(appParamsEmpty, true) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.True(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - - // 2. both exist and not empty - rd = makeResourcesData(0) - rd.SetAppLocalState(appState) - rd.SetAppParams(appParams, true) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - - // 3. both exist: holding not empty, param is empty - rd = makeResourcesData(0) - rd.SetAppLocalState(appState) - rd.SetAppParams(appParamsEmpty, true) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - if appState.Schema.NumEntries() == 0 { - a.True(rd.IsEmptyAppFields()) - } else { - a.False(rd.IsEmptyAppFields()) - } - a.False(rd.IsEmpty()) - - // 4. both exist: holding empty, param is not empty - rd = makeResourcesData(0) - rd.SetAppLocalState(appLocalEmpty) - rd.SetAppParams(appParams, true) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - - // 5. holding does not exist and params is empty - rd = makeResourcesData(0) - rd.SetAppParams(appParamsEmpty, false) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.False(rd.IsHolding()) - a.True(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - - // 6. holding does not exist and params is not empty - rd = makeResourcesData(0) - rd.SetAppParams(appParams, false) - a.True(rd.IsApp()) - a.True(rd.IsOwning()) - a.False(rd.IsHolding()) - a.False(rd.IsEmptyAppFields()) - a.False(rd.IsEmpty()) - - // 7. holding exist and not empty and params does not exist - rd = makeResourcesData(0) - rd.SetAppLocalState(appState) - a.True(rd.IsApp()) - a.False(rd.IsOwning()) - a.True(rd.IsHolding()) - if appState.Schema.NumEntries() == 0 { - a.True(rd.IsEmptyAppFields()) - } else { - a.False(rd.IsEmptyAppFields()) - } - a.False(rd.IsEmpty()) - - // 8. both do not exist - rd = makeResourcesData(0) - a.False(rd.IsApp()) - a.False(rd.IsOwning()) - a.False(rd.IsHolding()) - a.True(rd.IsEmptyAppFields()) - a.True(rd.IsEmpty()) - } -} - -func TestResourcesDataAsset(t *testing.T) { - partitiontest.PartitionTest(t) - - a := require.New(t) - - rd := resourcesData{} - a.False(rd.IsAsset()) - a.True(rd.IsEmpty()) - - rd = makeResourcesData(1) - a.False(rd.IsAsset()) - a.False(rd.IsHolding()) - a.False(rd.IsOwning()) - a.True(rd.IsEmpty()) - - // check empty - assetParamsEmpty := basics.AssetParams{} - rd = resourcesData{} - rd.SetAssetParams(assetParamsEmpty, false) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.True(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - a.Equal(assetParamsEmpty, rd.GetAssetParams()) - - assetHoldingEmpty := basics.AssetHolding{} - rd = resourcesData{} - rd.SetAssetHolding(assetHoldingEmpty) - a.True(rd.IsAsset()) - a.True(rd.IsHolding()) - a.True(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) - - // check both empty - rd = resourcesData{} - rd.SetAssetHolding(assetHoldingEmpty) - rd.SetAssetParams(assetParamsEmpty, true) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.True(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - a.Equal(assetParamsEmpty, rd.GetAssetParams()) - a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) - - // check empty states + non-empty params - assetParams := ledgertesting.RandomAssetParams() - rd = resourcesData{} - rd.SetAssetHolding(assetHoldingEmpty) - rd.SetAssetParams(assetParams, true) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - a.Equal(assetParams, rd.GetAssetParams()) - a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) - - assetHolding := ledgertesting.RandomAssetHolding(true) - rd.SetAssetHolding(assetHolding) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - a.Equal(assetParams, rd.GetAssetParams()) - a.Equal(assetHolding, rd.GetAssetHolding()) - - // check ClearAssetHolding - rd.ClearAssetHolding() - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.False(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - a.Equal(assetParams, rd.GetAssetParams()) - a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) - - // check ClearAssetParams - rd.SetAssetHolding(assetHolding) - rd.ClearAssetParams() - a.True(rd.IsAsset()) - a.False(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - a.Equal(assetParamsEmpty, rd.GetAssetParams()) - a.Equal(assetHolding, rd.GetAssetHolding()) - - // check both clear - rd.ClearAssetHolding() - a.False(rd.IsAsset()) - a.False(rd.IsOwning()) - a.False(rd.IsHolding()) - a.True(rd.IsEmptyAssetFields()) - a.True(rd.IsEmpty()) - a.Equal(assetParamsEmpty, rd.GetAssetParams()) - a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) - - // check params clear when non-empty params and empty holding - rd = resourcesData{} - rd.SetAssetHolding(assetHoldingEmpty) - rd.SetAssetParams(assetParams, true) - rd.ClearAssetParams() - a.True(rd.IsAsset()) - a.False(rd.IsOwning()) - a.True(rd.IsHolding()) - a.True(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - a.Equal(assetParamsEmpty, rd.GetAssetParams()) - a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) - - rd = resourcesData{} - rd.SetAssetHolding(assetHoldingEmpty) - a.True(rd.IsEmptyAssetFields()) - a.True(rd.IsAsset()) - a.False(rd.IsEmpty()) - a.Equal(rd.ResourceFlags, resourceFlagsEmptyAsset) - rd.ClearAssetHolding() - a.False(rd.IsAsset()) - a.True(rd.IsEmptyAssetFields()) - a.True(rd.IsEmpty()) - a.Equal(rd.ResourceFlags, resourceFlagsNotHolding) - - // check migration operations (accountDataResources) - // 1. both exist and empty - rd = makeResourcesData(0) - rd.SetAssetHolding(assetHoldingEmpty) - rd.SetAssetParams(assetParamsEmpty, true) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.True(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - - // 2. both exist and not empty - rd = makeResourcesData(0) - rd.SetAssetHolding(assetHolding) - rd.SetAssetParams(assetParams, true) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - - // 3. both exist: holding not empty, param is empty - rd = makeResourcesData(0) - rd.SetAssetHolding(assetHolding) - rd.SetAssetParams(assetParamsEmpty, true) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - - // 4. both exist: holding empty, param is not empty - rd = makeResourcesData(0) - rd.SetAssetHolding(assetHoldingEmpty) - rd.SetAssetParams(assetParams, true) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - - // 5. holding does not exist and params is empty - rd = makeResourcesData(0) - rd.SetAssetParams(assetParamsEmpty, false) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.False(rd.IsHolding()) - a.True(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - - // 6. holding does not exist and params is not empty - rd = makeResourcesData(0) - rd.SetAssetParams(assetParams, false) - a.True(rd.IsAsset()) - a.True(rd.IsOwning()) - a.False(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - - // 7. holding exist and not empty and params does not exist - rd = makeResourcesData(0) - rd.SetAssetHolding(assetHolding) - a.True(rd.IsAsset()) - a.False(rd.IsOwning()) - a.True(rd.IsHolding()) - a.False(rd.IsEmptyAssetFields()) - a.False(rd.IsEmpty()) - - // 8. both do not exist - rd = makeResourcesData(0) - a.False(rd.IsAsset()) - a.False(rd.IsOwning()) - a.False(rd.IsHolding()) - a.True(rd.IsEmptyAssetFields()) - a.True(rd.IsEmpty()) -} - -// TestResourcesDataSetData checks combinations of old/new values when -// updating resourceData from resourceDelta -func TestResourcesDataSetData(t *testing.T) { - partitiontest.PartitionTest(t) - - a := require.New(t) - - type deltaCode int - const ( - tri deltaCode = iota + 1 - del - emp - act - ) - - // apply deltas encoded as deltaCode to a base resourcesData for both apps and assets - apply := func(t *testing.T, base resourcesData, testType basics.CreatableType, pcode, hcode deltaCode) resourcesData { - if testType == basics.AssetCreatable { - var p ledgercore.AssetParamsDelta - var h ledgercore.AssetHoldingDelta - switch pcode { - case tri: - break - case del: - p = ledgercore.AssetParamsDelta{Deleted: true} - case emp: - p = ledgercore.AssetParamsDelta{Params: &basics.AssetParams{}} - case act: - p = ledgercore.AssetParamsDelta{Params: &basics.AssetParams{Total: 1000}} - default: - t.Logf("invalid pcode: %d", pcode) - t.Fail() - } - switch hcode { - case tri: - break - case del: - h = ledgercore.AssetHoldingDelta{Deleted: true} - case emp: - h = ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{}} - case act: - h = ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 555}} - default: - t.Logf("invalid hcode: %d", hcode) - t.Fail() - } - base.SetAssetData(p, h) - } else { - var p ledgercore.AppParamsDelta - var h ledgercore.AppLocalStateDelta - switch pcode { - case tri: - break - case del: - p = ledgercore.AppParamsDelta{Deleted: true} - case emp: - p = ledgercore.AppParamsDelta{Params: &basics.AppParams{}} - case act: - p = ledgercore.AppParamsDelta{Params: &basics.AppParams{ClearStateProgram: []byte{4, 5, 6}}} - default: - t.Logf("invalid pcode: %d", pcode) - t.Fail() - } - switch hcode { - case tri: - break - case del: - h = ledgercore.AppLocalStateDelta{Deleted: true} - case emp: - h = ledgercore.AppLocalStateDelta{LocalState: &basics.AppLocalState{}} - case act: - h = ledgercore.AppLocalStateDelta{LocalState: &basics.AppLocalState{Schema: basics.StateSchema{NumByteSlice: 5}}} - default: - t.Logf("invalid hcode: %d", hcode) - t.Fail() - } - base.SetAppData(p, h) - } - - return base - } - - itb := func(i int) (b bool) { - return i != 0 - } - - type testcase struct { - p deltaCode - h deltaCode - isAsset int - isOwning int - isHolding int - isEmptyFields int - isEmpty int - } - - empty := func(testType basics.CreatableType) resourcesData { - return makeResourcesData(0) - } - emptyParamsNoHolding := func(testType basics.CreatableType) resourcesData { - rd := makeResourcesData(0) - if testType == basics.AssetCreatable { - rd.SetAssetParams(basics.AssetParams{}, false) - } else { - rd.SetAppParams(basics.AppParams{}, false) - } - return rd - } - emptyParamsEmptyHolding := func(testType basics.CreatableType) resourcesData { - rd := makeResourcesData(0) - if testType == basics.AssetCreatable { - rd.SetAssetHolding(basics.AssetHolding{}) - rd.SetAssetParams(basics.AssetParams{}, true) - } else { - rd.SetAppLocalState(basics.AppLocalState{}) - rd.SetAppParams(basics.AppParams{}, true) - } - return rd - } - emptyParamsNotEmptyHolding := func(testType basics.CreatableType) resourcesData { - rd := makeResourcesData(0) - if testType == basics.AssetCreatable { - rd.SetAssetHolding(basics.AssetHolding{Amount: 111}) - rd.SetAssetParams(basics.AssetParams{}, true) - } else { - rd.SetAppLocalState(basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}) - rd.SetAppParams(basics.AppParams{}, true) - } - return rd - } - paramsNoHolding := func(testType basics.CreatableType) resourcesData { - rd := makeResourcesData(0) - if testType == basics.AssetCreatable { - rd.SetAssetParams(basics.AssetParams{Total: 222}, false) - } else { - rd.SetAppParams(basics.AppParams{ApprovalProgram: []byte{1, 2, 3}}, false) - } - return rd - } - paramsEmptyHolding := func(testType basics.CreatableType) resourcesData { - rd := makeResourcesData(0) - if testType == basics.AssetCreatable { - rd.SetAssetHolding(basics.AssetHolding{}) - rd.SetAssetParams(basics.AssetParams{Total: 222}, true) - } else { - rd.SetAppLocalState(basics.AppLocalState{}) - rd.SetAppParams(basics.AppParams{ApprovalProgram: []byte{1, 2, 3}}, true) - } - return rd - } - paramsAndHolding := func(testType basics.CreatableType) resourcesData { - rd := makeResourcesData(0) - if testType == basics.AssetCreatable { - rd.SetAssetHolding(basics.AssetHolding{Amount: 111}) - rd.SetAssetParams(basics.AssetParams{Total: 222}, true) - } else { - rd.SetAppLocalState(basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}) - rd.SetAppParams(basics.AppParams{ApprovalProgram: []byte{1, 2, 3}}, true) - } - return rd - } - noParamsEmptyHolding := func(testType basics.CreatableType) resourcesData { - rd := makeResourcesData(0) - if testType == basics.AssetCreatable { - rd.SetAssetHolding(basics.AssetHolding{}) - } else { - rd.SetAppLocalState(basics.AppLocalState{}) - } - return rd - } - noParamsNotEmptyHolding := func(testType basics.CreatableType) resourcesData { - rd := makeResourcesData(0) - if testType == basics.AssetCreatable { - rd.SetAssetHolding(basics.AssetHolding{Amount: 111}) - } else { - rd.SetAppLocalState(basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}) - } - return rd - } - - var tests = []struct { - name string - baseRD func(testType basics.CreatableType) resourcesData - testcases []testcase - }{ - { - "empty_base", empty, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 0, 0, 0, 1, 1}, - {del, tri, 0, 0, 0, 1, 1}, - {emp, tri, 1, 1, 0, 1, 0}, - {act, tri, 1, 1, 0, 0, 0}, - - {tri, del, 0, 0, 0, 1, 1}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 0, 1, 1, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 0, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - - { - "empty_params_no_holding", emptyParamsNoHolding, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 1, 1, 0, 1, 0}, - {del, tri, 0, 0, 0, 1, 1}, - {emp, tri, 1, 1, 0, 1, 0}, - {act, tri, 1, 1, 0, 0, 0}, - - {tri, del, 1, 1, 0, 1, 0}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 1, 1, 1, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 1, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - { - "empty_params_empty_holding", emptyParamsEmptyHolding, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 1, 1, 1, 1, 0}, - {del, tri, 1, 0, 1, 1, 0}, - {emp, tri, 1, 1, 1, 1, 0}, - {act, tri, 1, 1, 1, 0, 0}, - - {tri, del, 1, 1, 0, 1, 0}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 1, 1, 1, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 1, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - { - "empty_params_not_empty_holding", emptyParamsNotEmptyHolding, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 1, 1, 1, 0, 0}, - {del, tri, 1, 0, 1, 0, 0}, - {emp, tri, 1, 1, 1, 0, 0}, - {act, tri, 1, 1, 1, 0, 0}, - - {tri, del, 1, 1, 0, 1, 0}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 1, 1, 1, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 1, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - { - "params_no_holding", paramsNoHolding, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 1, 1, 0, 0, 0}, - {del, tri, 0, 0, 0, 1, 1}, - {emp, tri, 1, 1, 0, 1, 0}, - {act, tri, 1, 1, 0, 0, 0}, - - {tri, del, 1, 1, 0, 0, 0}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 1, 1, 0, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 1, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - { - "params_empty_holding", paramsEmptyHolding, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 1, 1, 1, 0, 0}, - {del, tri, 1, 0, 1, 1, 0}, - {emp, tri, 1, 1, 1, 1, 0}, - {act, tri, 1, 1, 1, 0, 0}, - - {tri, del, 1, 1, 0, 0, 0}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 1, 1, 0, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 1, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - { - "params_and_holding", paramsAndHolding, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 1, 1, 1, 0, 0}, - {del, tri, 1, 0, 1, 0, 0}, - {emp, tri, 1, 1, 1, 0, 0}, - {act, tri, 1, 1, 1, 0, 0}, - - {tri, del, 1, 1, 0, 0, 0}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 1, 1, 0, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 1, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - { - "no_params_empty_holding", noParamsEmptyHolding, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 1, 0, 1, 1, 0}, - {del, tri, 1, 0, 1, 1, 0}, - {emp, tri, 1, 1, 1, 1, 0}, - {act, tri, 1, 1, 1, 0, 0}, - - {tri, del, 0, 0, 0, 1, 1}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 0, 1, 1, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 0, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - { - "no_params_not_empty_holding", noParamsNotEmptyHolding, - []testcase{ - // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty - {tri, tri, 1, 0, 1, 0, 0}, - {del, tri, 1, 0, 1, 0, 0}, - {emp, tri, 1, 1, 1, 0, 0}, - {act, tri, 1, 1, 1, 0, 0}, - - {tri, del, 0, 0, 0, 1, 1}, - {del, del, 0, 0, 0, 1, 1}, - {emp, del, 1, 1, 0, 1, 0}, - {act, del, 1, 1, 0, 0, 0}, - - {tri, emp, 1, 0, 1, 1, 0}, - {del, emp, 1, 0, 1, 1, 0}, - {emp, emp, 1, 1, 1, 1, 0}, - {act, emp, 1, 1, 1, 0, 0}, - - {tri, act, 1, 0, 1, 0, 0}, - {del, act, 1, 0, 1, 0, 0}, - {emp, act, 1, 1, 1, 0, 0}, - {act, act, 1, 1, 1, 0, 0}, - }, - }, - } - for _, testType := range []basics.CreatableType{basics.AssetCreatable, basics.AppCreatable} { - for _, test := range tests { - var testTypeStr string - if testType == basics.AssetCreatable { - testTypeStr = "asset" - } else { - testTypeStr = "app" - } - t.Run(fmt.Sprintf("test_%s_%s", testTypeStr, test.name), func(t *testing.T) { - for i, ts := range test.testcases { - t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) { - rd := test.baseRD(testType) - rd = apply(t, rd, testType, ts.p, ts.h) - if testType == basics.AssetCreatable { - a.Equal(itb(ts.isAsset), rd.IsAsset()) - a.Equal(itb(ts.isEmptyFields), rd.IsEmptyAssetFields()) - a.False(rd.IsApp()) - a.True(rd.IsEmptyAppFields()) - } else { - a.Equal(itb(ts.isAsset), rd.IsApp()) - a.Equal(itb(ts.isEmptyFields), rd.IsEmptyAppFields()) - a.False(rd.IsAsset()) - a.True(rd.IsEmptyAssetFields()) - } - a.Equal(itb(ts.isOwning), rd.IsOwning()) - a.Equal(itb(ts.isHolding), rd.IsHolding()) - a.Equal(itb(ts.isEmpty), rd.IsEmpty()) - }) - } - }) - } - } -} - -// TestResourceDataRoundtripConversion ensures that basics.AppLocalState, basics.AppParams, -// basics.AssetHolding, and basics.AssetParams can be converted to resourcesData and back without -// losing any data. It uses reflection to be sure that no new fields are omitted. -// -// In other words, this test makes sure any new fields in basics.AppLocalState, basics.AppParams, -// basics.AssetHolding, or basics.AssetParam also get added to resourcesData. -func TestResourceDataRoundtripConversion(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - t.Run("basics.AppLocalState", func(t *testing.T) { - t.Parallel() - for i := 0; i < 1000; i++ { - randObj, _ := protocol.RandomizeObject(&basics.AppLocalState{}) - basicsAppLocalState := *randObj.(*basics.AppLocalState) - - var data resourcesData - data.SetAppLocalState(basicsAppLocalState) - roundTripAppLocalState := data.GetAppLocalState() - - require.Equal(t, basicsAppLocalState, roundTripAppLocalState) - } - }) - - t.Run("basics.AppParams", func(t *testing.T) { - t.Parallel() - for i := 0; i < 1000; i++ { - randObj, _ := protocol.RandomizeObject(&basics.AppParams{}) - basicsAppParams := *randObj.(*basics.AppParams) - - for _, haveHoldings := range []bool{true, false} { - var data resourcesData - data.SetAppParams(basicsAppParams, haveHoldings) - roundTripAppParams := data.GetAppParams() - - require.Equal(t, basicsAppParams, roundTripAppParams) - } - } - }) - - t.Run("basics.AssetHolding", func(t *testing.T) { - t.Parallel() - for i := 0; i < 1000; i++ { - randObj, _ := protocol.RandomizeObject(&basics.AssetHolding{}) - basicsAssetHolding := *randObj.(*basics.AssetHolding) - - var data resourcesData - data.SetAssetHolding(basicsAssetHolding) - roundTripAssetHolding := data.GetAssetHolding() - - require.Equal(t, basicsAssetHolding, roundTripAssetHolding) - } - }) - - t.Run("basics.AssetParams", func(t *testing.T) { - t.Parallel() - for i := 0; i < 1000; i++ { - randObj, _ := protocol.RandomizeObject(&basics.AssetParams{}) - basicsAssetParams := *randObj.(*basics.AssetParams) - - for _, haveHoldings := range []bool{true, false} { - var data resourcesData - data.SetAssetParams(basicsAssetParams, haveHoldings) - roundTripAssetParams := data.GetAssetParams() - - require.Equal(t, basicsAssetParams, roundTripAssetParams) - } - } - }) -} - -// TestBaseAccountDataRoundtripConversion ensures that baseAccountData can be converted to -// ledgercore.AccountData and basics.AccountData and back without losing any data. It uses -// reflection to be sure that no new fields are omitted. -// -// In other words, this test makes sure any new fields in baseAccountData also get added to -// ledgercore.AccountData and basics.AccountData. You should add a manual override in this test if -// the field really only belongs in baseAccountData. -func TestBaseAccountDataRoundtripConversion(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - t.Run("ledgercore.AccountData", func(t *testing.T) { - t.Parallel() - for i := 0; i < 1000; i++ { - randObj, _ := protocol.RandomizeObject(&baseAccountData{}) - baseAccount := *randObj.(*baseAccountData) - - ledgercoreAccount := baseAccount.GetLedgerCoreAccountData() - var roundTripAccount baseAccountData - roundTripAccount.SetCoreAccountData(&ledgercoreAccount) - - // Manually set UpdateRound, since it is lost in GetLedgerCoreAccountData - roundTripAccount.UpdateRound = baseAccount.UpdateRound - - require.Equal(t, baseAccount, roundTripAccount) - } - }) - - t.Run("basics.AccountData", func(t *testing.T) { - t.Parallel() - for i := 0; i < 1000; i++ { - randObj, _ := protocol.RandomizeObject(&baseAccountData{}) - baseAccount := *randObj.(*baseAccountData) - - basicsAccount := baseAccount.GetAccountData() - var roundTripAccount baseAccountData - roundTripAccount.SetAccountData(&basicsAccount) - - // Manually set UpdateRound, since it is lost in GetAccountData - roundTripAccount.UpdateRound = baseAccount.UpdateRound - - // Manually set resources, since resource information is lost in GetAccountData - roundTripAccount.TotalAssetParams = baseAccount.TotalAssetParams - roundTripAccount.TotalAssets = baseAccount.TotalAssets - roundTripAccount.TotalAppLocalStates = baseAccount.TotalAppLocalStates - roundTripAccount.TotalAppParams = baseAccount.TotalAppParams - - require.Equal(t, baseAccount, roundTripAccount) - } - }) -} - -// TestBasicsAccountDataRoundtripConversion ensures that basics.AccountData can be converted to -// baseAccountData and back without losing any data. It uses reflection to be sure that this test is -// always up-to-date with new fields. -// -// In other words, this test makes sure any new fields in basics.AccountData also get added to -// baseAccountData. -func TestBasicsAccountDataRoundtripConversion(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - for i := 0; i < 1000; i++ { - randObj, _ := protocol.RandomizeObject(&basics.AccountData{}) - basicsAccount := *randObj.(*basics.AccountData) - - var baseAccount baseAccountData - baseAccount.SetAccountData(&basicsAccount) - roundTripAccount := baseAccount.GetAccountData() - - // Manually set resources, since GetAccountData doesn't attempt to restore them - roundTripAccount.AssetParams = basicsAccount.AssetParams - roundTripAccount.Assets = basicsAccount.Assets - roundTripAccount.AppLocalStates = basicsAccount.AppLocalStates - roundTripAccount.AppParams = basicsAccount.AppParams - - require.Equal(t, basicsAccount, roundTripAccount) - require.Equal(t, uint64(len(roundTripAccount.AssetParams)), baseAccount.TotalAssetParams) - require.Equal(t, uint64(len(roundTripAccount.Assets)), baseAccount.TotalAssets) - require.Equal(t, uint64(len(roundTripAccount.AppLocalStates)), baseAccount.TotalAppLocalStates) - require.Equal(t, uint64(len(roundTripAccount.AppParams)), baseAccount.TotalAppParams) - } -} - -// TestLedgercoreAccountDataRoundtripConversion ensures that ledgercore.AccountData can be converted -// to baseAccountData and back without losing any data. It uses reflection to be sure that no new -// fields are omitted. -// -// In other words, this test makes sure any new fields in ledgercore.AccountData also get added to -// baseAccountData. -func TestLedgercoreAccountDataRoundtripConversion(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - for i := 0; i < 1000; i++ { - randObj, _ := protocol.RandomizeObject(&ledgercore.AccountData{}) - ledgercoreAccount := *randObj.(*ledgercore.AccountData) - - var baseAccount baseAccountData - baseAccount.SetCoreAccountData(&ledgercoreAccount) - roundTripAccount := baseAccount.GetLedgerCoreAccountData() - - require.Equal(t, ledgercoreAccount, roundTripAccount) - } -} - -func TestBaseAccountDataIsEmpty(t *testing.T) { - partitiontest.PartitionTest(t) - positiveTesting := func(t *testing.T) { - var ba baseAccountData - require.True(t, ba.IsEmpty()) - for i := 0; i < 20; i++ { - h := crypto.Hash([]byte{byte(i)}) - rnd := binary.BigEndian.Uint64(h[:]) - ba.UpdateRound = rnd - require.True(t, ba.IsEmpty()) - } - } - var empty baseAccountData - negativeTesting := func(t *testing.T) { - for i := 0; i < 10000; i++ { - randObj, _ := protocol.RandomizeObjectField(&baseAccountData{}) - ba := randObj.(*baseAccountData) - if *ba == empty || ba.UpdateRound != 0 { - continue - } - require.False(t, ba.IsEmpty(), "base account : %v", ba) - } - } - structureTesting := func(t *testing.T) { - encoding, err := json.Marshal(&empty) - zeros32 := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" - expectedEncoding := `{"Status":0,"MicroAlgos":{"Raw":0},"RewardsBase":0,"RewardedMicroAlgos":{"Raw":0},"AuthAddr":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ","TotalAppSchemaNumUint":0,"TotalAppSchemaNumByteSlice":0,"TotalExtraAppPages":0,"TotalAssetParams":0,"TotalAssets":0,"TotalAppParams":0,"TotalAppLocalStates":0,"TotalBoxes":0,"TotalBoxBytes":0,"VoteID":[` + zeros32 + `],"SelectionID":[` + zeros32 + `],"VoteFirstValid":0,"VoteLastValid":0,"VoteKeyDilution":0,"StateProofID":[` + zeros32 + `,` + zeros32 + `],"UpdateRound":0}` - require.NoError(t, err) - require.Equal(t, expectedEncoding, string(encoding)) - } - t.Run("Positive", positiveTesting) - t.Run("Negative", negativeTesting) - t.Run("Structure", structureTesting) - -} - -func TestBaseOnlineAccountDataIsEmpty(t *testing.T) { - partitiontest.PartitionTest(t) - - positiveTesting := func(t *testing.T) { - var ba baseOnlineAccountData - require.True(t, ba.IsEmpty()) - require.True(t, ba.IsVotingEmpty()) - ba.MicroAlgos.Raw = 100 - require.True(t, ba.IsVotingEmpty()) - ba.RewardsBase = 200 - require.True(t, ba.IsVotingEmpty()) - } - var empty baseOnlineAccountData - negativeTesting := func(t *testing.T) { - for i := 0; i < 10; i++ { - randObj, _ := protocol.RandomizeObjectField(&baseOnlineAccountData{}) - ba := randObj.(*baseOnlineAccountData) - if *ba == empty { - continue - } - require.False(t, ba.IsEmpty(), "base account : %v", ba) - break - } - { - var ba baseOnlineAccountData - ba.MicroAlgos.Raw = 100 - require.False(t, ba.IsEmpty()) - } - { - var ba baseOnlineAccountData - ba.RewardsBase = 200 - require.False(t, ba.IsEmpty()) - } - } - structureTesting := func(t *testing.T) { - encoding, err := json.Marshal(&empty) - zeros32 := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" - expectedEncoding := `{"VoteID":[` + zeros32 + `],"SelectionID":[` + zeros32 + `],"VoteFirstValid":0,"VoteLastValid":0,"VoteKeyDilution":0,"StateProofID":[` + zeros32 + `,` + zeros32 + `],"MicroAlgos":{"Raw":0},"RewardsBase":0}` - require.NoError(t, err) - require.Equal(t, expectedEncoding, string(encoding)) - } - t.Run("Positive", positiveTesting) - t.Run("Negative", negativeTesting) - t.Run("Structure", structureTesting) - -} - -func TestBaseOnlineAccountDataGettersSetters(t *testing.T) { - partitiontest.PartitionTest(t) - - proto := config.Consensus[protocol.ConsensusCurrentVersion] - addr := ledgertesting.RandomAddress() - data := ledgertesting.RandomAccountData(1) - data.Status = basics.Online - crypto.RandBytes(data.VoteID[:]) - crypto.RandBytes(data.SelectionID[:]) - crypto.RandBytes(data.StateProofID[:]) - data.VoteFirstValid = basics.Round(crypto.RandUint64()) - data.VoteLastValid = basics.Round(crypto.RandUint64()) // int64 is the max sqlite can store - data.VoteKeyDilution = crypto.RandUint64() - - var ba baseOnlineAccountData - ad := ledgercore.ToAccountData(data) - ba.SetCoreAccountData(&ad) - - require.Equal(t, data.MicroAlgos, ba.MicroAlgos) - require.Equal(t, data.RewardsBase, ba.RewardsBase) - require.Equal(t, data.VoteID, ba.VoteID) - require.Equal(t, data.SelectionID, ba.SelectionID) - require.Equal(t, data.VoteFirstValid, ba.VoteFirstValid) - require.Equal(t, data.VoteLastValid, ba.VoteLastValid) - require.Equal(t, data.VoteKeyDilution, ba.VoteKeyDilution) - require.Equal(t, data.StateProofID, ba.StateProofID) - - normBalance := basics.NormalizedOnlineAccountBalance(basics.Online, data.RewardsBase, data.MicroAlgos, proto) - require.Equal(t, normBalance, ba.NormalizedOnlineBalance(proto)) - oa := ba.GetOnlineAccount(addr, normBalance) - - require.Equal(t, addr, oa.Address) - require.Equal(t, ba.MicroAlgos, oa.MicroAlgos) - require.Equal(t, ba.RewardsBase, oa.RewardsBase) - require.Equal(t, normBalance, oa.NormalizedOnlineBalance) - require.Equal(t, ba.VoteFirstValid, oa.VoteFirstValid) - require.Equal(t, ba.VoteLastValid, oa.VoteLastValid) - require.Equal(t, ba.StateProofID, oa.StateProofID) - - rewardsLevel := uint64(1) - microAlgos, _, _ := basics.WithUpdatedRewards( - proto, basics.Online, oa.MicroAlgos, basics.MicroAlgos{}, ba.RewardsBase, rewardsLevel, - ) - oad := ba.GetOnlineAccountData(proto, rewardsLevel) - - require.Equal(t, microAlgos, oad.MicroAlgosWithRewards) - require.Equal(t, ba.VoteID, oad.VoteID) - require.Equal(t, ba.SelectionID, oad.SelectionID) - require.Equal(t, ba.StateProofID, oad.StateProofID) - require.Equal(t, ba.VoteFirstValid, oad.VoteFirstValid) - require.Equal(t, ba.VoteLastValid, oad.VoteLastValid) - require.Equal(t, ba.VoteKeyDilution, oad.VoteKeyDilution) -} - -func TestBaseVotingDataGettersSetters(t *testing.T) { - partitiontest.PartitionTest(t) - - data := ledgertesting.RandomAccountData(1) - data.Status = basics.Online - crypto.RandBytes(data.VoteID[:]) - crypto.RandBytes(data.SelectionID[:]) - crypto.RandBytes(data.StateProofID[:]) - data.VoteFirstValid = basics.Round(crypto.RandUint64()) - data.VoteLastValid = basics.Round(crypto.RandUint64()) // int64 is the max sqlite can store - data.VoteKeyDilution = crypto.RandUint64() - - var bv baseVotingData - require.True(t, bv.IsEmpty()) - - ad := ledgercore.ToAccountData(data) - bv.SetCoreAccountData(&ad) - - require.False(t, bv.IsEmpty()) - require.Equal(t, data.VoteID, bv.VoteID) - require.Equal(t, data.SelectionID, bv.SelectionID) - require.Equal(t, data.VoteFirstValid, bv.VoteFirstValid) - require.Equal(t, data.VoteLastValid, bv.VoteLastValid) - require.Equal(t, data.VoteKeyDilution, bv.VoteKeyDilution) - require.Equal(t, data.StateProofID, bv.StateProofID) -} - -func TestBaseOnlineAccountDataReflect(t *testing.T) { - partitiontest.PartitionTest(t) - - require.Equal(t, 4, reflect.TypeOf(baseOnlineAccountData{}).NumField(), "update all getters and setters for baseOnlineAccountData and change the field count") -} - -func TestBaseVotingDataReflect(t *testing.T) { - partitiontest.PartitionTest(t) - - require.Equal(t, 7, reflect.TypeOf(baseVotingData{}).NumField(), "update all getters and setters for baseVotingData and change the field count") -} - func TestLookupAccountAddressFromAddressID(t *testing.T) { partitiontest.PartitionTest(t) - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() addrs := make([]basics.Address, 100) @@ -2804,7 +1432,7 @@ func TestLookupAccountAddressFromAddressID(t *testing.T) { } addrsids := make(map[basics.Address]int64) err := dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - accountsInitTest(t, tx, make(map[basics.Address]basics.AccountData), protocol.ConsensusCurrentVersion) + store.AccountsInitTest(t, tx, make(map[basics.Address]basics.AccountData), protocol.ConsensusCurrentVersion) for i := range addrs { res, err := tx.ExecContext(ctx, "INSERT INTO accountbase (address, data) VALUES (?, ?)", addrs[i][:], []byte{12, 3, 4}) @@ -2822,8 +1450,10 @@ func TestLookupAccountAddressFromAddressID(t *testing.T) { require.NoError(t, err) err = dbs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + arw := store.NewAccountsSQLReaderWriter(tx) + for addr, addrid := range addrsids { - retAddr, err := lookupAccountAddressFromAddressID(ctx, tx, addrid) + retAddr, err := arw.LookupAccountAddressFromAddressID(ctx, addrid) if err != nil { return err } @@ -2832,7 +1462,7 @@ func TestLookupAccountAddressFromAddressID(t *testing.T) { } } // test fail case: - retAddr, err := lookupAccountAddressFromAddressID(ctx, tx, -1) + retAddr, err := arw.LookupAccountAddressFromAddressID(ctx, -1) if !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("unexpected error : %w", err) @@ -2928,7 +1558,7 @@ func (m *mockAccountWriter) setResource(addr basics.Address, cidx basics.Creatab return nil } -func (m *mockAccountWriter) lookup(addr basics.Address) (pad persistedAccountData, ok bool, err error) { +func (m *mockAccountWriter) Lookup(addr basics.Address) (pad store.PersistedAccountData, ok bool, err error) { rowid, ok := m.addresses[addr] if !ok { return @@ -2938,13 +1568,13 @@ func (m *mockAccountWriter) lookup(addr basics.Address) (pad persistedAccountDat err = fmt.Errorf("not found %s", addr.String()) return } - pad.accountData.SetCoreAccountData(&data) - pad.addr = addr - pad.rowid = rowid + pad.AccountData.SetCoreAccountData(&data) + pad.Addr = addr + pad.Rowid = rowid return } -func (m *mockAccountWriter) lookupResource(addr basics.Address, cidx basics.CreatableIndex) (prd persistedResourcesData, ok bool, err error) { +func (m *mockAccountWriter) LookupResource(addr basics.Address, cidx basics.CreatableIndex) (prd store.PersistedResourcesData, ok bool, err error) { rowid, ok := m.addresses[addr] if !ok { return @@ -2955,23 +1585,23 @@ func (m *mockAccountWriter) lookupResource(addr basics.Address, cidx basics.Crea return } if res.AppLocalState != nil { - prd.data.SetAppLocalState(*res.AppLocalState) + prd.Data.SetAppLocalState(*res.AppLocalState) } if res.AppParams != nil { - prd.data.SetAppParams(*res.AppParams, prd.data.IsHolding()) + prd.Data.SetAppParams(*res.AppParams, prd.Data.IsHolding()) } if res.AssetHolding != nil { - prd.data.SetAssetHolding(*res.AssetHolding) + prd.Data.SetAssetHolding(*res.AssetHolding) } if res.AssetParams != nil { - prd.data.SetAssetParams(*res.AssetParams, prd.data.IsHolding()) + prd.Data.SetAssetParams(*res.AssetParams, prd.Data.IsHolding()) } - prd.addrid = rowid - prd.aidx = cidx + prd.Addrid = rowid + prd.Aidx = cidx return } -func (m *mockAccountWriter) insertAccount(addr basics.Address, normBalance uint64, data baseAccountData) (rowid int64, err error) { +func (m *mockAccountWriter) InsertAccount(addr basics.Address, normBalance uint64, data store.BaseAccountData) (rowid int64, err error) { rowid, ok := m.addresses[addr] if ok { err = fmt.Errorf("insertAccount: addr %s, rowid %d: UNIQUE constraint failed", addr.String(), rowid) @@ -2984,7 +1614,7 @@ func (m *mockAccountWriter) insertAccount(addr basics.Address, normBalance uint6 return } -func (m *mockAccountWriter) deleteAccount(rowid int64) (rowsAffected int64, err error) { +func (m *mockAccountWriter) DeleteAccount(rowid int64) (rowsAffected int64, err error) { var addr basics.Address var ok bool if addr, ok = m.rowids[rowid]; !ok { @@ -2998,7 +1628,7 @@ func (m *mockAccountWriter) deleteAccount(rowid int64) (rowsAffected int64, err return 1, nil } -func (m *mockAccountWriter) updateAccount(rowid int64, normBalance uint64, data baseAccountData) (rowsAffected int64, err error) { +func (m *mockAccountWriter) UpdateAccount(rowid int64, normBalance uint64, data store.BaseAccountData) (rowsAffected int64, err error) { if _, ok := m.rowids[rowid]; !ok { return 0, fmt.Errorf("updateAccount: not found rowid %d", rowid) } @@ -3013,19 +1643,19 @@ func (m *mockAccountWriter) updateAccount(rowid int64, normBalance uint64, data return 1, nil } -func (m *mockAccountWriter) insertResource(addrid int64, aidx basics.CreatableIndex, data resourcesData) (rowid int64, err error) { +func (m *mockAccountWriter) InsertResource(addrid int64, aidx basics.CreatableIndex, data store.ResourcesData) (rowid int64, err error) { key := mockResourcesKey{addrid, aidx} if _, ok := m.resources[key]; ok { return 0, fmt.Errorf("insertResource: (%d, %d): UNIQUE constraint failed", addrid, aidx) } // use persistedResourcesData.AccountResource for conversion - prd := persistedResourcesData{data: data} + prd := store.PersistedResourcesData{Data: data} new := prd.AccountResource() m.resources[key] = new return 1, nil } -func (m *mockAccountWriter) deleteResource(addrid int64, aidx basics.CreatableIndex) (rowsAffected int64, err error) { +func (m *mockAccountWriter) DeleteResource(addrid int64, aidx basics.CreatableIndex) (rowsAffected int64, err error) { key := mockResourcesKey{addrid, aidx} if _, ok := m.resources[key]; !ok { return 0, nil @@ -3034,14 +1664,14 @@ func (m *mockAccountWriter) deleteResource(addrid int64, aidx basics.CreatableIn return 1, nil } -func (m *mockAccountWriter) updateResource(addrid int64, aidx basics.CreatableIndex, data resourcesData) (rowsAffected int64, err error) { +func (m *mockAccountWriter) UpdateResource(addrid int64, aidx basics.CreatableIndex, data store.ResourcesData) (rowsAffected int64, err error) { key := mockResourcesKey{addrid, aidx} old, ok := m.resources[key] if !ok { return 0, fmt.Errorf("updateResource: not found (%d, %d)", addrid, aidx) } // use persistedResourcesData.AccountResource for conversion - prd := persistedResourcesData{data: data} + prd := store.PersistedResourcesData{Data: data} new := prd.AccountResource() if new == old { return 0, nil @@ -3050,25 +1680,25 @@ func (m *mockAccountWriter) updateResource(addrid int64, aidx basics.CreatableIn return 1, nil } -func (m *mockAccountWriter) upsertKvPair(key string, value []byte) error { +func (m *mockAccountWriter) UpsertKvPair(key string, value []byte) error { m.kvStore[key] = value return nil } -func (m *mockAccountWriter) deleteKvPair(key string) error { +func (m *mockAccountWriter) DeleteKvPair(key string) error { delete(m.kvStore, key) return nil } -func (m *mockAccountWriter) insertCreatable(cidx basics.CreatableIndex, ctype basics.CreatableType, creator []byte) (rowid int64, err error) { +func (m *mockAccountWriter) InsertCreatable(cidx basics.CreatableIndex, ctype basics.CreatableType, creator []byte) (rowid int64, err error) { return 0, fmt.Errorf("insertCreatable: not implemented") } -func (m *mockAccountWriter) deleteCreatable(cidx basics.CreatableIndex, ctype basics.CreatableType) (rowsAffected int64, err error) { +func (m *mockAccountWriter) DeleteCreatable(cidx basics.CreatableIndex, ctype basics.CreatableType) (rowsAffected int64, err error) { return 0, fmt.Errorf("deleteCreatable: not implemented") } -func (m *mockAccountWriter) close() { +func (m *mockAccountWriter) Close() { } func factorial(n int) int { @@ -3238,24 +1868,24 @@ func TestAccountUnorderedUpdates(t *testing.T) { err = mock.setResource(addr1, basics.CreatableIndex(aidx), ledgercore.AccountResource{AppLocalState: &basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}}) a.NoError(err) - updates := make([]ledgercore.AccountDeltas, 4) + updates := make([]ledgercore.StateDelta, 4) // payment addr1 -> observer - updates[0].Upsert(addr1, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 9000000}, TotalAppLocalStates: 1}}) - updates[0].Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 11000000}, TotalAppParams: 1}}) + updates[0].Accts.Upsert(addr1, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 9000000}, TotalAppLocalStates: 1}}) + updates[0].Accts.Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 11000000}, TotalAppParams: 1}}) // fund addr2, opt-in - updates[1].Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 10000000}, TotalAppParams: 1}}) - updates[1].Upsert(addr2, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}, TotalAppLocalStates: 1}}) - updates[1].UpsertAppResource(addr2, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{LocalState: &basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}}) + updates[1].Accts.Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 10000000}, TotalAppParams: 1}}) + updates[1].Accts.Upsert(addr2, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}, TotalAppLocalStates: 1}}) + updates[1].Accts.UpsertAppResource(addr2, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{LocalState: &basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}}) // close addr1: delete app, move funds - updates[2].UpsertAppResource(addr1, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{Deleted: true}) - updates[2].Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 19000000}, TotalAppParams: 1}}) - updates[2].Upsert(addr1, ledgercore.AccountData{}) + updates[2].Accts.UpsertAppResource(addr1, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{Deleted: true}) + updates[2].Accts.Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 19000000}, TotalAppParams: 1}}) + updates[2].Accts.Upsert(addr1, ledgercore.AccountData{}) // this is not required but adds one more resource entry and helps in combinations testing // update the app - updates[3].UpsertAppResource(observer, aidx, ledgercore.AppParamsDelta{Params: &basics.AppParams{ApprovalProgram: []byte{4, 5, 6}}}, ledgercore.AppLocalStateDelta{}) + updates[3].Accts.UpsertAppResource(observer, aidx, ledgercore.AppParamsDelta{Params: &basics.AppParams{ApprovalProgram: []byte{4, 5, 6}}}, ledgercore.AppLocalStateDelta{}) dbRound := basics.Round(16541781) latestRound := basics.Round(16541801) @@ -3265,15 +1895,15 @@ func TestAccountUnorderedUpdates(t *testing.T) { var baseAccounts lruAccounts baseAccounts.init(nil, 100, 80) - pad, ok, err := mock.lookup(addr1) + pad, ok, err := mock.Lookup(addr1) a.NoError(err) a.True(ok) baseAccounts.write(pad) - pad, ok, err = mock.lookup(observer) + pad, ok, err = mock.Lookup(observer) a.NoError(err) a.True(ok) baseAccounts.write(pad) - baseAccounts.write(persistedAccountData{addr: addr2}) + baseAccounts.write(store.PersistedAccountData{Addr: addr2}) acctDeltas := makeCompactAccountDeltas(updates, dbRound, false, baseAccounts) a.Empty(acctDeltas.misses) @@ -3283,11 +1913,11 @@ func TestAccountUnorderedUpdates(t *testing.T) { var baseResources lruResources baseResources.init(nil, 100, 80) - prd, ok, err := mock.lookupResource(addr1, basics.CreatableIndex(aidx)) + prd, ok, err := mock.LookupResource(addr1, basics.CreatableIndex(aidx)) a.NoError(err) a.True(ok) baseResources.write(prd, addr1) - prd, ok, err = mock.lookupResource(observer, basics.CreatableIndex(aidx)) + prd, ok, err = mock.LookupResource(observer, basics.CreatableIndex(aidx)) a.NoError(err) a.True(ok) baseResources.write(prd, observer) @@ -3335,20 +1965,20 @@ func TestAccountsNewRoundDeletedResourceEntries(t *testing.T) { err = mock.setResource(addr1, basics.CreatableIndex(aidx), ledgercore.AccountResource{AppLocalState: &basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}}) a.NoError(err) - updates := make([]ledgercore.AccountDeltas, 3) + updates := make([]ledgercore.StateDelta, 3) // fund addr2, opt-in, delete app, move funds - updates[0].Upsert(addr2, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}, TotalAppLocalStates: 1}}) - updates[0].UpsertAppResource(addr2, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{LocalState: &basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}}) + updates[0].Accts.Upsert(addr2, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}, TotalAppLocalStates: 1}}) + updates[0].Accts.UpsertAppResource(addr2, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{LocalState: &basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}}) // close addr1: delete app, move funds - updates[1].UpsertAppResource(addr1, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{Deleted: true}) - updates[1].Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 20000000}, TotalAppParams: 1}}) - updates[1].Upsert(addr1, ledgercore.AccountData{}) + updates[1].Accts.UpsertAppResource(addr1, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{Deleted: true}) + updates[1].Accts.Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 20000000}, TotalAppParams: 1}}) + updates[1].Accts.Upsert(addr1, ledgercore.AccountData{}) // close addr2: delete app, move funds - updates[2].UpsertAppResource(addr2, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{Deleted: true}) - updates[2].Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 11000000}, TotalAppParams: 1}}) - updates[2].Upsert(addr2, ledgercore.AccountData{}) + updates[2].Accts.UpsertAppResource(addr2, aidx, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{Deleted: true}) + updates[2].Accts.Upsert(observer, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 11000000}, TotalAppParams: 1}}) + updates[2].Accts.Upsert(addr2, ledgercore.AccountData{}) dbRound := basics.Round(1) latestRound := basics.Round(10) @@ -3358,26 +1988,26 @@ func TestAccountsNewRoundDeletedResourceEntries(t *testing.T) { var baseResources lruResources baseResources.init(nil, 100, 80) - pad, ok, err := mock.lookup(addr1) + pad, ok, err := mock.Lookup(addr1) a.NoError(err) a.True(ok) baseAccounts.write(pad) - pad, ok, err = mock.lookup(observer) + pad, ok, err = mock.Lookup(observer) a.NoError(err) a.True(ok) baseAccounts.write(pad) - baseAccounts.write(persistedAccountData{addr: addr2}) // put an empty record for addr2 to get rid of lookups + baseAccounts.write(store.PersistedAccountData{Addr: addr2}) // put an empty record for addr2 to get rid of lookups acctDeltas := makeCompactAccountDeltas(updates, dbRound, false, baseAccounts) a.Empty(acctDeltas.misses) a.Equal(3, acctDeltas.len()) // we want to have (addr1, aidx) and (observer, aidx) - prd, ok, err := mock.lookupResource(addr1, basics.CreatableIndex(aidx)) + prd, ok, err := mock.LookupResource(addr1, basics.CreatableIndex(aidx)) a.NoError(err) a.True(ok) baseResources.write(prd, addr1) - prd, ok, err = mock.lookupResource(observer, basics.CreatableIndex(aidx)) + prd, ok, err = mock.LookupResource(observer, basics.CreatableIndex(aidx)) a.NoError(err) a.True(ok) baseResources.write(prd, observer) @@ -3399,9 +2029,9 @@ func TestAccountsNewRoundDeletedResourceEntries(t *testing.T) { addressesToCheck := map[basics.Address]bool{addr1: true, addr2: true} matches := 0 for _, upd := range updatedAccounts { - if addressesToCheck[upd.addr] { - a.Equal(int64(0), upd.rowid) - a.Empty(upd.accountData) + if addressesToCheck[upd.Addr] { + a.Equal(int64(0), upd.Rowid) + a.Empty(upd.AccountData) matches++ } } @@ -3410,9 +2040,9 @@ func TestAccountsNewRoundDeletedResourceEntries(t *testing.T) { for addr := range addressesToCheck { upd := updatedResources[addr] a.Equal(1, len(upd)) - a.Equal(int64(0), upd[0].addrid) - a.Equal(basics.CreatableIndex(aidx), upd[0].aidx) - a.Equal(makeResourcesData(uint64(0)), upd[0].data) + a.Equal(int64(0), upd[0].Addrid) + a.Equal(basics.CreatableIndex(aidx), upd[0].Aidx) + a.Equal(store.MakeResourcesData(uint64(0)), upd[0].Data) } } @@ -3420,12 +2050,12 @@ func BenchmarkLRUResources(b *testing.B) { var baseResources lruResources baseResources.init(nil, 1000, 850) - var data persistedResourcesData + var data store.PersistedResourcesData var has bool addrs := make([]basics.Address, 850) for i := 0; i < 850; i++ { - data.data.ApprovalProgram = make([]byte, 8096*4) - data.aidx = basics.CreatableIndex(1) + data.Data.ApprovalProgram = make([]byte, 8096*4) + data.Aidx = basics.CreatableIndex(1) addrBytes := ([]byte(fmt.Sprintf("%d", i)))[:32] var addr basics.Address for i, b := range addrBytes { @@ -3450,15 +2080,15 @@ func initBoxDatabase(b *testing.B, totalBoxes, boxSize int) (db.Pair, func(), er } proto := config.Consensus[protocol.ConsensusCurrentVersion] - dbs, fn := dbOpenTest(b, false) - setDbLogging(b, dbs) + dbs, fn := storetesting.DbOpenTest(b, false) + storetesting.SetDbLogging(b, dbs) cleanup := func() { cleanupTestDb(dbs, fn, false) } tx, err := dbs.Wdb.Handle.Begin() require.NoError(b, err) - _, err = accountsInit(tx, make(map[basics.Address]basics.AccountData), proto) + _, err = store.AccountsInitLightTest(b, tx, make(map[basics.Address]basics.AccountData), proto) require.NoError(b, err) err = tx.Commit() require.NoError(b, err) @@ -3469,17 +2099,17 @@ func initBoxDatabase(b *testing.B, totalBoxes, boxSize int) (db.Pair, func(), er for batch := 0; batch <= batchCount; batch++ { tx, err = dbs.Wdb.Handle.Begin() require.NoError(b, err) - writer, err := makeAccountsSQLWriter(tx, false, false, true, false) + writer, err := store.MakeAccountsSQLWriter(tx, false, false, true, false) require.NoError(b, err) for boxIdx := 0; boxIdx < totalBoxes/batchCount; boxIdx++ { - err = writer.upsertKvPair(fmt.Sprintf("%d", cnt), make([]byte, boxSize)) + err = writer.UpsertKvPair(fmt.Sprintf("%d", cnt), make([]byte, boxSize)) require.NoError(b, err) cnt++ } err = tx.Commit() require.NoError(b, err) - writer.close() + writer.Close() } err = dbs.Wdb.SetSynchronousMode(context.Background(), db.SynchronousModeFull, true) return dbs, cleanup, err @@ -3511,10 +2141,10 @@ func BenchmarkBoxDatabaseRead(b *testing.B) { require.NoError(b, err) var v sql.NullString for i := 0; i < b.N; i++ { - var pv persistedKVData + var pv store.PersistedKVData boxName := boxNames[i%totalBoxes] b.StartTimer() - err = lookupStmt.QueryRow([]byte(fmt.Sprintf("%d", boxName))).Scan(&pv.round, &v) + err = lookupStmt.QueryRow([]byte(fmt.Sprintf("%d", boxName))).Scan(&pv.Round, &v) b.StopTimer() require.NoError(b, err) require.True(b, v.Valid) @@ -3542,9 +2172,9 @@ func BenchmarkBoxDatabaseRead(b *testing.B) { require.NoError(b, err) var v sql.NullString for i := 0; i < b.N+lookback; i++ { - var pv persistedKVData + var pv store.PersistedKVData boxName := boxNames[i%totalBoxes] - err = lookupStmt.QueryRow([]byte(fmt.Sprintf("%d", boxName))).Scan(&pv.round, &v) + err = lookupStmt.QueryRow([]byte(fmt.Sprintf("%d", boxName))).Scan(&pv.Round, &v) require.NoError(b, err) require.True(b, v.Valid) @@ -3552,7 +2182,7 @@ func BenchmarkBoxDatabaseRead(b *testing.B) { if i >= lookback { boxName = boxNames[(i-lookback)%totalBoxes] b.StartTimer() - err = lookupStmt.QueryRow([]byte(fmt.Sprintf("%d", boxName))).Scan(&pv.round, &v) + err = lookupStmt.QueryRow([]byte(fmt.Sprintf("%d", boxName))).Scan(&pv.Round, &v) b.StopTimer() require.NoError(b, err) require.True(b, v.Valid) @@ -3589,17 +2219,19 @@ func TestAccountOnlineQueries(t *testing.T) { proto := config.Consensus[protocol.ConsensusCurrentVersion] - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() require.NoError(t, err) defer tx.Rollback() + arw := store.NewAccountsSQLReaderWriter(tx) + var accts map[basics.Address]basics.AccountData - accountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) - totals, err := accountsTotals(context.Background(), tx, false) + store.AccountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) + totals, err := arw.AccountsTotals(context.Background(), false) require.NoError(t, err) var baseAccounts lruAccounts @@ -3669,13 +2301,13 @@ func TestAccountOnlineQueries(t *testing.T) { delta3.Upsert(addrB, dataB2) delta3.Upsert(addrC, dataC3) - addRound := func(rnd basics.Round, updates ledgercore.AccountDeltas) { - totals = ledgertesting.CalculateNewRoundAccountTotals(t, updates, 0, proto, accts, totals) - accts = applyPartialDeltas(accts, updates) + addRound := func(rnd basics.Round, updates ledgercore.StateDelta) { + totals = ledgertesting.CalculateNewRoundAccountTotals(t, updates.Accts, 0, proto, accts, totals) + accts = applyPartialDeltas(accts, updates.Accts) oldBase := rnd - 1 - updatesCnt := makeCompactAccountDeltas([]ledgercore.AccountDeltas{updates}, oldBase, true, baseAccounts) - updatesOnlineCnt := makeCompactOnlineAccountDeltas([]ledgercore.AccountDeltas{updates}, oldBase, baseOnlineAccounts) + updatesCnt := makeCompactAccountDeltas([]ledgercore.StateDelta{updates}, oldBase, true, baseAccounts) + updatesOnlineCnt := makeCompactOnlineAccountDeltas([]ledgercore.AccountDeltas{updates.Accts}, oldBase, baseOnlineAccounts) err = updatesCnt.accountsLoadOld(tx) require.NoError(t, err) @@ -3683,7 +2315,7 @@ func TestAccountOnlineQueries(t *testing.T) { err = updatesOnlineCnt.accountsLoadOld(tx) require.NoError(t, err) - err = accountsPutTotals(tx, totals, false) + err = arw.AccountsPutTotals(totals, false) require.NoError(t, err) updatedAccts, _, _, err := accountsNewRound(tx, updatesCnt, compactResourcesDeltas{}, nil, nil, proto, rnd) require.NoError(t, err) @@ -3693,20 +2325,20 @@ func TestAccountOnlineQueries(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, updatedOnlineAccts) - err = updateAccountsRound(tx, rnd) + err = arw.UpdateAccountsRound(rnd) require.NoError(t, err) } - addRound(1, delta1) - addRound(2, delta2) - addRound(3, delta3) + addRound(1, ledgercore.StateDelta{Accts: delta1}) + addRound(2, ledgercore.StateDelta{Accts: delta2}) + addRound(3, ledgercore.StateDelta{Accts: delta3}) - queries, err := onlineAccountsInitDbQueries(tx) + queries, err := store.OnlineAccountsInitDbQueries(tx) require.NoError(t, err) // check round 1 rnd := basics.Round(1) - online, err := accountsOnlineTop(tx, rnd, 0, 10, proto) + online, err := arw.AccountsOnlineTop(rnd, 0, 10, proto) require.NoError(t, err) require.Equal(t, 2, len(online)) require.NotContains(t, online, addrC) @@ -3723,29 +2355,29 @@ func TestAccountOnlineQueries(t *testing.T) { require.Equal(t, addrB, onlineAcctB.Address) require.Equal(t, dataB1.AccountBaseData.MicroAlgos, onlineAcctB.MicroAlgos) - paod, err := queries.lookupOnline(addrA, rnd) + paod, err := queries.LookupOnline(addrA, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrA, paod.addr) - require.Equal(t, dataA1.AccountBaseData.MicroAlgos, paod.accountData.MicroAlgos) - require.Equal(t, voteIDA, paod.accountData.VoteID) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrA, paod.Addr) + require.Equal(t, dataA1.AccountBaseData.MicroAlgos, paod.AccountData.MicroAlgos) + require.Equal(t, voteIDA, paod.AccountData.VoteID) - paod, err = queries.lookupOnline(addrB, rnd) + paod, err = queries.LookupOnline(addrB, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrB, paod.addr) - require.Equal(t, dataB1.AccountBaseData.MicroAlgos, paod.accountData.MicroAlgos) - require.Equal(t, voteIDB, paod.accountData.VoteID) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrB, paod.Addr) + require.Equal(t, dataB1.AccountBaseData.MicroAlgos, paod.AccountData.MicroAlgos) + require.Equal(t, voteIDB, paod.AccountData.VoteID) - paod, err = queries.lookupOnline(addrC, rnd) + paod, err = queries.LookupOnline(addrC, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrC, paod.addr) - require.Empty(t, paod.accountData) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrC, paod.Addr) + require.Empty(t, paod.AccountData) // check round 2 rnd = basics.Round(2) - online, err = accountsOnlineTop(tx, rnd, 0, 10, proto) + online, err = arw.AccountsOnlineTop(rnd, 0, 10, proto) require.NoError(t, err) require.Equal(t, 1, len(online)) require.NotContains(t, online, addrA) @@ -3757,28 +2389,28 @@ func TestAccountOnlineQueries(t *testing.T) { require.Equal(t, addrB, onlineAcctB.Address) require.Equal(t, dataB1.AccountBaseData.MicroAlgos, onlineAcctB.MicroAlgos) - paod, err = queries.lookupOnline(addrA, rnd) + paod, err = queries.LookupOnline(addrA, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrA, paod.addr) - require.Empty(t, paod.accountData) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrA, paod.Addr) + require.Empty(t, paod.AccountData) - paod, err = queries.lookupOnline(addrB, rnd) + paod, err = queries.LookupOnline(addrB, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrB, paod.addr) - require.Equal(t, dataB1.AccountBaseData.MicroAlgos, paod.accountData.MicroAlgos) - require.Equal(t, voteIDB, paod.accountData.VoteID) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrB, paod.Addr) + require.Equal(t, dataB1.AccountBaseData.MicroAlgos, paod.AccountData.MicroAlgos) + require.Equal(t, voteIDB, paod.AccountData.VoteID) - paod, err = queries.lookupOnline(addrC, rnd) + paod, err = queries.LookupOnline(addrC, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrC, paod.addr) - require.Empty(t, paod.accountData) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrC, paod.Addr) + require.Empty(t, paod.AccountData) // check round 3 rnd = basics.Round(3) - online, err = accountsOnlineTop(tx, rnd, 0, 10, proto) + online, err = arw.AccountsOnlineTop(rnd, 0, 10, proto) require.NoError(t, err) require.Equal(t, 1, len(online)) require.NotContains(t, online, addrA) @@ -3790,26 +2422,26 @@ func TestAccountOnlineQueries(t *testing.T) { require.Equal(t, addrC, onlineAcctC.Address) require.Equal(t, dataC3.AccountBaseData.MicroAlgos, onlineAcctC.MicroAlgos) - paod, err = queries.lookupOnline(addrA, rnd) + paod, err = queries.LookupOnline(addrA, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrA, paod.addr) - require.Empty(t, paod.accountData) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrA, paod.Addr) + require.Empty(t, paod.AccountData) - paod, err = queries.lookupOnline(addrB, rnd) + paod, err = queries.LookupOnline(addrB, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrB, paod.addr) - require.Empty(t, paod.accountData) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrB, paod.Addr) + require.Empty(t, paod.AccountData) - paod, err = queries.lookupOnline(addrC, rnd) + paod, err = queries.LookupOnline(addrC, rnd) require.NoError(t, err) - require.Equal(t, basics.Round(3), paod.round) - require.Equal(t, addrC, paod.addr) - require.Equal(t, dataC3.AccountBaseData.MicroAlgos, paod.accountData.MicroAlgos) - require.Equal(t, voteIDC, paod.accountData.VoteID) + require.Equal(t, basics.Round(3), paod.Round) + require.Equal(t, addrC, paod.Addr) + require.Equal(t, dataC3.AccountBaseData.MicroAlgos, paod.AccountData.MicroAlgos) + require.Equal(t, voteIDC, paod.AccountData.VoteID) - paods, err := onlineAccountsAll(tx, 0) + paods, err := arw.OnlineAccountsAll(0) require.NoError(t, err) require.Equal(t, 5, len(paods)) @@ -3824,87 +2456,87 @@ func TestAccountOnlineQueries(t *testing.T) { // A | 2 | 0 checkAddrB := func() { - require.Equal(t, int64(2), paods[0].rowid) - require.Equal(t, basics.Round(1), paods[0].updRound) - require.Equal(t, addrB, paods[0].addr) - require.Equal(t, int64(4), paods[1].rowid) - require.Equal(t, basics.Round(3), paods[1].updRound) - require.Equal(t, addrB, paods[1].addr) + require.Equal(t, int64(2), paods[0].Rowid) + require.Equal(t, basics.Round(1), paods[0].UpdRound) + require.Equal(t, addrB, paods[0].Addr) + require.Equal(t, int64(4), paods[1].Rowid) + require.Equal(t, basics.Round(3), paods[1].UpdRound) + require.Equal(t, addrB, paods[1].Addr) } checkAddrC := func() { - require.Equal(t, int64(5), paods[2].rowid) - require.Equal(t, basics.Round(3), paods[2].updRound) - require.Equal(t, addrC, paods[2].addr) + require.Equal(t, int64(5), paods[2].Rowid) + require.Equal(t, basics.Round(3), paods[2].UpdRound) + require.Equal(t, addrC, paods[2].Addr) } checkAddrA := func() { - require.Equal(t, int64(1), paods[3].rowid) - require.Equal(t, basics.Round(1), paods[3].updRound) - require.Equal(t, addrA, paods[3].addr) - require.Equal(t, int64(3), paods[4].rowid) - require.Equal(t, basics.Round(2), paods[4].updRound) - require.Equal(t, addrA, paods[4].addr) + require.Equal(t, int64(1), paods[3].Rowid) + require.Equal(t, basics.Round(1), paods[3].UpdRound) + require.Equal(t, addrA, paods[3].Addr) + require.Equal(t, int64(3), paods[4].Rowid) + require.Equal(t, basics.Round(2), paods[4].UpdRound) + require.Equal(t, addrA, paods[4].Addr) } checkAddrB() checkAddrC() checkAddrA() - paods, err = onlineAccountsAll(tx, 3) + paods, err = arw.OnlineAccountsAll(3) require.NoError(t, err) require.Equal(t, 5, len(paods)) checkAddrB() checkAddrC() checkAddrA() - paods, err = onlineAccountsAll(tx, 2) + paods, err = arw.OnlineAccountsAll(2) require.NoError(t, err) require.Equal(t, 3, len(paods)) checkAddrB() checkAddrC() - paods, err = onlineAccountsAll(tx, 1) + paods, err = arw.OnlineAccountsAll(1) require.NoError(t, err) require.Equal(t, 2, len(paods)) checkAddrB() - paods, rnd, err = queries.lookupOnlineHistory(addrA) + paods, rnd, err = queries.LookupOnlineHistory(addrA) require.NoError(t, err) require.Equal(t, basics.Round(3), rnd) require.Equal(t, 2, len(paods)) - require.Equal(t, int64(1), paods[0].rowid) - require.Equal(t, basics.Round(1), paods[0].updRound) - require.Equal(t, int64(3), paods[1].rowid) - require.Equal(t, basics.Round(2), paods[1].updRound) + require.Equal(t, int64(1), paods[0].Rowid) + require.Equal(t, basics.Round(1), paods[0].UpdRound) + require.Equal(t, int64(3), paods[1].Rowid) + require.Equal(t, basics.Round(2), paods[1].UpdRound) - paods, rnd, err = queries.lookupOnlineHistory(addrB) + paods, rnd, err = queries.LookupOnlineHistory(addrB) require.NoError(t, err) require.Equal(t, basics.Round(3), rnd) require.Equal(t, 2, len(paods)) - require.Equal(t, int64(2), paods[0].rowid) - require.Equal(t, basics.Round(1), paods[0].updRound) - require.Equal(t, int64(4), paods[1].rowid) - require.Equal(t, basics.Round(3), paods[1].updRound) + require.Equal(t, int64(2), paods[0].Rowid) + require.Equal(t, basics.Round(1), paods[0].UpdRound) + require.Equal(t, int64(4), paods[1].Rowid) + require.Equal(t, basics.Round(3), paods[1].UpdRound) - paods, rnd, err = queries.lookupOnlineHistory(addrC) + paods, rnd, err = queries.LookupOnlineHistory(addrC) require.NoError(t, err) require.Equal(t, basics.Round(3), rnd) require.Equal(t, 1, len(paods)) - require.Equal(t, int64(5), paods[0].rowid) - require.Equal(t, basics.Round(3), paods[0].updRound) + require.Equal(t, int64(5), paods[0].Rowid) + require.Equal(t, basics.Round(3), paods[0].UpdRound) } type mockOnlineAccountsWriter struct { rowid int64 } -func (w *mockOnlineAccountsWriter) insertOnlineAccount(addr basics.Address, normBalance uint64, data baseOnlineAccountData, updRound uint64, voteLastValid uint64) (rowid int64, err error) { +func (w *mockOnlineAccountsWriter) InsertOnlineAccount(addr basics.Address, normBalance uint64, data store.BaseOnlineAccountData, updRound uint64, voteLastValid uint64) (rowid int64, err error) { w.rowid++ return w.rowid, nil } -func (w *mockOnlineAccountsWriter) close() {} +func (w *mockOnlineAccountsWriter) Close() {} func TestAccountOnlineAccountsNewRound(t *testing.T) { partitiontest.PartitionTest(t) @@ -3926,7 +2558,7 @@ func TestAccountOnlineAccountsNewRound(t *testing.T) { // acct B is new and offline deltaB := onlineAccountDelta{ address: addrB, - newAcct: []baseOnlineAccountData{{ + newAcct: []store.BaseOnlineAccountData{{ MicroAlgos: basics.MicroAlgos{Raw: 200_000_000}, }}, updRound: []uint64{1}, @@ -3935,9 +2567,9 @@ func TestAccountOnlineAccountsNewRound(t *testing.T) { // acct C is new and online deltaC := onlineAccountDelta{ address: addrC, - newAcct: []baseOnlineAccountData{{ + newAcct: []store.BaseOnlineAccountData{{ MicroAlgos: basics.MicroAlgos{Raw: 300_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 500}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 500}, }}, newStatus: []basics.Status{basics.Online}, updRound: []uint64{2}, @@ -3945,15 +2577,15 @@ func TestAccountOnlineAccountsNewRound(t *testing.T) { // acct D is old and went offline deltaD := onlineAccountDelta{ address: addrD, - oldAcct: persistedOnlineAccountData{ - addr: addrD, - accountData: baseOnlineAccountData{ + oldAcct: store.PersistedOnlineAccountData{ + Addr: addrD, + AccountData: store.BaseOnlineAccountData{ MicroAlgos: basics.MicroAlgos{Raw: 400_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 500}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 500}, }, - rowid: 1, + Rowid: 1, }, - newAcct: []baseOnlineAccountData{{ + newAcct: []store.BaseOnlineAccountData{{ MicroAlgos: basics.MicroAlgos{Raw: 400_000_000}, }}, newStatus: []basics.Status{basics.Offline}, @@ -3963,17 +2595,17 @@ func TestAccountOnlineAccountsNewRound(t *testing.T) { // acct E is old online deltaE := onlineAccountDelta{ address: addrE, - oldAcct: persistedOnlineAccountData{ - addr: addrE, - accountData: baseOnlineAccountData{ + oldAcct: store.PersistedOnlineAccountData{ + Addr: addrE, + AccountData: store.BaseOnlineAccountData{ MicroAlgos: basics.MicroAlgos{Raw: 500_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 500}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 500}, }, - rowid: 2, + Rowid: 2, }, - newAcct: []baseOnlineAccountData{{ + newAcct: []store.BaseOnlineAccountData{{ MicroAlgos: basics.MicroAlgos{Raw: 500_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 600}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 600}, }}, newStatus: []basics.Status{basics.Online}, updRound: []uint64{4}, @@ -3985,9 +2617,9 @@ func TestAccountOnlineAccountsNewRound(t *testing.T) { require.NoError(t, err) require.Len(t, updated, 3) - require.Equal(t, updated[0].addr, addrC) - require.Equal(t, updated[1].addr, addrD) - require.Equal(t, updated[2].addr, addrE) + require.Equal(t, updated[0].Addr, addrC) + require.Equal(t, updated[1].Addr, addrD) + require.Equal(t, updated[2].Addr, addrE) // check errors: new online with empty voting data deltaC.newStatus[0] = basics.Online @@ -4024,13 +2656,13 @@ func TestAccountOnlineAccountsNewRoundFlip(t *testing.T) { // acct A is new, offline and then online deltaA := onlineAccountDelta{ address: addrA, - newAcct: []baseOnlineAccountData{ + newAcct: []store.BaseOnlineAccountData{ { MicroAlgos: basics.MicroAlgos{Raw: 100_000_000}, }, { MicroAlgos: basics.MicroAlgos{Raw: 100_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 100}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 100}, }, }, updRound: []uint64{1, 2}, @@ -4039,10 +2671,10 @@ func TestAccountOnlineAccountsNewRoundFlip(t *testing.T) { // acct B is new and online and then offline deltaB := onlineAccountDelta{ address: addrB, - newAcct: []baseOnlineAccountData{ + newAcct: []store.BaseOnlineAccountData{ { MicroAlgos: basics.MicroAlgos{Raw: 200_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 200}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 200}, }, { MicroAlgos: basics.MicroAlgos{Raw: 200_000_000}, @@ -4054,18 +2686,18 @@ func TestAccountOnlineAccountsNewRoundFlip(t *testing.T) { // acct C is old online, then online and then offline deltaC := onlineAccountDelta{ address: addrC, - oldAcct: persistedOnlineAccountData{ - addr: addrC, - accountData: baseOnlineAccountData{ + oldAcct: store.PersistedOnlineAccountData{ + Addr: addrC, + AccountData: store.BaseOnlineAccountData{ MicroAlgos: basics.MicroAlgos{Raw: 300_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 300}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 300}, }, - rowid: 1, + Rowid: 1, }, - newAcct: []baseOnlineAccountData{ + newAcct: []store.BaseOnlineAccountData{ { MicroAlgos: basics.MicroAlgos{Raw: 300_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 301}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 301}, }, { MicroAlgos: basics.MicroAlgos{Raw: 300_000_000}, @@ -4081,26 +2713,28 @@ func TestAccountOnlineAccountsNewRoundFlip(t *testing.T) { require.NoError(t, err) require.Len(t, updated, 5) - require.Equal(t, updated[0].addr, addrA) - require.Equal(t, updated[1].addr, addrB) - require.Equal(t, updated[2].addr, addrB) - require.Equal(t, updated[3].addr, addrC) - require.Equal(t, updated[4].addr, addrC) + require.Equal(t, updated[0].Addr, addrA) + require.Equal(t, updated[1].Addr, addrB) + require.Equal(t, updated[2].Addr, addrB) + require.Equal(t, updated[3].Addr, addrC) + require.Equal(t, updated[4].Addr, addrC) } func TestAccountOnlineRoundParams(t *testing.T) { partitiontest.PartitionTest(t) - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() require.NoError(t, err) defer tx.Rollback() + arw := store.NewAccountsSQLReaderWriter(tx) + var accts map[basics.Address]basics.AccountData - accountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) + store.AccountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) // entry i is for round i+1 since db initialized with entry for round 0 const maxRounds = 40 // any number @@ -4111,127 +2745,24 @@ func TestAccountOnlineRoundParams(t *testing.T) { onlineRoundParams[i].RewardsLevel = uint64(i + 1) } - err = accountsPutOnlineRoundParams(tx, onlineRoundParams, 1) + err = arw.AccountsPutOnlineRoundParams(onlineRoundParams, 1) require.NoError(t, err) - dbOnlineRoundParams, endRound, err := accountsOnlineRoundParams(tx) + dbOnlineRoundParams, endRound, err := arw.AccountsOnlineRoundParams() require.NoError(t, err) require.Equal(t, maxRounds+1, len(dbOnlineRoundParams)) // +1 comes from init state require.Equal(t, onlineRoundParams, dbOnlineRoundParams[1:]) require.Equal(t, maxRounds, int(endRound)) - err = accountsPruneOnlineRoundParams(tx, 10) + err = arw.AccountsPruneOnlineRoundParams(10) require.NoError(t, err) - dbOnlineRoundParams, endRound, err = accountsOnlineRoundParams(tx) + dbOnlineRoundParams, endRound, err = arw.AccountsOnlineRoundParams() require.NoError(t, err) require.Equal(t, onlineRoundParams[9:], dbOnlineRoundParams) require.Equal(t, maxRounds, int(endRound)) } -func TestRowidsToChunkedArgs(t *testing.T) { - partitiontest.PartitionTest(t) - - res := rowidsToChunkedArgs([]int64{1}) - require.Equal(t, 1, cap(res)) - require.Equal(t, 1, len(res)) - require.Equal(t, 1, cap(res[0])) - require.Equal(t, 1, len(res[0])) - require.Equal(t, []interface{}{int64(1)}, res[0]) - - input := make([]int64, 999) - for i := 0; i < len(input); i++ { - input[i] = int64(i) - } - res = rowidsToChunkedArgs(input) - require.Equal(t, 1, cap(res)) - require.Equal(t, 1, len(res)) - require.Equal(t, 999, cap(res[0])) - require.Equal(t, 999, len(res[0])) - for i := 0; i < len(input); i++ { - require.Equal(t, interface{}(int64(i)), res[0][i]) - } - - input = make([]int64, 1001) - for i := 0; i < len(input); i++ { - input[i] = int64(i) - } - res = rowidsToChunkedArgs(input) - require.Equal(t, 2, cap(res)) - require.Equal(t, 2, len(res)) - require.Equal(t, 999, cap(res[0])) - require.Equal(t, 999, len(res[0])) - require.Equal(t, 2, cap(res[1])) - require.Equal(t, 2, len(res[1])) - for i := 0; i < 999; i++ { - require.Equal(t, interface{}(int64(i)), res[0][i]) - } - j := 0 - for i := 999; i < len(input); i++ { - require.Equal(t, interface{}(int64(i)), res[1][j]) - j++ - } - - input = make([]int64, 2*999) - for i := 0; i < len(input); i++ { - input[i] = int64(i) - } - res = rowidsToChunkedArgs(input) - require.Equal(t, 2, cap(res)) - require.Equal(t, 2, len(res)) - require.Equal(t, 999, cap(res[0])) - require.Equal(t, 999, len(res[0])) - require.Equal(t, 999, cap(res[1])) - require.Equal(t, 999, len(res[1])) - for i := 0; i < 999; i++ { - require.Equal(t, interface{}(int64(i)), res[0][i]) - } - j = 0 - for i := 999; i < len(input); i++ { - require.Equal(t, interface{}(int64(i)), res[1][j]) - j++ - } -} - -// TestAccountDBTxTailLoad checks txtailNewRound and loadTxTail delete and load right data -func TestAccountDBTxTailLoad(t *testing.T) { - partitiontest.PartitionTest(t) - - const inMem = true - dbs, _ := dbOpenTest(t, inMem) - setDbLogging(t, dbs) - defer dbs.Close() - - tx, err := dbs.Wdb.Handle.Begin() - require.NoError(t, err) - defer tx.Rollback() - - err = accountsCreateTxTailTable(context.Background(), tx) - require.NoError(t, err) - - // insert 1500 rounds and retain past 1001 - startRound := basics.Round(1) - endRound := basics.Round(1500) - roundData := make([][]byte, 1500) - const retainSize = 1001 - for i := startRound; i <= endRound; i++ { - data := txTailRound{Hdr: bookkeeping.BlockHeader{TimeStamp: int64(i)}} - roundData[i-1] = protocol.Encode(&data) - } - forgetBefore := (endRound + 1).SubSaturate(retainSize) - err = txtailNewRound(context.Background(), tx, startRound, roundData, forgetBefore) - require.NoError(t, err) - - data, _, baseRound, err := loadTxTail(context.Background(), tx, endRound) - require.NoError(t, err) - require.Len(t, data, retainSize) - require.Equal(t, basics.Round(endRound-retainSize+1), baseRound) // 500...1500 - - for i, entry := range data { - require.Equal(t, int64(i+int(baseRound)), entry.Hdr.TimeStamp) - } -} - // TestOnlineAccountsDeletion checks the onlineAccountsDelete preseves online accounts entries // and deleted only expired offline and online rows // Round 1 2 3 4 5 6 7 @@ -4245,8 +2776,8 @@ func TestAccountDBTxTailLoad(t *testing.T) { func TestOnlineAccountsDeletion(t *testing.T) { partitiontest.PartitionTest(t) - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() @@ -4254,7 +2785,9 @@ func TestOnlineAccountsDeletion(t *testing.T) { defer tx.Rollback() var accts map[basics.Address]basics.AccountData - accountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) + store.AccountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) + + arw := store.NewAccountsSQLReaderWriter(tx) updates := compactOnlineAccountDeltas{} addrA := ledgertesting.RandomAddress() @@ -4262,17 +2795,17 @@ func TestOnlineAccountsDeletion(t *testing.T) { deltaA := onlineAccountDelta{ address: addrA, - newAcct: []baseOnlineAccountData{ + newAcct: []store.BaseOnlineAccountData{ { MicroAlgos: basics.MicroAlgos{Raw: 100_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 100}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 100}, }, { MicroAlgos: basics.MicroAlgos{Raw: 100_000_000}, }, { MicroAlgos: basics.MicroAlgos{Raw: 100_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 600}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 600}, }, }, updRound: []uint64{1, 3, 6}, @@ -4281,14 +2814,14 @@ func TestOnlineAccountsDeletion(t *testing.T) { // acct B is new and online and then offline deltaB := onlineAccountDelta{ address: addrB, - newAcct: []baseOnlineAccountData{ + newAcct: []store.BaseOnlineAccountData{ { MicroAlgos: basics.MicroAlgos{Raw: 200_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 300}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 300}, }, { MicroAlgos: basics.MicroAlgos{Raw: 200_000_000}, - baseVotingData: baseVotingData{VoteFirstValid: 700}, + BaseVotingData: store.BaseVotingData{VoteFirstValid: 700}, }, }, updRound: []uint64{3, 7}, @@ -4296,11 +2829,11 @@ func TestOnlineAccountsDeletion(t *testing.T) { } updates.deltas = append(updates.deltas, deltaA, deltaB) - writer, err := makeOnlineAccountsSQLWriter(tx, updates.len() > 0) + writer, err := store.MakeOnlineAccountsSQLWriter(tx, updates.len() > 0) if err != nil { return } - defer writer.close() + defer writer.Close() lastUpdateRound := basics.Round(10) proto := config.Consensus[protocol.ConsensusCurrentVersion] @@ -4308,291 +2841,69 @@ func TestOnlineAccountsDeletion(t *testing.T) { require.NoError(t, err) require.Len(t, updated, 5) - queries, err := onlineAccountsInitDbQueries(tx) + queries, err := store.OnlineAccountsInitDbQueries(tx) require.NoError(t, err) var count int64 - var history []persistedOnlineAccountData + var history []store.PersistedOnlineAccountData var validThrough basics.Round for _, rnd := range []basics.Round{1, 2, 3} { - err = onlineAccountsDelete(tx, rnd) + err = arw.OnlineAccountsDelete(rnd) require.NoError(t, err) err = tx.QueryRow("SELECT COUNT(1) FROM onlineaccounts").Scan(&count) require.NoError(t, err) require.Equal(t, int64(5), count) - history, validThrough, err = queries.lookupOnlineHistory(addrA) + history, validThrough, err = queries.LookupOnlineHistory(addrA) require.NoError(t, err) require.Equal(t, basics.Round(0), validThrough) // not set require.Len(t, history, 3) - history, validThrough, err = queries.lookupOnlineHistory(addrB) + history, validThrough, err = queries.LookupOnlineHistory(addrB) require.NoError(t, err) require.Equal(t, basics.Round(0), validThrough) require.Len(t, history, 2) } for _, rnd := range []basics.Round{4, 5, 6, 7} { - err = onlineAccountsDelete(tx, rnd) + err = arw.OnlineAccountsDelete(rnd) require.NoError(t, err) err = tx.QueryRow("SELECT COUNT(1) FROM onlineaccounts").Scan(&count) require.NoError(t, err) require.Equal(t, int64(3), count) - history, validThrough, err = queries.lookupOnlineHistory(addrA) + history, validThrough, err = queries.LookupOnlineHistory(addrA) require.NoError(t, err) require.Equal(t, basics.Round(0), validThrough) require.Len(t, history, 1) - history, validThrough, err = queries.lookupOnlineHistory(addrB) + history, validThrough, err = queries.LookupOnlineHistory(addrB) require.NoError(t, err) require.Equal(t, basics.Round(0), validThrough) require.Len(t, history, 2) } for _, rnd := range []basics.Round{8, 9} { - err = onlineAccountsDelete(tx, rnd) + err = arw.OnlineAccountsDelete(rnd) require.NoError(t, err) err = tx.QueryRow("SELECT COUNT(1) FROM onlineaccounts").Scan(&count) require.NoError(t, err) require.Equal(t, int64(2), count) - history, validThrough, err = queries.lookupOnlineHistory(addrA) + history, validThrough, err = queries.LookupOnlineHistory(addrA) require.NoError(t, err) require.Equal(t, basics.Round(0), validThrough) require.Len(t, history, 1) - history, validThrough, err = queries.lookupOnlineHistory(addrB) + history, validThrough, err = queries.LookupOnlineHistory(addrB) require.NoError(t, err) require.Equal(t, basics.Round(0), validThrough) require.Len(t, history, 1) } } -// Test functions operating on catchpointfirststageinfo table. -func TestCatchpointFirstStageInfoTable(t *testing.T) { - partitiontest.PartitionTest(t) - - dbs, _ := dbOpenTest(t, true) - defer dbs.Close() - - ctx := context.Background() - - err := accountsCreateCatchpointFirstStageInfoTable(ctx, dbs.Wdb.Handle) - require.NoError(t, err) - - for _, round := range []basics.Round{4, 6, 8} { - info := catchpointFirstStageInfo{ - TotalAccounts: uint64(round) * 10, - } - err = insertOrReplaceCatchpointFirstStageInfo(ctx, dbs.Wdb.Handle, round, &info) - require.NoError(t, err) - } - - for _, round := range []basics.Round{4, 6, 8} { - info, exists, err := selectCatchpointFirstStageInfo(ctx, dbs.Rdb.Handle, round) - require.NoError(t, err) - require.True(t, exists) - - infoExpected := catchpointFirstStageInfo{ - TotalAccounts: uint64(round) * 10, - } - require.Equal(t, infoExpected, info) - } - - _, exists, err := selectCatchpointFirstStageInfo(ctx, dbs.Rdb.Handle, 7) - require.NoError(t, err) - require.False(t, exists) - - rounds, err := selectOldCatchpointFirstStageInfoRounds(ctx, dbs.Rdb.Handle, 6) - require.NoError(t, err) - require.Equal(t, []basics.Round{4, 6}, rounds) - - err = deleteOldCatchpointFirstStageInfo(ctx, dbs.Wdb.Handle, 6) - require.NoError(t, err) - - rounds, err = selectOldCatchpointFirstStageInfoRounds(ctx, dbs.Rdb.Handle, 9) - require.NoError(t, err) - require.Equal(t, []basics.Round{8}, rounds) -} - -func TestUnfinishedCatchpointsTable(t *testing.T) { - partitiontest.PartitionTest(t) - - dbs, _ := dbOpenTest(t, true) - defer dbs.Close() - - err := accountsCreateUnfinishedCatchpointsTable( - context.Background(), dbs.Wdb.Handle) - require.NoError(t, err) - - var d3 crypto.Digest - rand.Read(d3[:]) - err = insertUnfinishedCatchpoint(context.Background(), dbs.Wdb.Handle, 3, d3) - require.NoError(t, err) - - var d5 crypto.Digest - rand.Read(d5[:]) - err = insertUnfinishedCatchpoint(context.Background(), dbs.Wdb.Handle, 5, d5) - require.NoError(t, err) - - ret, err := selectUnfinishedCatchpoints(context.Background(), dbs.Rdb.Handle) - require.NoError(t, err) - expected := []unfinishedCatchpointRecord{ - { - round: 3, - blockHash: d3, - }, - { - round: 5, - blockHash: d5, - }, - } - require.Equal(t, expected, ret) - - err = deleteUnfinishedCatchpoint(context.Background(), dbs.Wdb.Handle, 3) - require.NoError(t, err) - - ret, err = selectUnfinishedCatchpoints(context.Background(), dbs.Rdb.Handle) - require.NoError(t, err) - expected = []unfinishedCatchpointRecord{ - { - round: 5, - blockHash: d5, - }, - } - require.Equal(t, expected, ret) -} - -func TestRemoveOfflineStateProofID(t *testing.T) { - partitiontest.PartitionTest(t) - - accts := ledgertesting.RandomAccounts(20, true) - expectedAccts := make(map[basics.Address]basics.AccountData) - for addr, acct := range accts { - rand.Read(acct.StateProofID[:]) - accts[addr] = acct - - expectedAcct := acct - if acct.Status != basics.Online { - expectedAcct.StateProofID = merklesignature.Commitment{} - } - expectedAccts[addr] = expectedAcct - - } - - buildDB := func(accounts map[basics.Address]basics.AccountData) (db.Pair, *sql.Tx) { - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) - - tx, err := dbs.Wdb.Handle.Begin() - require.NoError(t, err) - - // this is the same seq as accountsInitTest makes but it stops - // before the online accounts table creation to generate a trie and commit it - _, err = accountsInit(tx, accounts, config.Consensus[protocol.ConsensusCurrentVersion]) - require.NoError(t, err) - - err = accountsAddNormalizedBalance(tx, config.Consensus[protocol.ConsensusCurrentVersion]) - require.NoError(t, err) - - err = accountsCreateResourceTable(context.Background(), tx) - require.NoError(t, err) - - err = performResourceTableMigration(context.Background(), tx, nil) - require.NoError(t, err) - - return dbs, tx - } - - dbs, tx := buildDB(accts) - defer dbs.Close() - defer tx.Rollback() - - // make second copy of DB to prepare exepected/fixed merkle trie - expectedDBs, expectedTx := buildDB(expectedAccts) - defer expectedDBs.Close() - defer expectedTx.Rollback() - - // create account hashes - computeRootHash := func(tx *sql.Tx, expected bool) (crypto.Digest, error) { - rows, err := tx.Query("SELECT address, data FROM accountbase") - require.NoError(t, err) - defer rows.Close() - - mc, err := MakeMerkleCommitter(tx, false) - require.NoError(t, err) - trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) - require.NoError(t, err) - - var addr basics.Address - for rows.Next() { - var addrbuf []byte - var encodedAcctData []byte - err = rows.Scan(&addrbuf, &encodedAcctData) - require.NoError(t, err) - copy(addr[:], addrbuf) - var ba baseAccountData - err = protocol.Decode(encodedAcctData, &ba) - require.NoError(t, err) - if expected && ba.Status != basics.Online { - require.Equal(t, merklesignature.Commitment{}, ba.StateProofID) - } - addHash := accountHashBuilderV6(addr, &ba, encodedAcctData) - added, err := trie.Add(addHash) - require.NoError(t, err) - require.True(t, added) - } - _, err = trie.Evict(true) - require.NoError(t, err) - return trie.RootHash() - } - oldRoot, err := computeRootHash(tx, false) - require.NoError(t, err) - require.NotEmpty(t, oldRoot) - - expectedRoot, err := computeRootHash(expectedTx, true) - require.NoError(t, err) - require.NotEmpty(t, expectedRoot) - - err = accountsCreateOnlineAccountsTable(context.Background(), tx) - require.NoError(t, err) - err = performOnlineAccountsTableMigration(context.Background(), tx, nil, nil) - require.NoError(t, err) - - // get the new hash and ensure it does not match to the old one (data migrated) - mc, err := MakeMerkleCommitter(tx, false) - require.NoError(t, err) - trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) - require.NoError(t, err) - - newRoot, err := trie.RootHash() - require.NoError(t, err) - require.NotEmpty(t, newRoot) - - require.NotEqual(t, oldRoot, newRoot) - require.Equal(t, expectedRoot, newRoot) - - rows, err := tx.Query("SELECT addrid, data FROM accountbase") - require.NoError(t, err) - defer rows.Close() - - for rows.Next() { - var addrid sql.NullInt64 - var encodedAcctData []byte - err = rows.Scan(&addrid, &encodedAcctData) - require.NoError(t, err) - var ba baseAccountData - err = protocol.Decode(encodedAcctData, &ba) - require.NoError(t, err) - if ba.Status != basics.Online { - require.True(t, ba.StateProofID.IsEmpty()) - } - } -} - -func randomBaseAccountData() baseAccountData { - vd := baseVotingData{ +func randomBaseAccountData() store.BaseAccountData { + vd := store.BaseVotingData{ VoteFirstValid: basics.Round(crypto.RandUint64()), VoteLastValid: basics.Round(crypto.RandUint64()), VoteKeyDilution: crypto.RandUint64(), @@ -4601,7 +2912,7 @@ func randomBaseAccountData() baseAccountData { crypto.RandBytes(vd.StateProofID[:]) crypto.RandBytes(vd.SelectionID[:]) - baseAD := baseAccountData{ + baseAD := store.BaseAccountData{ Status: basics.Online, MicroAlgos: basics.MicroAlgos{Raw: crypto.RandUint64()}, RewardsBase: crypto.RandUint64(), @@ -4614,7 +2925,7 @@ func randomBaseAccountData() baseAccountData { TotalAssets: crypto.RandUint64(), TotalAppParams: crypto.RandUint64(), TotalAppLocalStates: crypto.RandUint64(), - baseVotingData: vd, + BaseVotingData: vd, UpdateRound: crypto.RandUint64(), } @@ -4638,12 +2949,12 @@ func makeString(len int) string { return s } -func randomAssetResourceData() resourcesData { +func randomAssetResourceData() store.ResourcesData { currentConsensusParams := config.Consensus[protocol.ConsensusCurrentVersion] // resourcesData is suiteable for keeping asset params, holding, app params, app local state // but only asset + holding or app + local state can appear there - rdAsset := resourcesData{ + rdAsset := store.ResourcesData{ Total: crypto.RandUint64(), Decimals: uint32(crypto.RandUint63() % uint64(math.MaxUint32)), DefaultFrozen: true, @@ -4664,10 +2975,10 @@ func randomAssetResourceData() resourcesData { return rdAsset } -func randomAppResourceData() resourcesData { +func randomAppResourceData() store.ResourcesData { currentConsensusParams := config.Consensus[protocol.ConsensusCurrentVersion] - rdApp := resourcesData{ + rdApp := store.ResourcesData{ SchemaNumUint: crypto.RandUint64(), SchemaNumByteSlice: crypto.RandUint64(), diff --git a/ledger/acctonline.go b/ledger/acctonline.go index e9a20046f3..bc300cf1f8 100644 --- a/ledger/acctonline.go +++ b/ledger/acctonline.go @@ -33,6 +33,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/util/db" "github.com/algorand/go-algorand/util/metrics" @@ -51,9 +52,10 @@ type modifiedOnlineAccount struct { } // cachedOnlineAccount is a light-weight version of persistedOnlineAccountData suitable for in-memory caching +// //msgp:ignore cachedOnlineAccount type cachedOnlineAccount struct { - baseOnlineAccountData + store.BaseOnlineAccountData updRound basics.Round } @@ -63,7 +65,7 @@ type onlineAccounts struct { dbs db.Pair // Prepared SQL statements for fast accounts DB lookups. - accountsq *onlineAccountsDbQueries + accountsq store.OnlineAccountsReader // cachedDBRoundOnline is always exactly tracker DB round (and therefore, onlineAccountsRound()), // cached to use in lookup functions @@ -150,9 +152,10 @@ func (ao *onlineAccounts) initializeFromDisk(l ledgerForTracker, lastBalancesRou ao.log = l.trackerLog() err = ao.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { + arw := store.NewAccountsSQLReaderWriter(tx) var err0 error var endRound basics.Round - ao.onlineRoundParamsData, endRound, err0 = accountsOnlineRoundParams(tx) + ao.onlineRoundParamsData, endRound, err0 = arw.AccountsOnlineRoundParams() if err0 != nil { return err0 } @@ -160,7 +163,7 @@ func (ao *onlineAccounts) initializeFromDisk(l ledgerForTracker, lastBalancesRou return fmt.Errorf("last onlineroundparams round %d does not match dbround %d", endRound, ao.cachedDBRoundOnline) } - onlineAccounts, err0 := onlineAccountsAll(tx, onlineAccountsCacheMaxSize) + onlineAccounts, err0 := arw.OnlineAccountsAll(onlineAccountsCacheMaxSize) if err0 != nil { return err0 } @@ -172,7 +175,7 @@ func (ao *onlineAccounts) initializeFromDisk(l ledgerForTracker, lastBalancesRou return } - ao.accountsq, err = onlineAccountsInitDbQueries(ao.dbs.Rdb.Handle) + ao.accountsq, err = store.OnlineAccountsInitDbQueries(ao.dbs.Rdb.Handle) if err != nil { return } @@ -193,7 +196,7 @@ func (ao *onlineAccounts) latest() basics.Round { // close closes the accountUpdates, waiting for all the child go-routine to complete func (ao *onlineAccounts) close() { if ao.accountsq != nil { - ao.accountsq.close() + ao.accountsq.Close() ao.accountsq = nil } @@ -419,18 +422,20 @@ func (ao *onlineAccounts) commitRound(ctx context.Context, tx *sql.Tx, dcc *defe return err } - err = onlineAccountsDelete(tx, dcc.onlineAccountsForgetBefore) + arw := store.NewAccountsSQLReaderWriter(tx) + + err = arw.OnlineAccountsDelete(dcc.onlineAccountsForgetBefore) if err != nil { return err } - err = accountsPutOnlineRoundParams(tx, dcc.onlineRoundParams, dcc.oldBase+1) + err = arw.AccountsPutOnlineRoundParams(dcc.onlineRoundParams, dcc.oldBase+1) if err != nil { return err } // delete all entries all older than maxBalLookback (or votersLookback) rounds ago - err = accountsPruneOnlineRoundParams(tx, dcc.onlineAccountsForgetBefore) + err = arw.AccountsPruneOnlineRoundParams(dcc.onlineAccountsForgetBefore) return } @@ -463,10 +468,10 @@ func (ao *onlineAccounts) postCommit(ctx context.Context, dcc *deferredCommitCon for _, persistedAcct := range dcc.updatedPersistedOnlineAccounts { ao.baseOnlineAccounts.write(persistedAcct) ao.onlineAccountsCache.writeFrontIfExist( - persistedAcct.addr, + persistedAcct.Addr, cachedOnlineAccount{ - baseOnlineAccountData: persistedAcct.accountData, - updRound: persistedAcct.updRound, + BaseOnlineAccountData: persistedAcct.AccountData, + updRound: persistedAcct.UpdRound, }) } @@ -526,7 +531,7 @@ func (ao *onlineAccounts) onlineTotalsEx(rnd basics.Round) (basics.MicroAlgos, e ao.log.Errorf("onlineTotalsImpl error: %v", err) } - totalsOnline, err = ao.accountsq.lookupOnlineTotalsHistory(rnd) + totalsOnline, err = ao.accountsq.LookupOnlineTotalsHistory(rnd) return totalsOnline, err } @@ -615,7 +620,7 @@ func (ao *onlineAccounts) lookupOnlineAccountData(rnd basics.Round, addr basics. var paramsOffset uint64 var rewardsProto config.ConsensusParams var rewardsLevel uint64 - var persistedData persistedOnlineAccountData + var persistedData store.PersistedOnlineAccountData // the loop serves retrying logic if the database advanced while // the function was analyzing deltas or caches. @@ -678,8 +683,8 @@ func (ao *onlineAccounts) lookupOnlineAccountData(rnd basics.Round, addr basics. // As an optimization, we avoid creating // a separate transaction here, and directly use a prepared SQL query // against the database. - persistedData, err = ao.accountsq.lookupOnline(addr, rnd) - if err != nil || persistedData.rowid == 0 { + persistedData, err = ao.accountsq.LookupOnline(addr, rnd) + if err != nil || persistedData.Rowid == 0 { // no such online account, return empty return ledgercore.OnlineAccountData{}, err } @@ -694,7 +699,7 @@ func (ao *onlineAccounts) lookupOnlineAccountData(rnd basics.Round, addr basics. // * if commitRound deletes some history after, the cache has additional entries and updRound comparison gets a right value // 2. after commitRound but before postCommit => OK, read full history, ignore the update from postCommit in writeFront's updRound comparison // 3. after postCommit => OK, postCommit does not add new entry with writeFrontIfExist, but here all the full history is loaded - persistedDataHistory, validThrough, err := ao.accountsq.lookupOnlineHistory(addr) + persistedDataHistory, validThrough, err := ao.accountsq.LookupOnlineHistory(addr) if err != nil || len(persistedDataHistory) == 0 { return ledgercore.OnlineAccountData{}, err } @@ -715,21 +720,21 @@ func (ao *onlineAccounts) lookupOnlineAccountData(rnd basics.Round, addr basics. } else { for _, data := range persistedDataHistory { written := ao.onlineAccountsCache.writeFront( - data.addr, + data.Addr, cachedOnlineAccount{ - baseOnlineAccountData: data.accountData, - updRound: data.updRound, + BaseOnlineAccountData: data.AccountData, + updRound: data.UpdRound, }) if !written { ao.accountsMu.Unlock() - err = fmt.Errorf("failed to write history of acct %s for round %d into online accounts cache", data.addr.String(), data.updRound) + err = fmt.Errorf("failed to write history of acct %s for round %d into online accounts cache", data.Addr.String(), data.UpdRound) return ledgercore.OnlineAccountData{}, err } } ao.log.Info("inserted new item to onlineAccountsCache") } ao.accountsMu.Unlock() - return persistedData.accountData.GetOnlineAccountData(rewardsProto, rewardsLevel), nil + return persistedData.AccountData.GetOnlineAccountData(rewardsProto, rewardsLevel), nil } // case 3.3: retry (for loop iterates and queries again) ao.accountsMu.Unlock() @@ -815,11 +820,12 @@ func (ao *onlineAccounts) TopOnlineAccounts(rnd basics.Round, voteRnd basics.Rou start := time.Now() ledgerAccountsonlinetopCount.Inc(nil) err = ao.dbs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - accts, err = accountsOnlineTop(tx, rnd, batchOffset, batchSize, genesisProto) + arw := store.NewAccountsSQLReaderWriter(tx) + accts, err = arw.AccountsOnlineTop(rnd, batchOffset, batchSize, genesisProto) if err != nil { return } - dbRound, err = accountsRound(tx) + dbRound, err = arw.AccountsRound() return }) ledgerAccountsonlinetopMicros.AddMicrosecondsSince(start, nil) diff --git a/ledger/acctonline_test.go b/ledger/acctonline_test.go index 67871c23e9..9debfc9e29 100644 --- a/ledger/acctonline_test.go +++ b/ledger/acctonline_test.go @@ -29,6 +29,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" @@ -81,6 +82,7 @@ func commitSyncPartial(t *testing.T, oa *onlineAccounts, ml *mockLedgerForTracke require.NoError(t, err) } err := ml.trackers.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + arw := store.NewAccountsSQLReaderWriter(tx) for _, lt := range ml.trackers.trackers { err0 := lt.commitRound(ctx, tx, dcc) if err0 != nil { @@ -88,7 +90,7 @@ func commitSyncPartial(t *testing.T, oa *onlineAccounts, ml *mockLedgerForTracke } } - return updateAccountsRound(tx, newBase) + return arw.UpdateAccountsRound(newBase) }) require.NoError(t, err) }() @@ -180,14 +182,14 @@ func TestAcctOnline(t *testing.T) { require.NoError(t, err) for _, bal := range allAccts { - data, err := oa.accountsq.lookupOnline(bal.Addr, 0) + data, err := oa.accountsq.LookupOnline(bal.Addr, 0) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.Equal(t, basics.Round(0), data.round) - require.Equal(t, bal.AccountData.MicroAlgos, data.accountData.MicroAlgos) - require.Equal(t, bal.AccountData.RewardsBase, data.accountData.RewardsBase) - require.Equal(t, bal.AccountData.VoteFirstValid, data.accountData.VoteFirstValid) - require.Equal(t, bal.AccountData.VoteLastValid, data.accountData.VoteLastValid) + require.Equal(t, bal.Addr, data.Addr) + require.Equal(t, basics.Round(0), data.Round) + require.Equal(t, bal.AccountData.MicroAlgos, data.AccountData.MicroAlgos) + require.Equal(t, bal.AccountData.RewardsBase, data.AccountData.RewardsBase) + require.Equal(t, bal.AccountData.VoteFirstValid, data.AccountData.VoteFirstValid) + require.Equal(t, bal.AccountData.VoteLastValid, data.AccountData.VoteLastValid) oad, err := oa.lookupOnlineAccountData(0, bal.Addr) require.NoError(t, err) @@ -219,29 +221,29 @@ func TestAcctOnline(t *testing.T) { rnd := i - basics.Round(maxDeltaLookback) acctIdx := int(rnd) - 1 bal := allAccts[acctIdx] - data, err := oa.accountsq.lookupOnline(bal.Addr, rnd) + data, err := oa.accountsq.LookupOnline(bal.Addr, rnd) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.NotEmpty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) - require.Empty(t, data.accountData) + require.Equal(t, bal.Addr, data.Addr) + require.NotEmpty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) + require.Empty(t, data.AccountData) data, has := oa.baseOnlineAccounts.read(bal.Addr) require.True(t, has) - require.NotEmpty(t, data.rowid) - require.Empty(t, data.accountData) + require.NotEmpty(t, data.Rowid) + require.Empty(t, data.AccountData) oad, err := oa.lookupOnlineAccountData(rnd, bal.Addr) require.NoError(t, err) require.Empty(t, oad) // check the prev original row is still there - data, err = oa.accountsq.lookupOnline(bal.Addr, rnd-1) + data, err = oa.accountsq.LookupOnline(bal.Addr, rnd-1) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.NotEmpty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) - require.NotEmpty(t, data.accountData) + require.Equal(t, bal.Addr, data.Addr) + require.NotEmpty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) + require.NotEmpty(t, data.AccountData) } // check data gets expired and removed from the DB @@ -252,17 +254,17 @@ func TestAcctOnline(t *testing.T) { rnd := i - basics.Round(maxBalLookback+maxDeltaLookback) acctIdx := int(rnd) - 1 bal := allAccts[acctIdx] - data, err := oa.accountsq.lookupOnline(bal.Addr, rnd) + data, err := oa.accountsq.LookupOnline(bal.Addr, rnd) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.Empty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) - require.Empty(t, data.accountData) + require.Equal(t, bal.Addr, data.Addr) + require.Empty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) + require.Empty(t, data.AccountData) data, has := oa.baseOnlineAccounts.read(bal.Addr) require.True(t, has) - require.NotEmpty(t, data.rowid) // TODO: FIXME: set rowid to empty for these items - require.Empty(t, data.accountData) + require.NotEmpty(t, data.Rowid) // TODO: FIXME: set rowid to empty for these items + require.Empty(t, data.AccountData) // committed round i => dbRound = i - maxDeltaLookback (= 13 for the account 0) // dbRound - maxBalLookback (= 1) is the "set offline" round for account 0 @@ -277,18 +279,18 @@ func TestAcctOnline(t *testing.T) { nextAcctIdx := acctIdx + 1 if nextAcctIdx < int(targetRound) { bal := allAccts[nextAcctIdx] - data, err := oa.accountsq.lookupOnline(bal.Addr, rnd) + data, err := oa.accountsq.LookupOnline(bal.Addr, rnd) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.NotEmpty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) - require.NotEmpty(t, data.accountData) + require.Equal(t, bal.Addr, data.Addr) + require.NotEmpty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) + require.NotEmpty(t, data.AccountData) // the most recent value is empty because the account is scheduled for removal data, has := oa.baseOnlineAccounts.read(bal.Addr) require.True(t, has) - require.NotEmpty(t, data.rowid) // TODO: FIXME: set rowid to empty for these items - require.Empty(t, data.accountData) + require.NotEmpty(t, data.Rowid) // TODO: FIXME: set rowid to empty for these items + require.Empty(t, data.AccountData) // account 1 went offline at round 2 => it offline at requested round 1+1=2 oad, err := oa.lookupOnlineAccountData(rnd+1, bal.Addr) @@ -301,18 +303,18 @@ func TestAcctOnline(t *testing.T) { nextNextAcctIdx := nextAcctIdx + 1 if nextNextAcctIdx < int(targetRound) { bal := allAccts[nextNextAcctIdx] - data, err := oa.accountsq.lookupOnline(bal.Addr, rnd) + data, err := oa.accountsq.LookupOnline(bal.Addr, rnd) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.NotEmpty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) - require.NotEmpty(t, data.accountData) + require.Equal(t, bal.Addr, data.Addr) + require.NotEmpty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) + require.NotEmpty(t, data.AccountData) // the most recent value is empty because the account is scheduled for removal data, has := oa.baseOnlineAccounts.read(bal.Addr) require.True(t, has) - require.NotEmpty(t, data.rowid) // TODO: FIXME: set rowid to empty for these items - require.Empty(t, data.accountData) + require.NotEmpty(t, data.Rowid) // TODO: FIXME: set rowid to empty for these items + require.Empty(t, data.AccountData) // account 2 went offline at round 3 => it online at requested round 1+1=2 oad, err := oa.lookupOnlineAccountData(rnd+1, bal.Addr) @@ -331,29 +333,29 @@ func TestAcctOnline(t *testing.T) { for i := numPersistedAccounts - maxBalLookback; i < numPersistedAccounts; i++ { bal := allAccts[i] // we expire account i at round i+1 - data, err := oa.accountsq.lookupOnline(bal.Addr, basics.Round(i+1)) + data, err := oa.accountsq.LookupOnline(bal.Addr, basics.Round(i+1)) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.NotEmpty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) - require.Empty(t, data.accountData) + require.Equal(t, bal.Addr, data.Addr) + require.NotEmpty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) + require.Empty(t, data.AccountData) data, has := oa.baseOnlineAccounts.read(bal.Addr) require.True(t, has) - require.NotEmpty(t, data.rowid) - require.Empty(t, data.accountData) + require.NotEmpty(t, data.Rowid) + require.Empty(t, data.AccountData) oad, err := oa.lookupOnlineAccountData(basics.Round(i+1), bal.Addr) require.NoError(t, err) require.Empty(t, oad) // ensure the online entry is still in the DB for the round i - data, err = oa.accountsq.lookupOnline(bal.Addr, basics.Round(i)) + data, err = oa.accountsq.LookupOnline(bal.Addr, basics.Round(i)) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.NotEmpty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) - require.NotEmpty(t, data.accountData) + require.Equal(t, bal.Addr, data.Addr) + require.NotEmpty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) + require.NotEmpty(t, data.AccountData) } // check maxDeltaLookback accounts in in-memory deltas, check it @@ -364,12 +366,12 @@ func TestAcctOnline(t *testing.T) { require.Empty(t, oad) // the table has old values b/c not committed yet - data, err := oa.accountsq.lookupOnline(bal.Addr, basics.Round(i)) + data, err := oa.accountsq.LookupOnline(bal.Addr, basics.Round(i)) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.NotEmpty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) - require.NotEmpty(t, data.accountData) + require.Equal(t, bal.Addr, data.Addr) + require.NotEmpty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) + require.NotEmpty(t, data.AccountData) // the base cache also does not have such entires data, has := oa.baseOnlineAccounts.read(bal.Addr) @@ -507,23 +509,23 @@ func TestAcctOnlineCache(t *testing.T) { rnd := i - basics.Round(maxDeltaLookback) acctIdx := (int(rnd) - 1) % numAccts bal := allAccts[acctIdx] - data, err := oa.accountsq.lookupOnline(bal.Addr, rnd) + data, err := oa.accountsq.LookupOnline(bal.Addr, rnd) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.NotEmpty(t, data.rowid) - require.Equal(t, oa.cachedDBRoundOnline, data.round) + require.Equal(t, bal.Addr, data.Addr) + require.NotEmpty(t, data.Rowid) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) if (rnd-1)%(numAccts*2) >= numAccts { - require.Empty(t, data.accountData) + require.Empty(t, data.AccountData) } else { - require.NotEmpty(t, data.accountData) + require.NotEmpty(t, data.AccountData) } cachedData, has := oa.onlineAccountsCache.read(bal.Addr, rnd) require.True(t, has) if (rnd-1)%(numAccts*2) >= numAccts { - require.Empty(t, cachedData.baseOnlineAccountData) + require.Empty(t, cachedData.BaseOnlineAccountData) } else { - require.NotEmpty(t, cachedData.baseOnlineAccountData) + require.NotEmpty(t, cachedData.BaseOnlineAccountData) } oad, err := oa.lookupOnlineAccountData(rnd, bal.Addr) @@ -540,24 +542,24 @@ func TestAcctOnlineCache(t *testing.T) { rnd := i - basics.Round(maxBalLookback+maxDeltaLookback) acctIdx := (int(rnd) - 1) % numAccts bal := allAccts[acctIdx] - data, err := oa.accountsq.lookupOnline(bal.Addr, rnd) + data, err := oa.accountsq.LookupOnline(bal.Addr, rnd) require.NoError(t, err) - require.Equal(t, bal.Addr, data.addr) - require.Equal(t, oa.cachedDBRoundOnline, data.round) + require.Equal(t, bal.Addr, data.Addr) + require.Equal(t, oa.cachedDBRoundOnline, data.Round) if (rnd-1)%(numAccts*2) >= numAccts { - require.Empty(t, data.accountData) - require.Empty(t, data.rowid) + require.Empty(t, data.AccountData) + require.Empty(t, data.Rowid) } else { - require.NotEmpty(t, data.rowid) - require.NotEmpty(t, data.accountData) + require.NotEmpty(t, data.Rowid) + require.NotEmpty(t, data.AccountData) } cachedData, has := oa.onlineAccountsCache.read(bal.Addr, rnd) require.True(t, has) if (rnd-1)%(numAccts*2) >= numAccts { - require.Empty(t, cachedData.baseOnlineAccountData) + require.Empty(t, cachedData.BaseOnlineAccountData) } else { - require.NotEmpty(t, cachedData.baseOnlineAccountData) + require.NotEmpty(t, cachedData.BaseOnlineAccountData) } // committed round i => dbRound = i - maxDeltaLookback @@ -573,7 +575,7 @@ func TestAcctOnlineCache(t *testing.T) { } require.Equal(t, targetRound-basics.Round(maxDeltaLookback), oa.cachedDBRoundOnline) - res, validThrough, err := oa.accountsq.lookupOnlineHistory(addrA) + res, validThrough, err := oa.accountsq.LookupOnlineHistory(addrA) require.NoError(t, err) require.Equal(t, oa.cachedDBRoundOnline, validThrough) // +1 because of deletion before X, and not checking acct state at X @@ -581,10 +583,10 @@ func TestAcctOnlineCache(t *testing.T) { // ensure the cache length corresponds to DB require.Equal(t, len(res), oa.onlineAccountsCache.accounts[addrA].Len()) for _, entry := range res { - cached, has := oa.onlineAccountsCache.read(addrA, entry.updRound) + cached, has := oa.onlineAccountsCache.read(addrA, entry.UpdRound) require.True(t, has) - require.Equal(t, entry.updRound, cached.updRound) - require.Equal(t, entry.accountData.VoteLastValid, cached.VoteLastValid) + require.Equal(t, entry.UpdRound, cached.updRound) + require.Equal(t, entry.AccountData.VoteLastValid, cached.VoteLastValid) } // ensure correct behavior after deleting cache @@ -616,7 +618,7 @@ func TestAcctOnlineCache(t *testing.T) { cachedData, has := oa.onlineAccountsCache.read(bal.Addr, oldRound) require.True(t, has) require.Equal(t, expectedRound, cachedData.updRound) - require.NotEmpty(t, cachedData.baseOnlineAccountData) + require.NotEmpty(t, cachedData.BaseOnlineAccountData) // cache should contain data for new rounds // (the last entry should be offline) @@ -625,7 +627,7 @@ func TestAcctOnlineCache(t *testing.T) { cachedData, has = oa.onlineAccountsCache.read(bal.Addr, newRound) require.True(t, has) require.Equal(t, newRound, cachedData.updRound) - require.Empty(t, cachedData.baseOnlineAccountData) + require.Empty(t, cachedData.BaseOnlineAccountData) }) } @@ -806,7 +808,8 @@ func TestAcctOnlineRoundParamsCache(t *testing.T) { var dbOnlineRoundParams []ledgercore.OnlineRoundParamsData var endRound basics.Round err := ao.dbs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - dbOnlineRoundParams, endRound, err = accountsOnlineRoundParams(tx) + arw := store.NewAccountsSQLReaderWriter(tx) + dbOnlineRoundParams, endRound, err = arw.AccountsOnlineRoundParams() return err }) require.NoError(t, err) @@ -1038,11 +1041,11 @@ func TestAcctOnlineCacheDBSync(t *testing.T) { require.NoError(t, err) require.Empty(t, data.VotingData.VoteLastValid) // ensure offline entry is in DB as well - pad, err := oa.accountsq.lookupOnline(addrA, 1) + pad, err := oa.accountsq.LookupOnline(addrA, 1) require.NoError(t, err) - require.Equal(t, addrA, pad.addr) - require.NotEmpty(t, pad.rowid) - require.Empty(t, pad.accountData.VoteLastValid) + require.Equal(t, addrA, pad.Addr) + require.NotEmpty(t, pad.Rowid) + require.Empty(t, pad.AccountData.VoteLastValid) // commit a block to get these entries removed // ensure the DB entry gone, the cache has it and lookupOnlineAccountData works as expected @@ -1071,11 +1074,11 @@ func TestAcctOnlineCacheDBSync(t *testing.T) { require.True(t, has) // full history loaded when looked up addrB prev time _, err = oa.lookupOnlineAccountData(1, addrB) require.Error(t, err) - pad, err = oa.accountsq.lookupOnline(addrB, 1) + pad, err = oa.accountsq.LookupOnline(addrB, 1) require.NoError(t, err) - require.Equal(t, addrB, pad.addr) - require.NotEmpty(t, pad.rowid) - require.NotEmpty(t, pad.accountData.VoteLastValid) + require.Equal(t, addrB, pad.Addr) + require.NotEmpty(t, pad.Rowid) + require.NotEmpty(t, pad.AccountData.VoteLastValid) }() // ensure the data not in deltas, in the cache and lookupOnlineAccountData still return a correct value @@ -1087,11 +1090,11 @@ func TestAcctOnlineCacheDBSync(t *testing.T) { data, err = oa.lookupOnlineAccountData(1, addrA) require.NoError(t, err) require.Empty(t, data.VotingData.VoteLastValid) - pad, err = oa.accountsq.lookupOnline(addrA, 1) + pad, err = oa.accountsq.LookupOnline(addrA, 1) require.NoError(t, err) - require.Equal(t, addrA, pad.addr) - require.Empty(t, pad.rowid) - require.Empty(t, pad.accountData.VoteLastValid) + require.Equal(t, addrA, pad.Addr) + require.Empty(t, pad.Rowid) + require.Empty(t, pad.AccountData.VoteLastValid) _, has = oa.accounts[addrB] require.False(t, has) @@ -1103,11 +1106,11 @@ func TestAcctOnlineCacheDBSync(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, data.VotingData.VoteLastValid) - pad, err = oa.accountsq.lookupOnline(addrB, 1) + pad, err = oa.accountsq.LookupOnline(addrB, 1) require.NoError(t, err) - require.Equal(t, addrB, pad.addr) - require.NotEmpty(t, pad.rowid) - require.NotEmpty(t, pad.accountData.VoteLastValid) + require.Equal(t, addrB, pad.Addr) + require.NotEmpty(t, pad.Rowid) + require.NotEmpty(t, pad.AccountData.VoteLastValid) }) } @@ -1190,7 +1193,7 @@ func TestAcctOnlineBaseAccountCache(t *testing.T) { commitSync(t, oa, ml, basics.Round(rnd)) poad, has := oa.baseOnlineAccounts.read(addrA) require.True(t, has) - require.Empty(t, poad.accountData) + require.Empty(t, poad.AccountData) data, err := oa.lookupOnlineAccountData(2, addrA) require.NoError(t, err) @@ -1207,7 +1210,7 @@ func TestAcctOnlineBaseAccountCache(t *testing.T) { poad, has = oa.baseOnlineAccounts.read(addrA) require.True(t, has) - require.NotEmpty(t, poad.accountData) + require.NotEmpty(t, poad.AccountData) data, err = oa.lookupOnlineAccountData(basics.Round(3), addrA) require.NoError(t, err) @@ -1290,7 +1293,8 @@ func TestAcctOnlineVotersLongerHistory(t *testing.T) { var dbOnlineRoundParams []ledgercore.OnlineRoundParamsData var endRound basics.Round err = oa.dbs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - dbOnlineRoundParams, endRound, err = accountsOnlineRoundParams(tx) + arw := store.NewAccountsSQLReaderWriter(tx) + dbOnlineRoundParams, endRound, err = arw.AccountsOnlineRoundParams() return err }) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 836a7c670d..c2cfc69fc6 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -36,6 +36,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/protocol" @@ -156,14 +157,14 @@ type accountUpdates struct { dbs db.Pair // Prepared SQL statements for fast accounts DB lookups. - accountsq *accountsDbQueries + accountsq store.AccountsReader // cachedDBRound is always exactly tracker DB round (and therefore, accountsRound()), // cached to use in lookup functions cachedDBRound basics.Round // deltas stores updates for every round after dbRound. - deltas []ledgercore.AccountDeltas + deltas []ledgercore.StateDelta // accounts stores the most recent account state for every // address that appears in deltas. @@ -173,16 +174,10 @@ type accountUpdates struct { // address&resource that appears in deltas. resources resourcesUpdates - // kvDeltas stores kvPair updates for every round after dbRound. - kvDeltas []map[string]ledgercore.KvValueDelta - // kvStore has the most recent kv pairs for every write/del that appears in // deltas. kvStore map[string]modifiedKvValue - // creatableDeltas stores creatable updates for every round after dbRound. - creatableDeltas []map[basics.CreatableIndex]ledgercore.ModifiedCreatable - // creatables stores the most recent state for every creatable that // appears in creatableDeltas creatables map[basics.CreatableIndex]ledgercore.ModifiedCreatable @@ -320,7 +315,7 @@ func (au *accountUpdates) loadFromDisk(l ledgerForTracker, lastBalancesRound bas // close closes the accountUpdates, waiting for all the child go-routine to complete func (au *accountUpdates) close() { if au.accountsq != nil { - au.accountsq.close() + au.accountsq.Close() au.accountsq = nil } au.baseAccounts.prune(0) @@ -380,7 +375,7 @@ func (au *accountUpdates) lookupKv(rnd basics.Round, key string, synchronized bo if indeltas { // Check if this is the most recent round, in which case, we can // use a cache of the most recent kvStore state - if offset == uint64(len(au.kvDeltas)) { + if offset == uint64(len(au.deltas)) { return mval.data, nil } @@ -389,7 +384,7 @@ func (au *accountUpdates) lookupKv(rnd basics.Round, key string, synchronized bo // backwards so later updates take priority. for offset > 0 { offset-- - mval, ok := au.kvDeltas[offset][key] + mval, ok := au.deltas[offset].KvMods[key] if ok { return mval.Data, nil } @@ -407,7 +402,7 @@ func (au *accountUpdates) lookupKv(rnd basics.Round, key string, synchronized bo // we don't technically need this, since it's already in the baseKV, however, writing this over // would ensure that we promote this field. au.baseKVs.writePending(pbd, key) - return pbd.value, nil + return pbd.Value, nil } if synchronized { @@ -419,25 +414,25 @@ func (au *accountUpdates) lookupKv(rnd basics.Round, key string, synchronized bo // roundOffset() made sure the round is exactly the one present in the // on-disk DB. - persistedData, err := au.accountsq.lookupKeyValue(key) + persistedData, err := au.accountsq.LookupKeyValue(key) if err != nil { return nil, err } - if persistedData.round == currentDbRound { + if persistedData.Round == currentDbRound { // if we read actual data return it. This includes deleted values // where persistedData.value == nil to avoid unnecessary db lookups // for deleted KVs. au.baseKVs.writePending(persistedData, key) - return persistedData.value, nil + return persistedData.Value, nil } // The db round is unexpected... if synchronized { - if persistedData.round < currentDbRound { + if persistedData.Round < currentDbRound { // Somehow the db is LOWER than it should be. - au.log.Errorf("accountUpdates.lookupKvPair: database round %d is behind in-memory round %d", persistedData.round, currentDbRound) - return nil, &StaleDatabaseRoundError{databaseRound: persistedData.round, memoryRound: currentDbRound} + au.log.Errorf("accountUpdates.lookupKvPair: database round %d is behind in-memory round %d", persistedData.Round, currentDbRound) + return nil, &StaleDatabaseRoundError{databaseRound: persistedData.Round, memoryRound: currentDbRound} } // The db is higher, so a write must have happened. Try again. au.accountsMu.RLock() @@ -448,8 +443,8 @@ func (au *accountUpdates) lookupKv(rnd basics.Round, key string, synchronized bo } } else { // in non-sync mode, we don't wait since we already assume that we're synchronized. - au.log.Errorf("accountUpdates.lookupKvPair: database round %d mismatching in-memory round %d", persistedData.round, currentDbRound) - return nil, &MismatchingDatabaseRoundError{databaseRound: persistedData.round, memoryRound: currentDbRound} + au.log.Errorf("accountUpdates.lookupKvPair: database round %d mismatching in-memory round %d", persistedData.Round, currentDbRound) + return nil, &MismatchingDatabaseRoundError{databaseRound: persistedData.Round, memoryRound: currentDbRound} } } @@ -513,7 +508,7 @@ func (au *accountUpdates) lookupKeysByPrefix(round basics.Round, keyPrefix strin for offset > 0 { offset-- - for keyInRound, mv := range au.kvDeltas[offset] { + for keyInRound, mv := range au.deltas[offset].KvMods { if !strings.HasPrefix(keyInRound, keyPrefix) { continue } @@ -552,7 +547,7 @@ func (au *accountUpdates) lookupKeysByPrefix(round basics.Round, keyPrefix strin // Finishing searching updates of this account in kvDeltas, keep going: use on-disk DB // to find the rest matching keys in DB. - dbRound, dbErr := au.accountsq.lookupKeysByPrefix(keyPrefix, maxKeyNum, results, resultCount) + dbRound, dbErr := au.accountsq.LookupKeysByPrefix(keyPrefix, maxKeyNum, results, resultCount) if dbErr != nil { return nil, dbErr } @@ -649,7 +644,7 @@ func (au *accountUpdates) listCreatables(maxCreatableIdx basics.CreatableIndex, // 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, dbRound, err := au.accountsq.listCreatables(maxCreatableIdx, numToFetch, ctype) + dbResults, dbRound, err := au.accountsq.ListCreatables(maxCreatableIdx, numToFetch, ctype) if err != nil { return nil, err } @@ -934,7 +929,8 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker, lastBalancesRou start := time.Now() ledgerAccountsinitCount.Inc(nil) err = au.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - totals, err0 := accountsTotals(ctx, tx, false) + arw := store.NewAccountsSQLReaderWriter(tx) + totals, err0 := arw.AccountsTotals(ctx, false) if err0 != nil { return err0 } @@ -948,7 +944,7 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker, lastBalancesRou return } - au.accountsq, err = accountsInitDbQueries(au.dbs.Rdb.Handle) + au.accountsq, err = store.AccountsInitDbQueries(au.dbs.Rdb.Handle) if err != nil { return } @@ -960,8 +956,6 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker, lastBalancesRou au.versions = []protocol.ConsensusVersion{hdr.CurrentProtocol} au.deltas = nil - au.kvDeltas = nil - au.creatableDeltas = nil au.accounts = make(map[basics.Address]modifiedAccount) au.resources = make(resourcesUpdates) au.kvStore = make(map[string]modifiedKvValue) @@ -987,10 +981,8 @@ func (au *accountUpdates) newBlockImpl(blk bookkeeping.Block, delta ledgercore.S if rnd != au.latest()+1 { au.log.Panicf("accountUpdates: newBlockImpl %d too far in the future, dbRound %d, deltas %d", rnd, au.cachedDBRound, len(au.deltas)) } - au.deltas = append(au.deltas, delta.Accts) + au.deltas = append(au.deltas, delta) au.versions = append(au.versions, blk.CurrentProtocol) - au.creatableDeltas = append(au.creatableDeltas, delta.Creatables) - au.kvDeltas = append(au.kvDeltas, delta.KvMods) au.deltasAccum = append(au.deltasAccum, delta.Accts.Len()+au.deltasAccum[len(au.deltasAccum)-1]) au.baseAccounts.flushPendingWrites() @@ -1070,8 +1062,8 @@ func (au *accountUpdates) lookupLatest(addr basics.Address) (data basics.Account var offset uint64 var rewardsProto config.ConsensusParams var rewardsLevel uint64 - var persistedData persistedAccountData - var persistedResources []persistedResourcesData + var persistedData store.PersistedAccountData + var persistedResources []store.PersistedResourcesData var resourceDbRound basics.Round withRewards := true @@ -1176,11 +1168,11 @@ func (au *accountUpdates) lookupLatest(addr basics.Address) (data basics.Account // use a cache of the most recent account state. ad = macct.data foundAccount = true - } else if pad, has := au.baseAccounts.read(addr); has && pad.round == currentDbRound { + } else if pad, has := au.baseAccounts.read(addr); has && pad.Round == currentDbRound { // we don't technically need this, since it's already in the baseAccounts, however, writing this over // would ensure that we promote this field. au.baseAccounts.writePending(pad) - ad = pad.accountData.GetLedgerCoreAccountData() + ad = pad.AccountData.GetLedgerCoreAccountData() foundAccount = true } @@ -1205,8 +1197,8 @@ func (au *accountUpdates) lookupLatest(addr basics.Address) (data basics.Account // we don't technically need this, since it's already in the baseResources, however, writing this over // would ensure that we promote this field. au.baseResources.writePending(prd, addr) - if prd.addrid != 0 { - if err := addResource(prd.aidx, rnd, prd.AccountResource()); err != nil { + if prd.Addrid != 0 { + if err := addResource(prd.Aidx, rnd, prd.AccountResource()); err != nil { return basics.AccountData{}, basics.Round(0), basics.MicroAlgos{}, err } } @@ -1225,15 +1217,15 @@ func (au *accountUpdates) lookupLatest(addr basics.Address) (data basics.Account // a separate transaction here, and directly use a prepared SQL query // against the database. if !foundAccount { - persistedData, err = au.accountsq.lookup(addr) + persistedData, err = au.accountsq.LookupAccount(addr) if err != nil { return basics.AccountData{}, basics.Round(0), basics.MicroAlgos{}, err } - if persistedData.round == currentDbRound { - if persistedData.rowid != 0 { + if persistedData.Round == currentDbRound { + if persistedData.Rowid != 0 { // if we read actual data return it au.baseAccounts.writePending(persistedData) - ad = persistedData.accountData.GetLedgerCoreAccountData() + ad = persistedData.AccountData.GetLedgerCoreAccountData() } else { ad = ledgercore.AccountData{} } @@ -1244,24 +1236,24 @@ func (au *accountUpdates) lookupLatest(addr basics.Address) (data basics.Account } } - if persistedData.round < currentDbRound { - au.log.Errorf("accountUpdates.lookupLatest: account database round %d is behind in-memory round %d", persistedData.round, currentDbRound) - return basics.AccountData{}, basics.Round(0), basics.MicroAlgos{}, &StaleDatabaseRoundError{databaseRound: persistedData.round, memoryRound: currentDbRound} + if persistedData.Round < currentDbRound { + au.log.Errorf("accountUpdates.lookupLatest: account database round %d is behind in-memory round %d", persistedData.Round, currentDbRound) + return basics.AccountData{}, basics.Round(0), basics.MicroAlgos{}, &StaleDatabaseRoundError{databaseRound: persistedData.Round, memoryRound: currentDbRound} } - if persistedData.round > currentDbRound { + if persistedData.Round > currentDbRound { goto tryAgain } } // Look for resources on disk - persistedResources, resourceDbRound, err = au.accountsq.lookupAllResources(addr) + persistedResources, resourceDbRound, err = au.accountsq.LookupAllResources(addr) if err != nil { return basics.AccountData{}, basics.Round(0), basics.MicroAlgos{}, err } if resourceDbRound == currentDbRound { for _, pd := range persistedResources { au.baseResources.writePending(pd, addr) - if err := addResource(pd.aidx, currentDbRound, pd.AccountResource()); err != nil { + if err := addResource(pd.Aidx, currentDbRound, pd.AccountResource()); err != nil { return basics.AccountData{}, basics.Round(0), basics.MicroAlgos{}, err } } @@ -1295,7 +1287,7 @@ func (au *accountUpdates) lookupResource(rnd basics.Round, addr basics.Address, } }() var offset uint64 - var persistedData persistedResourcesData + var persistedData store.PersistedResourcesData for { currentDbRound := au.cachedDBRound currentDeltaLen := len(au.deltas) @@ -1317,7 +1309,7 @@ func (au *accountUpdates) lookupResource(rnd basics.Round, addr basics.Address, // backwards to ensure that later updates take priority if present. for offset > 0 { offset-- - r, ok := au.deltas[offset].GetResource(addr, aidx, ctype) + r, ok := au.deltas[offset].Accts.GetResource(addr, aidx, ctype) if ok { // the returned validThrough here is not optimal, but it still correct. We could get a more accurate value by scanning // the deltas forward, but this would be time consuming loop, which might not pay off. @@ -1355,12 +1347,12 @@ func (au *accountUpdates) lookupResource(rnd basics.Round, addr basics.Address, // 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. - persistedData, err = au.accountsq.lookupResources(addr, aidx, ctype) + persistedData, err = au.accountsq.LookupResources(addr, aidx, ctype) if err != nil { return ledgercore.AccountResource{}, basics.Round(0), err } - if persistedData.round == currentDbRound { - if persistedData.addrid != 0 { + if persistedData.Round == currentDbRound { + if persistedData.Addrid != 0 { // if we read actual data return it au.baseResources.writePending(persistedData, addr) return persistedData.AccountResource(), rnd, nil @@ -1370,9 +1362,9 @@ func (au *accountUpdates) lookupResource(rnd basics.Round, addr basics.Address, return ledgercore.AccountResource{}, rnd, nil } if synchronized { - if persistedData.round < currentDbRound { - au.log.Errorf("accountUpdates.lookupResource: database round %d is behind in-memory round %d", persistedData.round, currentDbRound) - return ledgercore.AccountResource{}, basics.Round(0), &StaleDatabaseRoundError{databaseRound: persistedData.round, memoryRound: currentDbRound} + if persistedData.Round < currentDbRound { + au.log.Errorf("accountUpdates.lookupResource: database round %d is behind in-memory round %d", persistedData.Round, currentDbRound) + return ledgercore.AccountResource{}, basics.Round(0), &StaleDatabaseRoundError{databaseRound: persistedData.Round, memoryRound: currentDbRound} } au.accountsMu.RLock() needUnlock = true @@ -1381,12 +1373,29 @@ func (au *accountUpdates) lookupResource(rnd basics.Round, addr basics.Address, } } else { // in non-sync mode, we don't wait since we already assume that we're synchronized. - au.log.Errorf("accountUpdates.lookupResource: database round %d mismatching in-memory round %d", persistedData.round, currentDbRound) - return ledgercore.AccountResource{}, basics.Round(0), &MismatchingDatabaseRoundError{databaseRound: persistedData.round, memoryRound: currentDbRound} + au.log.Errorf("accountUpdates.lookupResource: database round %d mismatching in-memory round %d", persistedData.Round, currentDbRound) + return ledgercore.AccountResource{}, basics.Round(0), &MismatchingDatabaseRoundError{databaseRound: persistedData.Round, memoryRound: currentDbRound} } } } +func (au *accountUpdates) lookupStateDelta(rnd basics.Round) (ledgercore.StateDelta, error) { + au.accountsMu.RLock() + defer au.accountsMu.RUnlock() + var offset uint64 + var delta ledgercore.StateDelta + offset, err := au.roundOffset(rnd) + if err != nil { + return delta, err + } + if offset == 0 { + err = fmt.Errorf("round %d not in deltas: dbRound %d, deltas %d, offset %d", rnd, au.cachedDBRound, len(au.deltas), offset) + return delta, err + } + delta = au.deltas[offset-1] + return delta, err +} + // lookupWithoutRewards returns the account data for a given address at a given round. func (au *accountUpdates) lookupWithoutRewards(rnd basics.Round, addr basics.Address, synchronized bool) (data ledgercore.AccountData, validThrough basics.Round, rewardsVersion protocol.ConsensusVersion, rewardsLevel uint64, err error) { needUnlock := false @@ -1400,7 +1409,7 @@ func (au *accountUpdates) lookupWithoutRewards(rnd basics.Round, addr basics.Add } }() var offset uint64 - var persistedData persistedAccountData + var persistedData store.PersistedAccountData for { currentDbRound := au.cachedDBRound currentDeltaLen := len(au.deltas) @@ -1425,7 +1434,7 @@ func (au *accountUpdates) lookupWithoutRewards(rnd basics.Round, addr basics.Add // backwards to ensure that later updates take priority if present. for offset > 0 { offset-- - d, ok := au.deltas[offset].GetData(addr) + d, ok := au.deltas[offset].Accts.GetData(addr) if ok { // the returned validThrough here is not optimal, but it still correct. We could get a more accurate value by scanning // the deltas forward, but this would be time consuming loop, which might not pay off. @@ -1445,7 +1454,7 @@ func (au *accountUpdates) lookupWithoutRewards(rnd basics.Round, addr basics.Add // we don't technically need this, since it's already in the baseAccounts, however, writing this over // would ensure that we promote this field. au.baseAccounts.writePending(macct) - return macct.accountData.GetLedgerCoreAccountData(), rnd, rewardsVersion, rewardsLevel, nil + return macct.AccountData.GetLedgerCoreAccountData(), rnd, rewardsVersion, rewardsLevel, nil } // check baseAccoiunts again to see if it does not exist @@ -1463,24 +1472,24 @@ func (au *accountUpdates) lookupWithoutRewards(rnd basics.Round, addr basics.Add // 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. - persistedData, err = au.accountsq.lookup(addr) + persistedData, err = au.accountsq.LookupAccount(addr) if err != nil { return ledgercore.AccountData{}, basics.Round(0), "", 0, err } - if persistedData.round == currentDbRound { - if persistedData.rowid != 0 { + if persistedData.Round == currentDbRound { + if persistedData.Rowid != 0 { // if we read actual data return it au.baseAccounts.writePending(persistedData) - return persistedData.accountData.GetLedgerCoreAccountData(), rnd, rewardsVersion, rewardsLevel, nil + return persistedData.AccountData.GetLedgerCoreAccountData(), rnd, rewardsVersion, rewardsLevel, nil } au.baseAccounts.writeNotFoundPending(addr) // otherwise return empty return ledgercore.AccountData{}, rnd, rewardsVersion, rewardsLevel, nil } if synchronized { - if persistedData.round < currentDbRound { - au.log.Errorf("accountUpdates.lookupWithoutRewards: database round %d is behind in-memory round %d", persistedData.round, currentDbRound) - return ledgercore.AccountData{}, basics.Round(0), "", 0, &StaleDatabaseRoundError{databaseRound: persistedData.round, memoryRound: currentDbRound} + if persistedData.Round < currentDbRound { + au.log.Errorf("accountUpdates.lookupWithoutRewards: database round %d is behind in-memory round %d", persistedData.Round, currentDbRound) + return ledgercore.AccountData{}, basics.Round(0), "", 0, &StaleDatabaseRoundError{databaseRound: persistedData.Round, memoryRound: currentDbRound} } au.accountsMu.RLock() needUnlock = true @@ -1489,8 +1498,8 @@ func (au *accountUpdates) lookupWithoutRewards(rnd basics.Round, addr basics.Add } } else { // in non-sync mode, we don't wait since we already assume that we're synchronized. - au.log.Errorf("accountUpdates.lookupWithoutRewards: database round %d mismatching in-memory round %d", persistedData.round, currentDbRound) - return ledgercore.AccountData{}, basics.Round(0), "", 0, &MismatchingDatabaseRoundError{databaseRound: persistedData.round, memoryRound: currentDbRound} + au.log.Errorf("accountUpdates.lookupWithoutRewards: database round %d mismatching in-memory round %d", persistedData.Round, currentDbRound) + return ledgercore.AccountData{}, basics.Round(0), "", 0, &MismatchingDatabaseRoundError{databaseRound: persistedData.Round, memoryRound: currentDbRound} } } } @@ -1531,7 +1540,7 @@ func (au *accountUpdates) getCreatorForRound(rnd basics.Round, cidx basics.Creat } else { for offset > 0 { offset-- - creatableDelta, ok := au.creatableDeltas[offset][cidx] + creatableDelta, ok := au.deltas[offset].Creatables[cidx] if ok { if creatableDelta.Created && creatableDelta.Ctype == ctype { return creatableDelta.Creator, true, nil @@ -1546,7 +1555,7 @@ func (au *accountUpdates) getCreatorForRound(rnd basics.Round, cidx basics.Creat unlock = false } // Check the database - creator, ok, dbRound, err = au.accountsq.lookupCreator(cidx, ctype) + creator, ok, dbRound, err = au.accountsq.LookupCreator(cidx, ctype) if err != nil { return basics.Address{}, false, err } @@ -1631,8 +1640,8 @@ func (au *accountUpdates) prepareCommit(dcc *deferredCommitContext) error { // being updated multiple times. When that happen, we can safely omit the intermediate updates. dcc.compactAccountDeltas = makeCompactAccountDeltas(au.deltas[:offset], dcc.oldBase, setUpdateRound, au.baseAccounts) dcc.compactResourcesDeltas = makeCompactResourceDeltas(au.deltas[:offset], dcc.oldBase, setUpdateRound, au.baseAccounts, au.baseResources) - dcc.compactKvDeltas = compactKvDeltas(au.kvDeltas[:offset]) - dcc.compactCreatableDeltas = compactCreatableDeltas(au.creatableDeltas[:offset]) + dcc.compactKvDeltas = compactKvDeltas(au.deltas[:offset]) + dcc.compactCreatableDeltas = compactCreatableDeltas(au.deltas[:offset]) au.accountsMu.RUnlock() @@ -1675,7 +1684,7 @@ func (au *accountUpdates) commitRound(ctx context.Context, tx *sql.Tx, dcc *defe knownAddresses := make(map[basics.Address]int64, len(dcc.compactAccountDeltas.deltas)) for _, delta := range dcc.compactAccountDeltas.deltas { - knownAddresses[delta.oldAcct.addr] = delta.oldAcct.rowid + knownAddresses[delta.oldAcct.Addr] = delta.oldAcct.Rowid } err = dcc.compactResourcesDeltas.resourcesLoadOld(tx, knownAddresses) @@ -1687,7 +1696,9 @@ func (au *accountUpdates) commitRound(ctx context.Context, tx *sql.Tx, dcc *defe dcc.stats.OldAccountPreloadDuration = time.Duration(time.Now().UnixNano()) - dcc.stats.OldAccountPreloadDuration } - err = accountsPutTotals(tx, dcc.roundTotals, false) + arw := store.NewAccountsSQLReaderWriter(tx) + + err = arw.AccountsPutTotals(dcc.roundTotals, false) if err != nil { return err } @@ -1750,14 +1761,14 @@ func (au *accountUpdates) postCommit(ctx context.Context, dcc *deferredCommitCon for i := 0; i < dcc.compactResourcesDeltas.len(); i++ { resUpdate := dcc.compactResourcesDeltas.getByIdx(i) cnt := resUpdate.nAcctDeltas - key := accountCreatable{resUpdate.address, resUpdate.oldResource.aidx} + key := accountCreatable{resUpdate.address, resUpdate.oldResource.Aidx} macct, ok := au.resources[key] if !ok { - au.log.Panicf("inconsistency: flushed %d changes to (%s, %d), but not in au.resources", cnt, resUpdate.address, resUpdate.oldResource.aidx) + au.log.Panicf("inconsistency: flushed %d changes to (%s, %d), but not in au.resources", cnt, resUpdate.address, resUpdate.oldResource.Aidx) } if cnt > macct.ndeltas { - au.log.Panicf("inconsistency: flushed %d changes to (%s, %d), but au.resources had %d", cnt, resUpdate.address, resUpdate.oldResource.aidx, macct.ndeltas) + au.log.Panicf("inconsistency: flushed %d changes to (%s, %d), but au.resources had %d", cnt, resUpdate.address, resUpdate.oldResource.Aidx, macct.ndeltas) } else if cnt == macct.ndeltas { delete(au.resources, key) } else { @@ -1821,8 +1832,7 @@ func (au *accountUpdates) postCommit(ctx context.Context, dcc *deferredCommitCon const deltasClearThreshold = 500 if offset > deltasClearThreshold { for i := uint64(0); i < offset; i++ { - au.deltas[i] = ledgercore.AccountDeltas{} - au.creatableDeltas[i] = nil + au.deltas[i] = ledgercore.StateDelta{} } } @@ -1830,8 +1840,6 @@ func (au *accountUpdates) postCommit(ctx context.Context, dcc *deferredCommitCon au.deltasAccum = au.deltasAccum[offset:] au.versions = au.versions[offset:] au.roundTotals = au.roundTotals[offset:] - au.kvDeltas = au.kvDeltas[offset:] - au.creatableDeltas = au.creatableDeltas[offset:] au.cachedDBRound = newBase au.accountsMu.Unlock() @@ -1862,18 +1870,19 @@ func (au *accountUpdates) postCommit(ctx context.Context, dcc *deferredCommitCon func (au *accountUpdates) postCommitUnlocked(ctx context.Context, dcc *deferredCommitContext) { } -// compactKvDeltas takes an array of kv deltas (one array entry per round), and +// compactKvDeltas takes an array of StateDeltas containing kv deltas (one array entry per round), and // compacts the array into a single map that contains all the // changes. Intermediate changes are eliminated. It counts the number of // changes per round by specifying it in the ndeltas field of the // modifiedKv. The modifiedValues in the returned map have the earliest // mv.oldData, and the newest mv.data. -func compactKvDeltas(kvDeltas []map[string]ledgercore.KvValueDelta) map[string]modifiedKvValue { - if len(kvDeltas) == 0 { +func compactKvDeltas(stateDeltas []ledgercore.StateDelta) map[string]modifiedKvValue { + if len(stateDeltas) == 0 { return nil } outKvDeltas := make(map[string]modifiedKvValue) - for _, roundKv := range kvDeltas { + for _, stateDelta := range stateDeltas { + roundKv := stateDelta.KvMods for key, current := range roundKv { prev, ok := outKvDeltas[key] if !ok { // Record only the first OldData @@ -1887,16 +1896,18 @@ func compactKvDeltas(kvDeltas []map[string]ledgercore.KvValueDelta) map[string]m return outKvDeltas } -// compactCreatableDeltas takes an array of creatables map deltas ( one array entry per round ), and compact the array into a single -// map that contains all the deltas changes. While doing that, the function eliminate any intermediate changes. +// compactCreatableDeltas takes an array of StateDeltas containing creatables map deltas ( one array entry per round ), +// and compacts the array into a single map that of createable deltas which contains all the deltas changes. +// While doing that, the function eliminate any intermediate changes. // It counts the number of changes per round by specifying it in the ndeltas field of the modifiedCreatable. -func compactCreatableDeltas(creatableDeltas []map[basics.CreatableIndex]ledgercore.ModifiedCreatable) (outCreatableDeltas map[basics.CreatableIndex]ledgercore.ModifiedCreatable) { - if len(creatableDeltas) == 0 { +func compactCreatableDeltas(stateDeltas []ledgercore.StateDelta) (outCreatableDeltas map[basics.CreatableIndex]ledgercore.ModifiedCreatable) { + if len(stateDeltas) == 0 { return } // the sizes of the maps here aren't super accurate, but would hopefully be a rough estimate for a reasonable starting point. - outCreatableDeltas = make(map[basics.CreatableIndex]ledgercore.ModifiedCreatable, 1+len(creatableDeltas[0])*len(creatableDeltas)) - for _, roundCreatable := range creatableDeltas { + outCreatableDeltas = make(map[basics.CreatableIndex]ledgercore.ModifiedCreatable, 1+len(stateDeltas[0].KvMods)*len(stateDeltas)) + for _, stateDelta := range stateDeltas { + roundCreatable := stateDelta.Creatables for creatableIdx, creatable := range roundCreatable { if prev, has := outCreatableDeltas[creatableIdx]; has { outCreatableDeltas[creatableIdx] = ledgercore.ModifiedCreatable{ diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index a19217fed4..7329c61e12 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -38,6 +38,8 @@ import ( "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger/internal" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" + storetesting "github.com/algorand/go-algorand/ledger/store/testing" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -76,8 +78,23 @@ func accumulateTotals(t testing.TB, consensusVersion protocol.ConsensusVersion, return } +func setupAccts(niter int) []map[basics.Address]basics.AccountData { + accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(niter, true)} + pooldata := basics.AccountData{} + pooldata.MicroAlgos.Raw = 100 * 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 + + return accts +} + func makeMockLedgerForTrackerWithLogger(t testing.TB, inMemory bool, initialBlocksCount int, consensusVersion protocol.ConsensusVersion, accts []map[basics.Address]basics.AccountData, l logging.Logger) *mockLedgerForTracker { - dbs, fileName := dbOpenTest(t, inMemory) + dbs, fileName := storetesting.DbOpenTest(t, inMemory) dbs.Rdb.SetLogger(l) dbs.Wdb.SetLogger(l) @@ -258,7 +275,7 @@ func (au *accountUpdates) allBalances(rnd basics.Round) (bals map[basics.Address for offset := uint64(0); offset < offsetLimit; offset++ { deltas := au.deltas[offset] - bals = ledgercore.AccumulateDeltas(bals, deltas) + bals = ledgercore.AccumulateDeltas(bals, deltas.Accts) } return } @@ -360,6 +377,10 @@ func checkAcctUpdates(t *testing.T, au *accountUpdates, ao *onlineAccounts, base require.NoError(t, err) require.Equal(t, totals.Raw, totalOnline) + auTotals, err := au.onlineTotals(rnd) + require.NoError(t, err) + require.Equal(t, totals.Raw, auTotals.Raw) + d, validThrough, err := au.LookupWithoutRewards(rnd, ledgertesting.RandomAddress()) require.NoError(t, err) require.GreaterOrEqualf(t, uint64(validThrough), uint64(rnd), fmt.Sprintf("validThrough :%v\nrnd :%v\n", validThrough, rnd)) @@ -377,7 +398,8 @@ func checkAcctUpdatesConsistency(t *testing.T, au *accountUpdates, rnd basics.Ro accounts := make(map[basics.Address]modifiedAccount) resources := make(resourcesUpdates) - for _, rdelta := range au.deltas { + for _, sdelta := range au.deltas { + rdelta := sdelta.Accts for i := 0; i < rdelta.Len(); i++ { addr, adelta := rdelta.GetByIdx(i) macct := accounts[addr] @@ -407,7 +429,7 @@ func checkAcctUpdatesConsistency(t *testing.T, au *accountUpdates, rnd basics.Ro require.Equal(t, au.accounts, accounts) require.Equal(t, au.resources, resources) - latest := au.deltas[len(au.deltas)-1] + latest := au.deltas[len(au.deltas)-1].Accts for i := 0; i < latest.Len(); i++ { addr, acct := latest.GetByIdx(i) d, r, withoutRewards, err := au.lookupLatest(addr) @@ -472,19 +494,8 @@ func TestAcctUpdates(t *testing.T) { conf.MaxAcctLookback = lookback - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(20, true)} + accts := setupAccts(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 - initialBlocksCount := int(lookback) ml := makeMockLedgerForTracker(t, true, initialBlocksCount, protocol.ConsensusCurrentVersion, accts) defer ml.Close() @@ -562,7 +573,8 @@ func TestAcctUpdates(t *testing.T) { // check the account totals. var dbRound basics.Round err := ml.dbs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - dbRound, err = accountsRound(tx) + arw := store.NewAccountsSQLReaderWriter(tx) + dbRound, err = arw.AccountsRound() return }) require.NoError(t, err) @@ -575,7 +587,8 @@ func TestAcctUpdates(t *testing.T) { expectedTotals := ledgertesting.CalculateNewRoundAccountTotals(t, updates, rewardsLevels[dbRound], proto, nil, ledgercore.AccountTotals{}) var actualTotals ledgercore.AccountTotals err = ml.dbs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - actualTotals, err = accountsTotals(ctx, tx, false) + arw := store.NewAccountsSQLReaderWriter(tx) + actualTotals, err = arw.AccountsTotals(ctx, false) return }) require.NoError(t, err) @@ -593,19 +606,9 @@ func TestAcctUpdatesFastUpdates(t *testing.T) { } proto := config.Consensus[protocol.ConsensusCurrentVersion] - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(20, true)} + accts := setupAccts(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 - conf := config.GetDefaultLocal() conf.CatchpointInterval = 1 initialBlocksCount := int(conf.MaxAcctLookback) @@ -680,21 +683,10 @@ func BenchmarkBalancesChanges(b *testing.B) { protocolVersion := protocol.ConsensusCurrentVersion initialRounds := uint64(1) - accountsCount := 5000 - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(accountsCount, true)} + accts := setupAccts(accountsCount) rewardsLevels := []uint64{0} - pooldata := basics.AccountData{} - pooldata.MicroAlgos.Raw = 1000 * 1000 * 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 - ml := makeMockLedgerForTracker(b, true, int(initialRounds), protocolVersion, accts) defer ml.Close() @@ -771,26 +763,26 @@ func BenchmarkBalancesChanges(b *testing.B) { func BenchmarkCalibrateNodesPerPage(b *testing.B) { b.Skip("This benchmark was used to tune up the NodesPerPage; it's not really useful otherwise") - defaultNodesPerPage := merkleCommitterNodesPerPage + defaultNodesPerPage := store.MerkleCommitterNodesPerPage for nodesPerPage := 32; nodesPerPage < 300; nodesPerPage++ { b.Run(fmt.Sprintf("Test_merkleCommitterNodesPerPage_%d", nodesPerPage), func(b *testing.B) { - merkleCommitterNodesPerPage = int64(nodesPerPage) + store.MerkleCommitterNodesPerPage = int64(nodesPerPage) BenchmarkBalancesChanges(b) }) } - merkleCommitterNodesPerPage = defaultNodesPerPage + store.MerkleCommitterNodesPerPage = defaultNodesPerPage } func BenchmarkCalibrateCacheNodeSize(b *testing.B) { - //b.Skip("This benchmark was used to tune up the trieCachedNodesCount; it's not really useful otherwise") - defaultTrieCachedNodesCount := trieCachedNodesCount + //b.Skip("This benchmark was used to tune up the TrieCachedNodesCount; it's not really useful otherwise") + defaultTrieCachedNodesCount := store.TrieCachedNodesCount for cacheSize := 3000; cacheSize < 50000; cacheSize += 1000 { b.Run(fmt.Sprintf("Test_cacheSize_%d", cacheSize), func(b *testing.B) { - trieCachedNodesCount = cacheSize + store.TrieCachedNodesCount = cacheSize BenchmarkBalancesChanges(b) }) } - trieCachedNodesCount = defaultTrieCachedNodesCount + store.TrieCachedNodesCount = defaultTrieCachedNodesCount } // TestLargeAccountCountCatchpointGeneration creates a ledger containing a large set of accounts ( i.e. 100K accounts ) @@ -818,22 +810,11 @@ func TestLargeAccountCountCatchpointGeneration(t *testing.T) { config.Consensus[testProtocolVersion] = protoParams defer func() { delete(config.Consensus, testProtocolVersion) - os.RemoveAll(CatchpointDirName) + os.RemoveAll(store.CatchpointDirName) }() - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(100000, true)} + accts := setupAccts(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 - conf := config.GetDefaultLocal() conf.CatchpointInterval = 1 conf.Archival = true @@ -912,18 +893,7 @@ func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { inMemory := true testFunction := func(t *testing.T) { - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(9, true)} - - 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 - + accts := setupAccts(9) ml := makeMockLedgerForTracker(t, inMemory, 10, testProtocolVersion, accts) defer ml.Close() @@ -1180,8 +1150,8 @@ func TestListCreatables(t *testing.T) { numElementsPerSegement := 25 // set up the database - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() @@ -1191,14 +1161,11 @@ func TestListCreatables(t *testing.T) { proto := config.Consensus[protocol.ConsensusCurrentVersion] accts := make(map[basics.Address]basics.AccountData) - _ = accountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) - require.NoError(t, err) - - err = accountsAddNormalizedBalance(tx, proto) + _ = store.AccountsInitTest(t, tx, accts, protocol.ConsensusCurrentVersion) require.NoError(t, err) au := &accountUpdates{} - au.accountsq, err = accountsInitDbQueries(tx) + au.accountsq, err = store.AccountsInitDbQueries(tx) require.NoError(t, err) // ******* All results are obtained from the cache. Empty database ******* @@ -1435,7 +1402,7 @@ func TestKVCache(t *testing.T) { name := fmt.Sprintf("%d", uint64(startKV)+uint64(j)) persistedValue, has := au.baseKVs.read(name) require.True(t, has) - require.Equal(t, kvMap[name], persistedValue.value) + require.Equal(t, kvMap[name], persistedValue.Value) } } } @@ -1468,7 +1435,7 @@ func TestKVCache(t *testing.T) { for name := range kvMods { persistedValue, has := au.baseKVs.read(name) require.True(t, has) - require.Equal(t, kvMap[name], persistedValue.value) + require.Equal(t, kvMap[name], persistedValue.Value) } } @@ -1485,7 +1452,7 @@ func TestKVCache(t *testing.T) { persistedValue, has := au.baseKVs.read(name) require.True(t, has) expectedValue := fmt.Sprintf("modified value%s", name) - require.Equal(t, expectedValue, string(persistedValue.value)) + require.Equal(t, expectedValue, string(persistedValue.Value)) } } } @@ -1520,7 +1487,7 @@ func TestKVCache(t *testing.T) { persistedValue, has := au.baseKVs.read(name) require.True(t, has) value := fmt.Sprintf("modified value%s", name) - require.Equal(t, value, string(persistedValue.value)) + require.Equal(t, value, string(persistedValue.Value)) } } @@ -1536,13 +1503,15 @@ func TestKVCache(t *testing.T) { name := fmt.Sprintf("%d", uint64(startKV)+uint64(j)) persistedValue, has := au.baseKVs.read(name) require.True(t, has) - require.True(t, persistedValue.value == nil) + require.True(t, persistedValue.Value == nil) } } } } func accountsAll(tx *sql.Tx) (bals map[basics.Address]basics.AccountData, err error) { + arw := store.NewAccountsSQLReaderWriter(tx) + rows, err := tx.Query("SELECT rowid, address, data FROM accountbase") if err != nil { return @@ -1559,7 +1528,7 @@ func accountsAll(tx *sql.Tx) (bals map[basics.Address]basics.AccountData, err er return } - var data baseAccountData + var data store.BaseAccountData err = protocol.Decode(buf, &data) if err != nil { return @@ -1573,7 +1542,7 @@ func accountsAll(tx *sql.Tx) (bals map[basics.Address]basics.AccountData, err er copy(addr[:], addrbuf) var ad basics.AccountData - ad, err = loadFullAccount(context.Background(), tx, "resources", addr, rowid.Int64, data) + ad, err = arw.LoadFullAccount(context.Background(), "resources", addr, rowid.Int64, data) if err != nil { return } @@ -1588,18 +1557,7 @@ func accountsAll(tx *sql.Tx) (bals map[basics.Address]basics.AccountData, err er func BenchmarkLargeMerkleTrieRebuild(b *testing.B) { proto := config.Consensus[protocol.ConsensusCurrentVersion] - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(5, true)} - - 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 - + accts := setupAccts(5) ml := makeMockLedgerForTracker(b, true, 10, protocol.ConsensusCurrentVersion, accts) defer ml.Close() @@ -1614,7 +1572,7 @@ func BenchmarkLargeMerkleTrieRebuild(b *testing.B) { var updates compactAccountDeltas for k := 0; i < accountsNumber-5-2 && k < 1024; k++ { addr := ledgertesting.RandomAddress() - acctData := baseAccountData{} + acctData := store.BaseAccountData{} acctData.MicroAlgos.Raw = 1 updates.upsert(addr, accountDelta{newAcct: acctData}) i++ @@ -1628,7 +1586,8 @@ func BenchmarkLargeMerkleTrieRebuild(b *testing.B) { } err := ml.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - return updateAccountsHashRound(ctx, tx, 1) + arw := store.NewAccountsSQLReaderWriter(tx) + return arw.UpdateAccountsHashRound(ctx, 1) }) require.NoError(b, err) @@ -1647,7 +1606,7 @@ func BenchmarkCompactDeltas(b *testing.B) { b.N = 500 } window := 5000 - accountDeltas := make([]ledgercore.AccountDeltas, b.N) + stateDeltas := make([]ledgercore.StateDelta, b.N) addrs := make([]basics.Address, b.N*window) for i := 0; i < len(addrs); i++ { addrs[i] = basics.Address(crypto.Hash([]byte{byte(i % 256), byte((i / 256) % 256), byte(i / 65536)})) @@ -1659,7 +1618,7 @@ func BenchmarkCompactDeltas(b *testing.B) { start = window/2 + (rnd-1)*window } for k := start; k < start+window; k++ { - accountDeltas[rnd].Upsert(addrs[k], ledgercore.AccountData{}) + stateDeltas[rnd].Accts.Upsert(addrs[k], ledgercore.AccountData{}) m[addrs[k]] = basics.AccountData{} } } @@ -1667,7 +1626,7 @@ func BenchmarkCompactDeltas(b *testing.B) { baseAccounts.init(nil, 100, 80) b.ResetTimer() - makeCompactAccountDeltas(accountDeltas, 0, false, baseAccounts) + makeCompactAccountDeltas(stateDeltas, 0, false, baseAccounts) }) } @@ -1679,59 +1638,57 @@ func TestCompactDeltas(t *testing.T) { addrs[i] = basics.Address(crypto.Hash([]byte{byte(i % 256), byte((i / 256) % 256), byte(i / 65536)})) } - accountDeltas := make([]ledgercore.AccountDeltas, 1) - creatableDeltas := make([]map[basics.CreatableIndex]ledgercore.ModifiedCreatable, 1) - creatableDeltas[0] = make(map[basics.CreatableIndex]ledgercore.ModifiedCreatable) - accountDeltas[0].Upsert(addrs[0], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 2}}}) - creatableDeltas[0][100] = ledgercore.ModifiedCreatable{Creator: addrs[2], Created: true} + stateDeltas := make([]ledgercore.StateDelta, 1) + stateDeltas[0].Creatables = make(map[basics.CreatableIndex]ledgercore.ModifiedCreatable) + stateDeltas[0].Accts.Upsert(addrs[0], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 2}}}) + stateDeltas[0].Creatables[100] = ledgercore.ModifiedCreatable{Creator: addrs[2], Created: true} var baseAccounts lruAccounts baseAccounts.init(nil, 100, 80) - outAccountDeltas := makeCompactAccountDeltas(accountDeltas, basics.Round(1), true, baseAccounts) - outCreatableDeltas := compactCreatableDeltas(creatableDeltas) + outAccountDeltas := makeCompactAccountDeltas(stateDeltas, basics.Round(1), true, baseAccounts) + outCreatableDeltas := compactCreatableDeltas(stateDeltas) - require.Equal(t, accountDeltas[0].Len(), outAccountDeltas.len()) - require.Equal(t, len(creatableDeltas[0]), len(outCreatableDeltas)) - require.Equal(t, accountDeltas[0].Len(), len(outAccountDeltas.misses)) + require.Equal(t, stateDeltas[0].Accts.Len(), outAccountDeltas.len()) + require.Equal(t, len(stateDeltas[0].Creatables), len(outCreatableDeltas)) + require.Equal(t, stateDeltas[0].Accts.Len(), len(outAccountDeltas.misses)) // check deltas with missing accounts delta, _ := outAccountDeltas.get(addrs[0]) - require.Equal(t, persistedAccountData{}, delta.oldAcct) + require.Equal(t, store.PersistedAccountData{}, delta.oldAcct) require.NotEmpty(t, delta.newAcct) require.Equal(t, ledgercore.ModifiedCreatable{Creator: addrs[2], Created: true, Ndeltas: 1}, outCreatableDeltas[100]) // check deltas without missing accounts - baseAccounts.write(persistedAccountData{addr: addrs[0], accountData: baseAccountData{}}) - outAccountDeltas = makeCompactAccountDeltas(accountDeltas, basics.Round(1), true, baseAccounts) + baseAccounts.write(store.PersistedAccountData{Addr: addrs[0], AccountData: store.BaseAccountData{}}) + outAccountDeltas = makeCompactAccountDeltas(stateDeltas, basics.Round(1), true, baseAccounts) require.Equal(t, 0, len(outAccountDeltas.misses)) delta, _ = outAccountDeltas.get(addrs[0]) - require.Equal(t, persistedAccountData{addr: addrs[0]}, delta.oldAcct) - require.Equal(t, baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 2}, UpdateRound: 2}, delta.newAcct) + require.Equal(t, store.PersistedAccountData{Addr: addrs[0]}, delta.oldAcct) + require.Equal(t, store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 2}, UpdateRound: 2}, delta.newAcct) require.Equal(t, ledgercore.ModifiedCreatable{Creator: addrs[2], Created: true, Ndeltas: 1}, outCreatableDeltas[100]) baseAccounts.init(nil, 100, 80) // add another round - accountDeltas = append(accountDeltas, ledgercore.AccountDeltas{}) - creatableDeltas = append(creatableDeltas, make(map[basics.CreatableIndex]ledgercore.ModifiedCreatable)) - accountDeltas[1].Upsert(addrs[0], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 3}}}) - accountDeltas[1].Upsert(addrs[3], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 8}}}) + stateDeltas = append(stateDeltas, ledgercore.StateDelta{Creatables: make(map[basics.CreatableIndex]ledgercore.ModifiedCreatable)}) + stateDeltas[1].Accts.Upsert(addrs[0], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 3}}}) + stateDeltas[1].Accts.Upsert(addrs[3], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 8}}}) - creatableDeltas[1][100] = ledgercore.ModifiedCreatable{Creator: addrs[2], Created: false} - creatableDeltas[1][101] = ledgercore.ModifiedCreatable{Creator: addrs[4], Created: true} + stateDeltas[1].Creatables[100] = ledgercore.ModifiedCreatable{Creator: addrs[2], Created: false} + stateDeltas[1].Creatables[101] = ledgercore.ModifiedCreatable{Creator: addrs[4], Created: true} - baseAccounts.write(persistedAccountData{addr: addrs[0], accountData: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 1}}}) - baseAccounts.write(persistedAccountData{addr: addrs[3], accountData: baseAccountData{}}) - outAccountDeltas = makeCompactAccountDeltas(accountDeltas, basics.Round(1), true, baseAccounts) - outCreatableDeltas = compactCreatableDeltas(creatableDeltas) + baseAccounts.write(store.PersistedAccountData{Addr: addrs[0], AccountData: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: 1}}}) + baseAccounts.write(store.PersistedAccountData{Addr: addrs[3], AccountData: store.BaseAccountData{}}) + outAccountDeltas = makeCompactAccountDeltas(stateDeltas, basics.Round(1), true, baseAccounts) + outCreatableDeltas = compactCreatableDeltas(stateDeltas) require.Equal(t, 2, outAccountDeltas.len()) require.Equal(t, 2, len(outCreatableDeltas)) delta, _ = outAccountDeltas.get(addrs[0]) - require.Equal(t, uint64(1), delta.oldAcct.accountData.MicroAlgos.Raw) + require.Equal(t, uint64(1), delta.oldAcct.AccountData.MicroAlgos.Raw) require.Equal(t, uint64(3), delta.newAcct.MicroAlgos.Raw) require.Equal(t, int(2), delta.nAcctDeltas) delta, _ = outAccountDeltas.get(addrs[3]) - require.Equal(t, uint64(0), delta.oldAcct.accountData.MicroAlgos.Raw) + require.Equal(t, uint64(0), delta.oldAcct.AccountData.MicroAlgos.Raw) require.Equal(t, uint64(8), delta.newAcct.MicroAlgos.Raw) require.Equal(t, int(1), delta.nAcctDeltas) @@ -1756,60 +1713,60 @@ func TestCompactDeltasResources(t *testing.T) { baseResources.init(nil, 100, 80) // check empty deltas do no produce empty resourcesData records - accountDeltas := make([]ledgercore.AccountDeltas, 1) - accountDeltas[0].UpsertAppResource(addrs[0], 100, ledgercore.AppParamsDelta{Deleted: true}, ledgercore.AppLocalStateDelta{}) - accountDeltas[0].UpsertAppResource(addrs[1], 101, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{Deleted: true}) - accountDeltas[0].UpsertAssetResource(addrs[2], 102, ledgercore.AssetParamsDelta{Deleted: true}, ledgercore.AssetHoldingDelta{}) - accountDeltas[0].UpsertAssetResource(addrs[3], 103, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Deleted: true}) + stateDeltas := make([]ledgercore.StateDelta, 1) + stateDeltas[0].Accts.UpsertAppResource(addrs[0], 100, ledgercore.AppParamsDelta{Deleted: true}, ledgercore.AppLocalStateDelta{}) + stateDeltas[0].Accts.UpsertAppResource(addrs[1], 101, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{Deleted: true}) + stateDeltas[0].Accts.UpsertAssetResource(addrs[2], 102, ledgercore.AssetParamsDelta{Deleted: true}, ledgercore.AssetHoldingDelta{}) + stateDeltas[0].Accts.UpsertAssetResource(addrs[3], 103, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Deleted: true}) - outResourcesDeltas := makeCompactResourceDeltas(accountDeltas, basics.Round(1), true, baseAccounts, baseResources) + outResourcesDeltas := makeCompactResourceDeltas(stateDeltas, basics.Round(1), true, baseAccounts, baseResources) delta, _ := outResourcesDeltas.get(addrs[0], 100) require.NotEmpty(t, delta.newResource) require.True(t, !delta.newResource.IsApp() && !delta.newResource.IsAsset()) - require.Equal(t, resourceFlagsNotHolding, delta.newResource.ResourceFlags) + require.Equal(t, store.ResourceFlagsNotHolding, delta.newResource.ResourceFlags) delta, _ = outResourcesDeltas.get(addrs[1], 101) require.NotEmpty(t, delta.newResource) require.True(t, !delta.newResource.IsApp() && !delta.newResource.IsAsset()) - require.Equal(t, resourceFlagsNotHolding, delta.newResource.ResourceFlags) + require.Equal(t, store.ResourceFlagsNotHolding, delta.newResource.ResourceFlags) delta, _ = outResourcesDeltas.get(addrs[2], 102) require.NotEmpty(t, delta.newResource) require.True(t, !delta.newResource.IsApp() && !delta.newResource.IsAsset()) - require.Equal(t, resourceFlagsNotHolding, delta.newResource.ResourceFlags) + require.Equal(t, store.ResourceFlagsNotHolding, delta.newResource.ResourceFlags) delta, _ = outResourcesDeltas.get(addrs[3], 103) require.NotEmpty(t, delta.newResource) require.True(t, !delta.newResource.IsApp() && !delta.newResource.IsAsset()) - require.Equal(t, resourceFlagsNotHolding, delta.newResource.ResourceFlags) + require.Equal(t, store.ResourceFlagsNotHolding, delta.newResource.ResourceFlags) // check actual data on non-empty input - accountDeltas = make([]ledgercore.AccountDeltas, 1) + stateDeltas = make([]ledgercore.StateDelta, 1) // addr 0 has app params and a local state for another app appParams100 := basics.AppParams{ApprovalProgram: []byte{100}} appLocalState200 := basics.AppLocalState{KeyValue: basics.TealKeyValue{"200": basics.TealValue{Type: basics.TealBytesType, Bytes: "200"}}} - accountDeltas[0].UpsertAppResource(addrs[0], 100, ledgercore.AppParamsDelta{Params: &appParams100}, ledgercore.AppLocalStateDelta{}) - accountDeltas[0].UpsertAppResource(addrs[0], 200, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{LocalState: &appLocalState200}) + stateDeltas[0].Accts.UpsertAppResource(addrs[0], 100, ledgercore.AppParamsDelta{Params: &appParams100}, ledgercore.AppLocalStateDelta{}) + stateDeltas[0].Accts.UpsertAppResource(addrs[0], 200, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{LocalState: &appLocalState200}) // addr 1 has app params and a local state for the same app appParams101 := basics.AppParams{ApprovalProgram: []byte{101}} appLocalState101 := basics.AppLocalState{KeyValue: basics.TealKeyValue{"101": basics.TealValue{Type: basics.TealBytesType, Bytes: "101"}}} - accountDeltas[0].UpsertAppResource(addrs[1], 101, ledgercore.AppParamsDelta{Params: &appParams101}, ledgercore.AppLocalStateDelta{LocalState: &appLocalState101}) + stateDeltas[0].Accts.UpsertAppResource(addrs[1], 101, ledgercore.AppParamsDelta{Params: &appParams101}, ledgercore.AppLocalStateDelta{LocalState: &appLocalState101}) // addr 2 has asset params and a holding for another asset assetParams102 := basics.AssetParams{Total: 102} assetHolding202 := basics.AssetHolding{Amount: 202} - accountDeltas[0].UpsertAssetResource(addrs[2], 102, ledgercore.AssetParamsDelta{Params: &assetParams102}, ledgercore.AssetHoldingDelta{}) - accountDeltas[0].UpsertAssetResource(addrs[2], 202, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &assetHolding202}) + stateDeltas[0].Accts.UpsertAssetResource(addrs[2], 102, ledgercore.AssetParamsDelta{Params: &assetParams102}, ledgercore.AssetHoldingDelta{}) + stateDeltas[0].Accts.UpsertAssetResource(addrs[2], 202, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &assetHolding202}) // addr 3 has asset params and a holding for the same asset assetParams103 := basics.AssetParams{Total: 103} assetHolding103 := basics.AssetHolding{Amount: 103} - accountDeltas[0].UpsertAssetResource(addrs[3], 103, ledgercore.AssetParamsDelta{Params: &assetParams103}, ledgercore.AssetHoldingDelta{Holding: &assetHolding103}) + stateDeltas[0].Accts.UpsertAssetResource(addrs[3], 103, ledgercore.AssetParamsDelta{Params: &assetParams103}, ledgercore.AssetHoldingDelta{Holding: &assetHolding103}) baseResources.init(nil, 100, 80) - outResourcesDeltas = makeCompactResourceDeltas(accountDeltas, basics.Round(1), true, baseAccounts, baseResources) + outResourcesDeltas = makeCompactResourceDeltas(stateDeltas, basics.Round(1), true, baseAccounts, baseResources) // 6 entries are missing: same app (asset) params and local state are combined into a single entry require.Equal(t, 6, len(outResourcesDeltas.misses)) require.Equal(t, 6, len(outResourcesDeltas.deltas)) @@ -1853,23 +1810,23 @@ func TestCompactDeltasResources(t *testing.T) { for i := int64(0); i < 4; i++ { delta, idx := outResourcesDeltas.get(addrs[i], basics.CreatableIndex(100+i)) require.NotEqual(t, -1, idx) - require.Equal(t, persistedResourcesData{aidx: basics.CreatableIndex(100 + i)}, delta.oldResource) + require.Equal(t, store.PersistedResourcesData{Aidx: basics.CreatableIndex(100 + i)}, delta.oldResource) if i%2 == 0 { delta, idx = outResourcesDeltas.get(addrs[i], basics.CreatableIndex(200+i)) require.NotEqual(t, -1, idx) - require.Equal(t, persistedResourcesData{aidx: basics.CreatableIndex(200 + i)}, delta.oldResource) + require.Equal(t, store.PersistedResourcesData{Aidx: basics.CreatableIndex(200 + i)}, delta.oldResource) } } // check deltas without missing accounts for i := int64(0); i < 4; i++ { - baseResources.write(persistedResourcesData{addrid: i + 1, aidx: basics.CreatableIndex(100 + i)}, addrs[i]) + baseResources.write(store.PersistedResourcesData{Addrid: i + 1, Aidx: basics.CreatableIndex(100 + i)}, addrs[i]) if i%2 == 0 { - baseResources.write(persistedResourcesData{addrid: i + 1, aidx: basics.CreatableIndex(200 + i)}, addrs[i]) + baseResources.write(store.PersistedResourcesData{Addrid: i + 1, Aidx: basics.CreatableIndex(200 + i)}, addrs[i]) } } - outResourcesDeltas = makeCompactResourceDeltas(accountDeltas, basics.Round(1), true, baseAccounts, baseResources) + outResourcesDeltas = makeCompactResourceDeltas(stateDeltas, basics.Round(1), true, baseAccounts, baseResources) require.Equal(t, 0, len(outResourcesDeltas.misses)) require.Equal(t, 6, len(outResourcesDeltas.deltas)) @@ -1877,28 +1834,28 @@ func TestCompactDeltasResources(t *testing.T) { for i := int64(0); i < 4; i++ { delta, idx := outResourcesDeltas.get(addrs[i], basics.CreatableIndex(100+i)) require.NotEqual(t, -1, idx) - require.Equal(t, persistedResourcesData{addrid: i + 1, aidx: basics.CreatableIndex(100 + i)}, delta.oldResource) + require.Equal(t, store.PersistedResourcesData{Addrid: i + 1, Aidx: basics.CreatableIndex(100 + i)}, delta.oldResource) if i%2 == 0 { delta, idx = outResourcesDeltas.get(addrs[i], basics.CreatableIndex(200+i)) require.NotEqual(t, -1, idx) - require.Equal(t, persistedResourcesData{addrid: i + 1, aidx: basics.CreatableIndex(200 + i)}, delta.oldResource) + require.Equal(t, store.PersistedResourcesData{Addrid: i + 1, Aidx: basics.CreatableIndex(200 + i)}, delta.oldResource) } } // add another round - accountDeltas = append(accountDeltas, ledgercore.AccountDeltas{}) - accountDeltas[1].Upsert(addrs[0], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 3}}}) - accountDeltas[1].Upsert(addrs[3], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 8}}}) + stateDeltas = append(stateDeltas, ledgercore.StateDelta{}) + stateDeltas[1].Accts.Upsert(addrs[0], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 3}}}) + stateDeltas[1].Accts.Upsert(addrs[3], ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 8}}}) appLocalState100 := basics.AppLocalState{KeyValue: basics.TealKeyValue{"100": basics.TealValue{Type: basics.TealBytesType, Bytes: "100"}}} - accountDeltas[1].UpsertAppResource(addrs[0], 100, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{LocalState: &appLocalState100}) + stateDeltas[1].Accts.UpsertAppResource(addrs[0], 100, ledgercore.AppParamsDelta{}, ledgercore.AppLocalStateDelta{LocalState: &appLocalState100}) appParams104 := basics.AppParams{ApprovalProgram: []byte{104}} appLocalState204 := basics.AppLocalState{KeyValue: basics.TealKeyValue{"204": basics.TealValue{Type: basics.TealBytesType, Bytes: "204"}}} - accountDeltas[1].UpsertAppResource(addrs[4], 104, ledgercore.AppParamsDelta{Params: &appParams104}, ledgercore.AppLocalStateDelta{LocalState: &appLocalState204}) + stateDeltas[1].Accts.UpsertAppResource(addrs[4], 104, ledgercore.AppParamsDelta{Params: &appParams104}, ledgercore.AppLocalStateDelta{LocalState: &appLocalState204}) - baseResources.write(persistedResourcesData{addrid: 5 /* 4+1 */, aidx: basics.CreatableIndex(104)}, addrs[4]) - outResourcesDeltas = makeCompactResourceDeltas(accountDeltas, basics.Round(1), true, baseAccounts, baseResources) + baseResources.write(store.PersistedResourcesData{Addrid: 5 /* 4+1 */, Aidx: basics.CreatableIndex(104)}, addrs[4]) + outResourcesDeltas = makeCompactResourceDeltas(stateDeltas, basics.Round(1), true, baseAccounts, baseResources) require.Equal(t, 0, len(outResourcesDeltas.misses)) require.Equal(t, 7, len(outResourcesDeltas.deltas)) @@ -1926,19 +1883,8 @@ func TestAcctUpdatesCachesInitialization(t *testing.T) { initialRounds := uint64(1) accountsCount := 5 - - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(accountsCount, true)} rewardsLevels := []uint64{0} - - pooldata := basics.AccountData{} - pooldata.MicroAlgos.Raw = 1000 * 1000 * 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 + accts := setupAccts(accountsCount) ml := makeMockLedgerForTracker(t, true, int(initialRounds), protocolVersion, accts) ml.log.SetLevel(logging.Warn) @@ -2023,18 +1969,8 @@ func TestAcctUpdatesSplittingConsensusVersionCommits(t *testing.T) { initialRounds := uint64(1) accountsCount := 5 - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(accountsCount, true)} rewardsLevels := []uint64{0} - - pooldata := basics.AccountData{} - pooldata.MicroAlgos.Raw = 1000 * 1000 * 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 + accts := setupAccts(accountsCount) ml := makeMockLedgerForTracker(t, true, int(initialRounds), initProtocolVersion, accts) ml.log.SetLevel(logging.Warn) @@ -2139,20 +2075,9 @@ func TestAcctUpdatesSplittingConsensusVersionCommitsBoundary(t *testing.T) { initProtocolVersion := protocol.ConsensusV20 initialRounds := uint64(1) - accountsCount := 5 - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(accountsCount, true)} rewardsLevels := []uint64{0} - - pooldata := basics.AccountData{} - pooldata.MicroAlgos.Raw = 1000 * 1000 * 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 + accts := setupAccts(accountsCount) ml := makeMockLedgerForTracker(t, true, int(initialRounds), initProtocolVersion, accts) ml.log.SetLevel(logging.Warn) @@ -2289,17 +2214,7 @@ func TestAcctUpdatesSplittingConsensusVersionCommitsBoundary(t *testing.T) { func TestAcctUpdatesResources(t *testing.T) { partitiontest.PartitionTest(t) - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(20, true)} - pooldata := basics.AccountData{} - pooldata.MicroAlgos.Raw = 100 * 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 - + accts := setupAccts(20) testProtocolVersion := protocol.ConsensusCurrentVersion protoParams := config.Consensus[testProtocolVersion] @@ -2438,11 +2353,12 @@ func TestAcctUpdatesResources(t *testing.T) { err := au.prepareCommit(dcc) require.NoError(t, err) err = ml.trackers.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + arw := store.NewAccountsSQLReaderWriter(tx) err = au.commitRound(ctx, tx, dcc) if err != nil { return err } - err = updateAccountsRound(tx, newBase) + err = arw.UpdateAccountsRound(newBase) return err }) require.NoError(t, err) @@ -2538,19 +2454,9 @@ func testAcctUpdatesLookupRetry(t *testing.T, assertFn func(au *accountUpdates, testProtocolVersion := protocol.ConsensusCurrentVersion proto := config.Consensus[testProtocolVersion] - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(20, true)} + accts := setupAccts(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 - conf := config.GetDefaultLocal() initialBlocksCount := int(conf.MaxAcctLookback) ml := makeMockLedgerForTracker(t, false, initialBlocksCount, testProtocolVersion, accts) @@ -2731,11 +2637,12 @@ func auCommitSync(t *testing.T, rnd basics.Round, au *accountUpdates, ml *mockLe err := au.prepareCommit(dcc) require.NoError(t, err) err = ml.trackers.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + arw := store.NewAccountsSQLReaderWriter(tx) err = au.commitRound(ctx, tx, dcc) if err != nil { return err } - err = updateAccountsRound(tx, newBase) + err = arw.UpdateAccountsRound(newBase) return err }) require.NoError(t, err) @@ -2786,16 +2693,7 @@ func auNewBlock(t *testing.T, rnd basics.Round, au *accountUpdates, base map[bas func TestAcctUpdatesLookupLatestCacheRetry(t *testing.T) { partitiontest.PartitionTest(t) - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(20, true)} - pooldata := basics.AccountData{} - pooldata.MicroAlgos.Raw = 100 * 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 + accts := setupAccts(20) testProtocolVersion := protocol.ConsensusCurrentVersion protoParams := config.Consensus[testProtocolVersion] @@ -2856,16 +2754,16 @@ func TestAcctUpdatesLookupLatestCacheRetry(t *testing.T) { pad, ok := au.baseAccounts.read(addr1) require.True(t, ok) - pad.round = au.cachedDBRound + pad.Round = au.cachedDBRound au.baseAccounts.write(pad) prd, ok := au.baseResources.read(addr1, basics.CreatableIndex(aidx1)) require.True(t, ok) - prd.round = oldCachedDBRound + prd.Round = oldCachedDBRound au.baseResources.write(prd, addr1) prd, ok = au.baseResources.read(addr1, basics.CreatableIndex(aidx2)) require.True(t, ok) - prd.round = oldCachedDBRound + prd.Round = oldCachedDBRound au.baseResources.write(prd, addr1) var ad basics.AccountData @@ -2917,16 +2815,7 @@ func TestAcctUpdatesLookupLatestCacheRetry(t *testing.T) { func TestAcctUpdatesLookupResources(t *testing.T) { partitiontest.PartitionTest(t) - accts := []map[basics.Address]basics.AccountData{ledgertesting.RandomAccounts(1, true)} - pooldata := basics.AccountData{} - pooldata.MicroAlgos.Raw = 100 * 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 + accts := setupAccts(1) testProtocolVersion := protocol.ConsensusVersion("test-protocol-TestAcctUpdatesLookupResources") protoParams := config.Consensus[protocol.ConsensusCurrentVersion] @@ -2999,3 +2888,162 @@ func TestAcctUpdatesLookupResources(t *testing.T) { require.Contains(t, data.Assets, aidx3) require.NotContains(t, data.Assets, aidx2) } + +// TestAcctUpdatesLookupStateDelta simulates rounds w/ both account and kv changes in them, +// validating that a StateDelta can be retrieved for expected rounds containing the same updates. +func TestAcctUpdatesLookupStateDelta(t *testing.T) { + partitiontest.PartitionTest(t) + + initialBlocksCount := 1 + accts := setupAccts(1) + + testProtocolVersion := protocol.ConsensusVersion("test-protocol-TestAcctUpdatesLookupStateDelta") + protoParams := config.Consensus[protocol.ConsensusCurrentVersion] + protoParams.MaxBalLookback = 2 + protoParams.SeedLookback = 1 + protoParams.SeedRefreshInterval = 1 + config.Consensus[testProtocolVersion] = protoParams + defer func() { + delete(config.Consensus, testProtocolVersion) + }() + + ml := makeMockLedgerForTracker(t, true, initialBlocksCount, testProtocolVersion, accts) + defer ml.Close() + + conf := config.GetDefaultLocal() + au, _ := newAcctUpdates(t, ml, conf) + defer au.close() + + knownCreatables := make(map[basics.CreatableIndex]bool) + + var addr1 basics.Address + for addr := range accts[0] { + if addr != testSinkAddr && addr != testPoolAddr { + addr1 = addr + break + } + } + + aidx1 := basics.AssetIndex(1) + aidx2 := basics.AssetIndex(2) + aidx3 := basics.AssetIndex(3) + + // Stores AccountDeltas for each round. These are used as the source of truth for comparing retrieved StateDeltas. + updatesI := make(map[basics.Round]ledgercore.AccountDeltas) + // Stores KVMods for each round. These are used as the source of truth for comparing retireved StateDeltas. + var roundMods = make(map[basics.Round]map[string]ledgercore.KvValueDelta) + + // Sets up random keys & values to store. + kvCnt := 1000 + kvsPerBlock := 100 + curKV := 0 + var currentRound basics.Round + kvMap := make(map[string][]byte) + for i := 0; i < kvCnt; i++ { + kvMap[fmt.Sprintf("%d", i)] = []byte(fmt.Sprintf("value%d", i)) + } + + // Iterate through rounds 1..9, creating KvDeltas and modifying some accounts/assets + for i := 1; i < kvCnt/kvsPerBlock; i++ { + var updates ledgercore.AccountDeltas + currentRound = currentRound + 1 + // Construct KvMods for round + kvMods := make(map[string]ledgercore.KvValueDelta) + if i < kvCnt/kvsPerBlock { + for j := 0; j < kvsPerBlock; j++ { + name := fmt.Sprintf("%d", curKV) + curKV++ + val := kvMap[name] + kvMods[name] = ledgercore.KvValueDelta{Data: val, OldData: nil} + } + } + // Stores created kvMods for assertions against StateDeltas + roundMods[currentRound] = kvMods + + // Construct acct updates. These are arbitrary updates made for a few rounds to ensure they are properly + // reflected in the retrieved StateDeltas. + if i == 1 { + updates.Upsert(addr1, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}, TotalAssets: 1}}) + updates.UpsertAssetResource(addr1, aidx1, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 100}}) + } + if uint64(i) == protoParams.MaxBalLookback+2 { + updates.Upsert(addr1, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}, TotalAssets: 3}}) + updates.UpsertAssetResource(addr1, aidx2, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 200}}) + updates.UpsertAssetResource(addr1, aidx3, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 300}}) + } + if uint64(i) == protoParams.MaxBalLookback+3 { + updates.Upsert(addr1, ledgercore.AccountData{AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 1000000}, TotalAssets: 2}}) + updates.UpsertAssetResource(addr1, aidx2, ledgercore.AssetParamsDelta{}, ledgercore.AssetHoldingDelta{Deleted: true}) + } + // Store whatever updates were made for later assertions. + updatesI[basics.Round(i)] = updates + base := accts[i-1] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + // Commit the block with the changes + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, currentRound, au, base, opts, kvMods) + auCommitSync(t, currentRound, au, ml) + + // Ensure the db round is what we expect, and the proper amount is in cache versus in ledger. + rnd := au.latest() + require.Equal(t, currentRound, rnd) + if uint64(currentRound) > conf.MaxAcctLookback { + require.Equal(t, basics.Round(uint64(currentRound)-conf.MaxAcctLookback), au.cachedDBRound) + } else { + require.Equal(t, basics.Round(0), au.cachedDBRound) + } + + // Iterate backwards through deltas, ensuring proper data exists in StateDelta + for j := uint64(rnd); j > uint64(au.cachedDBRound); j-- { + // fetch StateDelta + actualDelta, err := au.lookupStateDelta(basics.Round(j)) + require.NoError(t, err) + actualAccountDeltas := actualDelta.Accts + actualKvDeltas := actualDelta.KvMods + + // Make sure we know about the expected changes for the delta's round + expectedAccountDeltas, has := updatesI[basics.Round(j)] + require.True(t, has) + // Do basic checking on the size and existence of accounts in deltas + require.Equal(t, expectedAccountDeltas.Len(), actualAccountDeltas.Len()) + require.Equal(t, len(expectedAccountDeltas.Accts), len(actualAccountDeltas.Accts)) + for _, acct := range expectedAccountDeltas.Accts { + _, has := actualAccountDeltas.GetBasicsAccountData(acct.Addr) + require.True(t, has) + } + // Do basic checking on the existence of asset changes in deltas + require.Equal(t, len(expectedAccountDeltas.AssetResources), len(actualAccountDeltas.AssetResources)) + for _, asset := range expectedAccountDeltas.AssetResources { + _, ok := actualAccountDeltas.GetResource(asset.Addr, basics.CreatableIndex(asset.Aidx), basics.AssetCreatable) + require.True(t, ok) + } + require.Equal(t, len(expectedAccountDeltas.AppResources), len(actualAccountDeltas.AppResources)) + + // Validate KvDeltas contains updates w/ new/old values. + startKV := (j - 1) * uint64(kvsPerBlock) + expectedKvDeltas, has := roundMods[basics.Round(j)] + require.True(t, has) + for kv := 0; kv < kvsPerBlock; kv++ { + name := fmt.Sprintf("%d", startKV+uint64(kv)) + delta, has := actualKvDeltas[name] + require.True(t, has) + expectedDelta, has := expectedKvDeltas[name] + require.True(t, has) + require.Equal(t, expectedDelta.Data, delta.Data) + require.Equal(t, expectedDelta.OldData, delta.OldData) + + } + } + } + // For rounds evicted from cache, perform sanity checks to confirm intended + // side effects took effect. + data, rnd, _, err := au.lookupLatest(addr1) + require.NoError(t, err) + require.Equal(t, basics.Round(kvCnt/kvsPerBlock-1), rnd) + require.Len(t, data.Assets, 2) + require.Contains(t, data.Assets, aidx1) + require.Contains(t, data.Assets, aidx3) + require.NotContains(t, data.Assets, aidx2) +} diff --git a/ledger/applications_test.go b/ledger/applications_test.go index 012e291b4e..67dbfd50be 100644 --- a/ledger/applications_test.go +++ b/ledger/applications_test.go @@ -27,6 +27,7 @@ import ( "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/store" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -250,48 +251,45 @@ return` commitRound(1, 3, l) // dump accounts - var rowid int64 - var dbRound basics.Round - var buf []byte - err = l.accts.accountsq.lookupStmt.QueryRow(creator[:]).Scan(&rowid, &dbRound, &buf) + entryAcc, err := l.accts.accountsq.LookupAccount(creator) a.NoError(err) - a.Equal(basics.Round(4), dbRound) - a.Equal(expectedCreatorBase, buf) - err = l.accts.accountsq.lookupResourcesStmt.QueryRow(creator[:], basics.CreatableIndex(appIdx)).Scan(&rowid, &dbRound, &buf) + a.Equal(basics.Round(4), entryAcc.Round) + a.Equal(expectedCreatorBase, protocol.Encode(&entryAcc.AccountData)) + entryRes, err := l.accts.accountsq.LookupResources(creator, basics.CreatableIndex(appIdx), basics.AppCreatable) a.NoError(err) - a.Equal(basics.Round(4), dbRound) - a.Equal(expectedCreatorResource, buf) + a.Equal(basics.Round(4), entryRes.Round) + a.Equal(expectedCreatorResource, protocol.Encode(&entryRes.Data)) - err = l.accts.accountsq.lookupStmt.QueryRow(userOptin[:]).Scan(&rowid, &dbRound, &buf) + entryAcc, err = l.accts.accountsq.LookupAccount(userOptin) a.NoError(err) - a.Equal(basics.Round(4), dbRound) - a.Equal(expectedUserOptInBase, buf) - err = l.accts.accountsq.lookupResourcesStmt.QueryRow(userOptin[:], basics.CreatableIndex(appIdx)).Scan(&rowid, &dbRound, &buf) + a.Equal(basics.Round(4), entryAcc.Round) + a.Equal(expectedUserOptInBase, protocol.Encode(&entryAcc.AccountData)) + entryRes, err = l.accts.accountsq.LookupResources(userOptin, basics.CreatableIndex(appIdx), basics.AppCreatable) a.NoError(err) - a.Equal(basics.Round(4), dbRound) - a.Equal(expectedUserOptInResource, buf) + a.Equal(basics.Round(4), entryRes.Round) + a.Equal(expectedUserOptInResource, protocol.Encode(&entryRes.Data)) - pad, err := l.accts.accountsq.lookup(userOptin) + pad, err := l.accts.accountsq.LookupAccount(userOptin) a.NoError(err) a.NotEmpty(pad) - prd, err := l.accts.accountsq.lookupResources(userOptin, basics.CreatableIndex(appIdx), basics.AppCreatable) + prd, err := l.accts.accountsq.LookupResources(userOptin, basics.CreatableIndex(appIdx), basics.AppCreatable) a.NoError(err) - a.Nil(prd.data.GetAppLocalState().KeyValue) + a.Nil(prd.Data.GetAppLocalState().KeyValue) ad, rnd, _, err := l.LookupLatest(userOptin) - a.Equal(dbRound, rnd) + a.Equal(basics.Round(4), rnd) a.NoError(err) a.Nil(ad.AppLocalStates[appIdx].KeyValue) - err = l.accts.accountsq.lookupStmt.QueryRow(userLocal[:]).Scan(&rowid, &dbRound, &buf) + entryAcc, err = l.accts.accountsq.LookupAccount(userLocal) a.NoError(err) - a.Equal(basics.Round(4), dbRound) - a.Equal(expectedUserLocalBase, buf) - err = l.accts.accountsq.lookupResourcesStmt.QueryRow(userLocal[:], basics.CreatableIndex(appIdx)).Scan(&rowid, &dbRound, &buf) + a.Equal(basics.Round(4), entryAcc.Round) + a.Equal(expectedUserLocalBase, protocol.Encode(&entryAcc.AccountData)) + entryRes, err = l.accts.accountsq.LookupResources(userLocal, basics.CreatableIndex(appIdx), basics.AppCreatable) a.NoError(err) - a.Equal(basics.Round(4), dbRound) - a.Equal(expectedUserLocalResource, buf) + a.Equal(basics.Round(4), entryRes.Round) + a.Equal(expectedUserLocalResource, protocol.Encode(&entryRes.Data)) - ar, err := l.LookupApplication(dbRound, userLocal, appIdx) + ar, err := l.LookupApplication(basics.Round(4), userLocal, appIdx) a.NoError(err) a.Equal("local", ar.AppLocalState.KeyValue["lk"].Bytes) @@ -753,15 +751,15 @@ return` a.NoError(err) a.Empty(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas) - pad, err := l.accts.accountsq.lookup(userLocal) + pad, err := l.accts.accountsq.LookupAccount(userLocal) a.NoError(err) - a.Equal(baseAccountData{}, pad.accountData) - a.Zero(pad.rowid) - prd, err := l.accts.accountsq.lookupResources(userLocal, basics.CreatableIndex(appIdx), basics.AppCreatable) + a.Equal(store.BaseAccountData{}, pad.AccountData) + a.Zero(pad.Rowid) + prd, err := l.accts.accountsq.LookupResources(userLocal, basics.CreatableIndex(appIdx), basics.AppCreatable) a.NoError(err) - a.Zero(prd.addrid) - emptyResourceData := makeResourcesData(0) - a.Equal(emptyResourceData, prd.data) + a.Zero(prd.Addrid) + emptyResourceData := store.MakeResourcesData(0) + a.Equal(emptyResourceData, prd.Data) } func TestAppEmptyAccountsGlobal(t *testing.T) { @@ -889,15 +887,15 @@ return` a.Contains(blk.Payset[0].ApplyData.EvalDelta.GlobalDelta, "gk") a.Equal(blk.Payset[0].ApplyData.EvalDelta.GlobalDelta["gk"].Bytes, "global") - pad, err := l.accts.accountsq.lookup(creator) + pad, err := l.accts.accountsq.LookupAccount(creator) a.NoError(err) - a.Empty(pad.accountData) - a.Zero(pad.rowid) - prd, err := l.accts.accountsq.lookupResources(creator, basics.CreatableIndex(appIdx), basics.AppCreatable) + a.Empty(pad.AccountData) + a.Zero(pad.Rowid) + prd, err := l.accts.accountsq.LookupResources(creator, basics.CreatableIndex(appIdx), basics.AppCreatable) a.NoError(err) - a.Zero(prd.addrid) - emptyResourceData := makeResourcesData(0) - a.Equal(emptyResourceData, prd.data) + a.Zero(prd.Addrid) + emptyResourceData := store.MakeResourcesData(0) + a.Equal(emptyResourceData, prd.Data) } func TestAppAccountDeltaIndicesCompatibility1(t *testing.T) { diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 6e61ee0abf..50d367d65e 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -40,6 +40,7 @@ import ( "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger/internal" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store/blockdb" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" @@ -219,10 +220,10 @@ func TestArchivalRestart(t *testing.T) { var latest, earliest basics.Round err = l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - latest, err = blockLatest(tx) + latest, err = blockdb.BlockLatest(tx) require.NoError(t, err) - earliest, err = blockEarliest(tx) + earliest, err = blockdb.BlockEarliest(tx) require.NoError(t, err) return err }) @@ -236,10 +237,10 @@ func TestArchivalRestart(t *testing.T) { defer l.Close() err = l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - latest, err = blockLatest(tx) + latest, err = blockdb.BlockLatest(tx) require.NoError(t, err) - earliest, err = blockEarliest(tx) + earliest, err = blockdb.BlockEarliest(tx) require.NoError(t, err) return err }) @@ -754,10 +755,10 @@ func TestArchivalFromNonArchival(t *testing.T) { var latest, earliest basics.Round err = l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - latest, err = blockLatest(tx) + latest, err = blockdb.BlockLatest(tx) require.NoError(t, err) - earliest, err = blockEarliest(tx) + earliest, err = blockdb.BlockEarliest(tx) require.NoError(t, err) return err }) @@ -774,10 +775,10 @@ func TestArchivalFromNonArchival(t *testing.T) { defer l.Close() err = l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - latest, err = blockLatest(tx) + latest, err = blockdb.BlockLatest(tx) require.NoError(t, err) - earliest, err = blockEarliest(tx) + earliest, err = blockdb.BlockEarliest(tx) require.NoError(t, err) return err }) diff --git a/ledger/blockqueue.go b/ledger/blockqueue.go index cb14d62943..4dfa1ae2cc 100644 --- a/ledger/blockqueue.go +++ b/ledger/blockqueue.go @@ -29,6 +29,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store/blockdb" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/metrics" @@ -61,7 +62,7 @@ func bqInit(l *Ledger) (*blockQueue, error) { start := time.Now() err := bq.l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error - bq.lastCommitted, err0 = blockLatest(tx) + bq.lastCommitted, err0 = blockdb.BlockLatest(tx) return err0 }) ledgerBlockqInitMicros.AddMicrosecondsSince(start, nil) @@ -111,7 +112,7 @@ func (bq *blockQueue) syncer() { ledgerSyncBlockputCount.Inc(nil) 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) + err0 := blockdb.BlockPut(tx, e.block, e.cert) if err0 != nil { return err0 } @@ -146,7 +147,7 @@ func (bq *blockQueue) syncer() { bfstart := time.Now() ledgerSyncBlockforgetCount.Inc(nil) err = bq.l.blockDBs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - return blockForgetBefore(tx, minToSave) + return blockdb.BlockForgetBefore(tx, minToSave) }) ledgerSyncBlockforgetMicros.AddMicrosecondsSince(bfstart, nil) if err != nil { @@ -261,7 +262,7 @@ func (bq *blockQueue) getBlock(r basics.Round) (blk bookkeeping.Block, err error ledgerGetblockCount.Inc(nil) err = bq.l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error - blk, err0 = blockGet(tx, r) + blk, err0 = blockdb.BlockGet(tx, r) return err0 }) ledgerGetblockMicros.AddMicrosecondsSince(start, nil) @@ -283,7 +284,7 @@ func (bq *blockQueue) getBlockHdr(r basics.Round) (hdr bookkeeping.BlockHeader, ledgerGetblockhdrCount.Inc(nil) err = bq.l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error - hdr, err0 = blockGetHdr(tx, r) + hdr, err0 = blockdb.BlockGetHdr(tx, r) return err0 }) ledgerGetblockhdrMicros.AddMicrosecondsSince(start, nil) @@ -309,7 +310,7 @@ func (bq *blockQueue) getEncodedBlockCert(r basics.Round) (blk []byte, cert []by ledgerGeteblockcertCount.Inc(nil) err = bq.l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error - blk, cert, err0 = blockGetEncodedCert(tx, r) + blk, cert, err0 = blockdb.BlockGetEncodedCert(tx, r) return err0 }) ledgerGeteblockcertMicros.AddMicrosecondsSince(start, nil) @@ -331,7 +332,7 @@ func (bq *blockQueue) getBlockCert(r basics.Round) (blk bookkeeping.Block, cert ledgerGetblockcertCount.Inc(nil) err = bq.l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error - blk, cert, err0 = blockGetCert(tx, r) + blk, cert, err0 = blockdb.BlockGetCert(tx, r) return err0 }) ledgerGetblockcertMicros.AddMicrosecondsSince(start, nil) diff --git a/ledger/blockqueue_test.go b/ledger/blockqueue_test.go index 0f62798901..90aa7b4fee 100644 --- a/ledger/blockqueue_test.go +++ b/ledger/blockqueue_test.go @@ -26,6 +26,7 @@ import ( "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/ledger/ledgercore" ledgertesting "github.com/algorand/go-algorand/ledger/testing" @@ -34,6 +35,33 @@ import ( "github.com/algorand/go-algorand/test/partitiontest" ) +func randomBlock(r basics.Round) blockEntry { + b := bookkeeping.Block{} + c := agreement.Certificate{} + + b.BlockHeader.Round = r + b.BlockHeader.TimeStamp = int64(crypto.RandUint64()) + b.RewardsPool = testPoolAddr + b.FeeSink = testSinkAddr + c.Round = r + + return blockEntry{ + block: b, + cert: c, + } +} + +func randomInitChain(proto protocol.ConsensusVersion, nblock int) []blockEntry { + res := make([]blockEntry, 0) + for i := 0; i < nblock; i++ { + blkent := randomBlock(basics.Round(i)) + blkent.cert = agreement.Certificate{} + blkent.block.CurrentProtocol = proto + res = append(res, blkent) + } + return res +} + func TestPutBlockTooOld(t *testing.T) { partitiontest.PartitionTest(t) diff --git a/ledger/catchpointtracker.go b/ledger/catchpointtracker.go index 584a9447d8..2867b285c7 100644 --- a/ledger/catchpointtracker.go +++ b/ledger/catchpointtracker.go @@ -22,7 +22,6 @@ import ( "compress/gzip" "context" "database/sql" - "encoding/binary" "encoding/hex" "errors" "fmt" @@ -30,7 +29,6 @@ import ( "os" "path/filepath" "strconv" - "strings" "sync/atomic" "time" @@ -43,28 +41,19 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" "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" ) -// trieCachedNodesCount defines how many balances trie nodes we would like to keep around in memory. -// value was calibrated using BenchmarkCalibrateCacheNodeSize -var trieCachedNodesCount = 9000 - -// merkleCommitterNodesPerPage controls how many nodes will be stored in a single page -// value was calibrated using BenchmarkCalibrateNodesPerPage -var merkleCommitterNodesPerPage = int64(116) - const ( // trieRebuildAccountChunkSize defines the number of accounts that would get read at a single chunk // before added to the trie during trie construction trieRebuildAccountChunkSize = 16384 // trieRebuildCommitFrequency defines the number of accounts that would get added before we call evict to commit the changes and adjust the memory cache. trieRebuildCommitFrequency = 65536 - // CatchpointDirName represents the directory name in which all the catchpoints files are stored - CatchpointDirName = "catchpoints" // CatchpointFileVersionV5 is the catchpoint file version that was used when the database schema was V0-V5. CatchpointFileVersionV5 = uint64(0200) @@ -74,14 +63,6 @@ const ( CatchpointFileVersionV6 = uint64(0201) ) -// TrieMemoryConfig is the memory configuration setup used for the merkle trie. -var TrieMemoryConfig = merkletrie.MemoryConfig{ - NodesCountPerPage: merkleCommitterNodesPerPage, - CachedNodesCount: trieCachedNodesCount, - PageFillFactor: 0.95, - MaxChildrenPagesThreshold: 64, -} - func catchpointStage1Encoder(w io.Writer) (io.WriteCloser, error) { return snappy.NewBufferedWriter(w), nil } @@ -96,6 +77,11 @@ func catchpointStage1Decoder(r io.Reader) (io.ReadCloser, error) { return snappyReadCloser{snappy.NewReader(r)}, nil } +type catchpointStore interface { + store.CatchpointWriter + store.CatchpointReader +} + type catchpointTracker struct { // dbDirectory is the directory where the ledger and block sql file resides as well as the parent directory for the catchup files to be generated dbDirectory string @@ -111,13 +97,14 @@ type catchpointTracker struct { enableGeneratingCatchpointFiles bool // Prepared SQL statements for fast accounts DB lookups. - accountsq *accountsDbQueries + accountsq store.AccountsReader // log copied from ledger log logging.Logger // Connection to the database. - dbs db.Pair + dbs db.Pair + catchpointStore catchpointStore // The last catchpoint label that was written to the database. Should always align with what's in the database. // note that this is the last catchpoint *label* and not the catchpoint file. @@ -230,13 +217,14 @@ func (ct *catchpointTracker) finishFirstStage(ctx context.Context, dbRound basic } f := func(ctx context.Context, tx *sql.Tx) error { + crw := store.NewCatchpointSQLReaderWriter(tx) err := ct.recordFirstStageInfo(ctx, tx, dbRound, totalKVs, totalAccounts, totalChunks, biggestChunkLen) if err != nil { return err } // Clear the db record. - return writeCatchpointStateUint64(ctx, tx, catchpointStateWritingFirstStageInfo, 0) + return crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateWritingFirstStageInfo, 0) } return ct.dbs.Wdb.Atomic(f) } @@ -244,8 +232,8 @@ func (ct *catchpointTracker) finishFirstStage(ctx context.Context, dbRound basic // Possibly finish generating first stage catchpoint db record and data file after // a crash. func (ct *catchpointTracker) finishFirstStageAfterCrash(dbRound basics.Round) error { - v, err := readCatchpointStateUint64( - context.Background(), ct.dbs.Rdb.Handle, catchpointStateWritingFirstStageInfo) + v, err := ct.catchpointStore.ReadCatchpointStateUint64( + context.Background(), store.CatchpointStateWritingFirstStageInfo) if err != nil { return err } @@ -255,9 +243,9 @@ func (ct *catchpointTracker) finishFirstStageAfterCrash(dbRound basics.Round) er // First, delete the unfinished data file. relCatchpointDataFilePath := filepath.Join( - CatchpointDirName, + store.CatchpointDirName, makeCatchpointDataFilePath(dbRound)) - err = removeSingleCatchpointFileFromDisk(ct.dbDirectory, relCatchpointDataFilePath) + err = store.RemoveSingleCatchpointFileFromDisk(ct.dbDirectory, relCatchpointDataFilePath) if err != nil { return err } @@ -266,7 +254,7 @@ func (ct *catchpointTracker) finishFirstStageAfterCrash(dbRound basics.Round) er } func (ct *catchpointTracker) finishCatchpointsAfterCrash(catchpointLookback uint64) error { - records, err := selectUnfinishedCatchpoints(context.Background(), ct.dbs.Rdb.Handle) + records, err := ct.catchpointStore.SelectUnfinishedCatchpoints(context.Background()) if err != nil { return err } @@ -274,15 +262,15 @@ func (ct *catchpointTracker) finishCatchpointsAfterCrash(catchpointLookback uint for _, record := range records { // First, delete the unfinished catchpoint file. relCatchpointFilePath := filepath.Join( - CatchpointDirName, - makeCatchpointFilePath(basics.Round(record.round))) - err = removeSingleCatchpointFileFromDisk(ct.dbDirectory, relCatchpointFilePath) + store.CatchpointDirName, + store.MakeCatchpointFilePath(basics.Round(record.Round))) + err = store.RemoveSingleCatchpointFileFromDisk(ct.dbDirectory, relCatchpointFilePath) if err != nil { return err } err = ct.finishCatchpoint( - context.Background(), record.round, record.blockHash, catchpointLookback) + context.Background(), record.Round, record.BlockHash, catchpointLookback) if err != nil { return err } @@ -299,8 +287,8 @@ func (ct *catchpointTracker) recoverFromCrash(dbRound basics.Round) error { ctx := context.Background() - catchpointLookback, err := readCatchpointStateUint64( - ctx, ct.dbs.Rdb.Handle, catchpointStateCatchpointLookback) + catchpointLookback, err := ct.catchpointStore.ReadCatchpointStateUint64( + ctx, store.CatchpointStateCatchpointLookback) if err != nil { return err } @@ -331,6 +319,7 @@ func (ct *catchpointTracker) recoverFromCrash(dbRound basics.Round) error { func (ct *catchpointTracker) loadFromDisk(l ledgerForTracker, dbRound basics.Round) (err error) { ct.log = l.trackerLog() ct.dbs = l.trackerDB() + ct.catchpointStore = store.NewCatchpointSQLReaderWriter(l.trackerDB().Wdb.Handle) ct.roundDigest = nil ct.catchpointDataWriting = 0 @@ -345,13 +334,13 @@ func (ct *catchpointTracker) loadFromDisk(l ledgerForTracker, dbRound basics.Rou return err } - ct.accountsq, err = accountsInitDbQueries(ct.dbs.Rdb.Handle) + ct.accountsq, err = store.AccountsInitDbQueries(ct.dbs.Rdb.Handle) if err != nil { return } - ct.lastCatchpointLabel, err = readCatchpointStateString( - context.Background(), ct.dbs.Rdb.Handle, catchpointStateLastCatchpoint) + ct.lastCatchpointLabel, err = ct.catchpointStore.ReadCatchpointStateString( + context.Background(), store.CatchpointStateLastCatchpoint) if err != nil { return } @@ -508,16 +497,19 @@ func (ct *catchpointTracker) commitRound(ctx context.Context, tx *sql.Tx, dcc *d } }() + crw := store.NewCatchpointSQLReaderWriter(tx) + arw := store.NewAccountsSQLReaderWriter(tx) + if ct.catchpointEnabled() { - var mc *MerkleCommitter - mc, err = MakeMerkleCommitter(tx, false) + var mc *store.MerkleCommitter + mc, err = store.MakeMerkleCommitter(tx, false) if err != nil { return } var trie *merkletrie.Trie if ct.balancesTrie == nil { - trie, err = merkletrie.MakeTrie(mc, TrieMemoryConfig) + trie, err = merkletrie.MakeTrie(mc, store.TrieMemoryConfig) if err != nil { ct.log.Warnf("unable to create merkle trie during committedUpTo: %v", err) return err @@ -543,27 +535,25 @@ func (ct *catchpointTracker) commitRound(ctx context.Context, tx *sql.Tx, dcc *d dcc.stats.MerkleTrieUpdateDuration = now - dcc.stats.MerkleTrieUpdateDuration } - err = updateAccountsHashRound(ctx, tx, treeTargetRound) + err = arw.UpdateAccountsHashRound(ctx, treeTargetRound) if err != nil { return err } if dcc.catchpointFirstStage { - err = writeCatchpointStateUint64(ctx, tx, catchpointStateWritingFirstStageInfo, 1) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateWritingFirstStageInfo, 1) if err != nil { return err } } - err = writeCatchpointStateUint64( - ctx, tx, catchpointStateCatchpointLookback, dcc.catchpointLookback) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchpointLookback, dcc.catchpointLookback) if err != nil { return err } for _, round := range ct.calculateCatchpointRounds(dcc) { - err = insertUnfinishedCatchpoint( - ctx, tx, round, dcc.committedRoundDigests[round-dcc.oldBase-1]) + err = crw.InsertUnfinishedCatchpoint(ctx, round, dcc.committedRoundDigests[round-dcc.oldBase-1]) if err != nil { return err } @@ -719,7 +709,7 @@ func repackCatchpoint(ctx context.Context, header CatchpointFileHeader, biggestC // Create a catchpoint (a label and possibly a file with db record) and remove // the unfinished catchpoint record. -func (ct *catchpointTracker) createCatchpoint(ctx context.Context, accountsRound basics.Round, round basics.Round, dataInfo catchpointFirstStageInfo, blockHash crypto.Digest) error { +func (ct *catchpointTracker) createCatchpoint(ctx context.Context, accountsRound basics.Round, round basics.Round, dataInfo store.CatchpointFirstStageInfo, blockHash crypto.Digest) error { startTime := time.Now() label := ledgercore.MakeCatchpointLabel( round, blockHash, dataInfo.TrieBalancesHash, dataInfo.Totals).String() @@ -728,8 +718,8 @@ func (ct *catchpointTracker) createCatchpoint(ctx context.Context, accountsRound "creating catchpoint round: %d accountsRound: %d label: %s", round, accountsRound, label) - err := writeCatchpointStateString( - ctx, ct.dbs.Wdb.Handle, catchpointStateLastCatchpoint, label) + err := ct.catchpointStore.WriteCatchpointStateString( + ctx, store.CatchpointStateLastCatchpoint, label) if err != nil { return err } @@ -742,7 +732,7 @@ func (ct *catchpointTracker) createCatchpoint(ctx context.Context, accountsRound return nil } - catchpointDataFilePath := filepath.Join(ct.dbDirectory, CatchpointDirName) + catchpointDataFilePath := filepath.Join(ct.dbDirectory, store.CatchpointDirName) catchpointDataFilePath = filepath.Join(catchpointDataFilePath, makeCatchpointDataFilePath(accountsRound)) @@ -769,7 +759,7 @@ func (ct *catchpointTracker) createCatchpoint(ctx context.Context, accountsRound } relCatchpointFilePath := - filepath.Join(CatchpointDirName, makeCatchpointFilePath(round)) + filepath.Join(store.CatchpointDirName, store.MakeCatchpointFilePath(round)) absCatchpointFilePath := filepath.Join(ct.dbDirectory, relCatchpointFilePath) err = os.MkdirAll(filepath.Dir(absCatchpointFilePath), 0700) @@ -788,11 +778,12 @@ func (ct *catchpointTracker) createCatchpoint(ctx context.Context, accountsRound } err = ct.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + crw := store.NewCatchpointSQLReaderWriter(tx) err = ct.recordCatchpointFile(ctx, tx, round, relCatchpointFilePath, fileInfo.Size()) if err != nil { return err } - return deleteUnfinishedCatchpoint(ctx, tx, round) + return crw.DeleteUnfinishedCatchpoint(ctx, round) }) if err != nil { return err @@ -816,14 +807,13 @@ func (ct *catchpointTracker) finishCatchpoint(ctx context.Context, round basics. ct.log.Infof("finishing catchpoint round: %d accountsRound: %d", round, accountsRound) - dataInfo, exists, err := - selectCatchpointFirstStageInfo(ctx, ct.dbs.Rdb.Handle, accountsRound) + dataInfo, exists, err := ct.catchpointStore.SelectCatchpointFirstStageInfo(ctx, accountsRound) if err != nil { return err } if !exists { - return deleteUnfinishedCatchpoint(ctx, ct.dbs.Wdb.Handle, round) + return ct.catchpointStore.DeleteUnfinishedCatchpoint(ctx, round) } return ct.createCatchpoint(ctx, accountsRound, round, dataInfo, blockHash) } @@ -860,22 +850,21 @@ func (ct *catchpointTracker) calculateCatchpointRounds(dcc *deferredCommitContex // Delete old first stage catchpoint records and data files. func (ct *catchpointTracker) pruneFirstStageRecordsData(ctx context.Context, maxRoundToDelete basics.Round) error { - rounds, err := selectOldCatchpointFirstStageInfoRounds( - ctx, ct.dbs.Rdb.Handle, maxRoundToDelete) + rounds, err := ct.catchpointStore.SelectOldCatchpointFirstStageInfoRounds(ctx, maxRoundToDelete) if err != nil { return err } for _, round := range rounds { relCatchpointDataFilePath := - filepath.Join(CatchpointDirName, makeCatchpointDataFilePath(round)) - err = removeSingleCatchpointFileFromDisk(ct.dbDirectory, relCatchpointDataFilePath) + filepath.Join(store.CatchpointDirName, makeCatchpointDataFilePath(round)) + err = store.RemoveSingleCatchpointFileFromDisk(ct.dbDirectory, relCatchpointDataFilePath) if err != nil { return err } } - return deleteOldCatchpointFirstStageInfo(ctx, ct.dbs.Rdb.Handle, maxRoundToDelete) + return ct.catchpointStore.DeleteOldCatchpointFirstStageInfo(ctx, maxRoundToDelete) } func (ct *catchpointTracker) postCommitUnlocked(ctx context.Context, dcc *deferredCommitContext) { @@ -940,27 +929,27 @@ func (ct *catchpointTracker) accountsUpdateBalances(accountsDeltas compactAccoun for i := 0; i < accountsDeltas.len(); i++ { delta := accountsDeltas.getByIdx(i) - if !delta.oldAcct.accountData.IsEmpty() { - deleteHash := accountHashBuilderV6(delta.address, &delta.oldAcct.accountData, protocol.Encode(&delta.oldAcct.accountData)) + if !delta.oldAcct.AccountData.IsEmpty() { + deleteHash := store.AccountHashBuilderV6(delta.address, &delta.oldAcct.AccountData, protocol.Encode(&delta.oldAcct.AccountData)) deleted, err = ct.balancesTrie.Delete(deleteHash) if err != nil { return fmt.Errorf("failed to delete hash '%s' from merkle trie for account %v: %w", hex.EncodeToString(deleteHash), delta.address, err) } if !deleted { - ct.log.Warnf("failed to delete hash '%s' from merkle trie for account %v", hex.EncodeToString(deleteHash), delta.address) + ct.log.Errorf("failed to delete hash '%s' from merkle trie for account %v", hex.EncodeToString(deleteHash), delta.address) } else { accumulatedChanges++ } } if !delta.newAcct.IsEmpty() { - addHash := accountHashBuilderV6(delta.address, &delta.newAcct, protocol.Encode(&delta.newAcct)) + addHash := store.AccountHashBuilderV6(delta.address, &delta.newAcct, protocol.Encode(&delta.newAcct)) added, err = ct.balancesTrie.Add(addHash) if err != nil { return fmt.Errorf("attempted to add duplicate hash '%s' to merkle trie for account %v: %w", hex.EncodeToString(addHash), delta.address, err) } if !added { - ct.log.Warnf("attempted to add duplicate hash '%s' to merkle trie for account %v", hex.EncodeToString(addHash), delta.address) + ct.log.Errorf("attempted to add duplicate hash '%s' to merkle trie for account %v", hex.EncodeToString(addHash), delta.address) } else { accumulatedChanges++ } @@ -970,8 +959,8 @@ func (ct *catchpointTracker) accountsUpdateBalances(accountsDeltas compactAccoun for i := 0; i < resourcesDeltas.len(); i++ { resDelta := resourcesDeltas.getByIdx(i) addr := resDelta.address - if !resDelta.oldResource.data.IsEmpty() { - deleteHash, err := resourcesHashBuilderV6(&resDelta.oldResource.data, addr, resDelta.oldResource.aidx, resDelta.oldResource.data.UpdateRound, protocol.Encode(&resDelta.oldResource.data)) + if !resDelta.oldResource.Data.IsEmpty() { + deleteHash, err := store.ResourcesHashBuilderV6(&resDelta.oldResource.Data, addr, resDelta.oldResource.Aidx, resDelta.oldResource.Data.UpdateRound, protocol.Encode(&resDelta.oldResource.Data)) if err != nil { return err } @@ -980,14 +969,14 @@ func (ct *catchpointTracker) accountsUpdateBalances(accountsDeltas compactAccoun return fmt.Errorf("failed to delete resource hash '%s' from merkle trie for account %v: %w", hex.EncodeToString(deleteHash), addr, err) } if !deleted { - ct.log.Warnf("failed to delete resource hash '%s' from merkle trie for account %v", hex.EncodeToString(deleteHash), addr) + ct.log.Errorf("failed to delete resource hash '%s' from merkle trie for account %v", hex.EncodeToString(deleteHash), addr) } else { accumulatedChanges++ } } if !resDelta.newResource.IsEmpty() { - addHash, err := resourcesHashBuilderV6(&resDelta.newResource, addr, resDelta.oldResource.aidx, resDelta.newResource.UpdateRound, protocol.Encode(&resDelta.newResource)) + addHash, err := store.ResourcesHashBuilderV6(&resDelta.newResource, addr, resDelta.oldResource.Aidx, resDelta.newResource.UpdateRound, protocol.Encode(&resDelta.newResource)) if err != nil { return err } @@ -996,7 +985,7 @@ func (ct *catchpointTracker) accountsUpdateBalances(accountsDeltas compactAccoun return fmt.Errorf("attempted to add duplicate resource hash '%s' to merkle trie for account %v: %w", hex.EncodeToString(addHash), addr, err) } if !added { - ct.log.Warnf("attempted to add duplicate resource hash '%s' to merkle trie for account %v", hex.EncodeToString(addHash), addr) + ct.log.Errorf("attempted to add duplicate resource hash '%s' to merkle trie for account %v", hex.EncodeToString(addHash), addr) } else { accumulatedChanges++ } @@ -1012,26 +1001,26 @@ func (ct *catchpointTracker) accountsUpdateBalances(accountsDeltas compactAccoun if mv.data != nil && bytes.Equal(mv.oldData, mv.data) { continue // changed back within the delta span } - deleteHash := kvHashBuilderV6(key, mv.oldData) + deleteHash := store.KvHashBuilderV6(key, mv.oldData) deleted, err = ct.balancesTrie.Delete(deleteHash) if err != nil { return fmt.Errorf("failed to delete kv hash '%s' from merkle trie for key %v: %w", hex.EncodeToString(deleteHash), key, err) } if !deleted { - ct.log.Warnf("failed to delete kv hash '%s' from merkle trie for key %v", hex.EncodeToString(deleteHash), key) + ct.log.Errorf("failed to delete kv hash '%s' from merkle trie for key %v", hex.EncodeToString(deleteHash), key) } else { accumulatedChanges++ } } if mv.data != nil { - addHash := kvHashBuilderV6(key, mv.data) + addHash := store.KvHashBuilderV6(key, mv.data) added, err = ct.balancesTrie.Add(addHash) if err != nil { return fmt.Errorf("attempted to add duplicate kv hash '%s' from merkle trie for key %v: %w", hex.EncodeToString(addHash), key, err) } if !added { - ct.log.Warnf("attempted to add duplicate kv hash '%s' from merkle trie for key %v", hex.EncodeToString(addHash), key) + ct.log.Errorf("attempted to add duplicate kv hash '%s' from merkle trie for key %v", hex.EncodeToString(addHash), key) } else { accumulatedChanges++ } @@ -1047,7 +1036,7 @@ func (ct *catchpointTracker) accountsUpdateBalances(accountsDeltas compactAccoun if ct.log.GetTelemetryEnabled() { root, rootErr := ct.balancesTrie.RootHash() if rootErr != nil { - ct.log.Infof("accountsUpdateBalances: error retrieving balances trie root: %v", rootErr) + ct.log.Errorf("accountsUpdateBalances: error retrieving balances trie root: %v", rootErr) return } ct.log.EventWithDetails(telemetryspec.Accounts, telemetryspec.CatchpointRootUpdateEvent, telemetryspec.CatchpointRootUpdateEventDetails{ @@ -1083,7 +1072,7 @@ func (ct *catchpointTracker) generateCatchpointData(ctx context.Context, account BalancesWriteTime: uint64(updatingBalancesDuration.Nanoseconds()), } - catchpointDataFilePath := filepath.Join(ct.dbDirectory, CatchpointDirName) + catchpointDataFilePath := filepath.Join(ct.dbDirectory, store.CatchpointDirName) catchpointDataFilePath = filepath.Join(catchpointDataFilePath, makeCatchpointDataFilePath(accountsRound)) @@ -1178,18 +1167,19 @@ func (ct *catchpointTracker) generateCatchpointData(ctx context.Context, account } func (ct *catchpointTracker) recordFirstStageInfo(ctx context.Context, tx *sql.Tx, accountsRound basics.Round, totalKVs uint64, totalAccounts uint64, totalChunks uint64, biggestChunkLen uint64) error { - accountTotals, err := accountsTotals(ctx, tx, false) + arw := store.NewAccountsSQLReaderWriter(tx) + accountTotals, err := arw.AccountsTotals(ctx, false) if err != nil { return err } { - mc, err := MakeMerkleCommitter(tx, false) + mc, err := store.MakeMerkleCommitter(tx, false) if err != nil { return err } if ct.balancesTrie == nil { - trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) + trie, err := merkletrie.MakeTrie(mc, store.TrieMemoryConfig) if err != nil { return err } @@ -1203,7 +1193,8 @@ func (ct *catchpointTracker) recordFirstStageInfo(ctx context.Context, tx *sql.T return err } - info := catchpointFirstStageInfo{ + crw := store.NewCatchpointSQLReaderWriter(tx) + info := store.CatchpointFirstStageInfo{ Totals: accountTotals, TotalAccounts: totalAccounts, TotalKVs: totalKVs, @@ -1211,37 +1202,27 @@ func (ct *catchpointTracker) recordFirstStageInfo(ctx context.Context, tx *sql.T BiggestChunkLen: biggestChunkLen, TrieBalancesHash: trieBalancesHash, } - return insertOrReplaceCatchpointFirstStageInfo(ctx, tx, accountsRound, &info) + return crw.InsertOrReplaceCatchpointFirstStageInfo(ctx, accountsRound, &info) } func makeCatchpointDataFilePath(accountsRound basics.Round) string { return strconv.FormatInt(int64(accountsRound), 10) + ".data" } -func makeCatchpointFilePath(round basics.Round) string { - irnd := int64(round) / 256 - outStr := "" - for irnd > 0 { - outStr = filepath.Join(outStr, fmt.Sprintf("%02x", irnd%256)) - irnd = irnd / 256 - } - outStr = filepath.Join(outStr, strconv.FormatInt(int64(round), 10)+".catchpoint") - return outStr -} - // recordCatchpointFile stores the provided fileName as the stored catchpoint for the given round. // after a successful insert operation to the database, it would delete up to 2 old entries, as needed. // deleting 2 entries while inserting single entry allow us to adjust the size of the backing storage and have the // database and storage realign. func (ct *catchpointTracker) recordCatchpointFile(ctx context.Context, e db.Executable, round basics.Round, relCatchpointFilePath string, fileSize int64) (err error) { + crw := store.NewCatchpointSQLReaderWriter(e) if ct.catchpointFileHistoryLength != 0 { - err = storeCatchpoint(ctx, e, round, relCatchpointFilePath, "", fileSize) + err = crw.StoreCatchpoint(ctx, round, relCatchpointFilePath, "", fileSize) if err != nil { ct.log.Warnf("catchpointTracker.recordCatchpointFile() unable to save catchpoint: %v", err) return } } else { - err = removeSingleCatchpointFileFromDisk(ct.dbDirectory, relCatchpointFilePath) + err = store.RemoveSingleCatchpointFileFromDisk(ct.dbDirectory, relCatchpointFilePath) if err != nil { ct.log.Warnf("catchpointTracker.recordCatchpointFile() unable to remove file (%s): %v", relCatchpointFilePath, err) return @@ -1251,16 +1232,16 @@ func (ct *catchpointTracker) recordCatchpointFile(ctx context.Context, e db.Exec return } var filesToDelete map[basics.Round]string - filesToDelete, err = getOldestCatchpointFiles(ctx, e, 2, ct.catchpointFileHistoryLength) + filesToDelete, err = crw.GetOldestCatchpointFiles(ctx, 2, ct.catchpointFileHistoryLength) if err != nil { return fmt.Errorf("unable to delete catchpoint file, getOldestCatchpointFiles failed : %v", err) } for round, fileToDelete := range filesToDelete { - err = removeSingleCatchpointFileFromDisk(ct.dbDirectory, fileToDelete) + err = store.RemoveSingleCatchpointFileFromDisk(ct.dbDirectory, fileToDelete) if err != nil { return err } - err = storeCatchpoint(ctx, e, round, "", "", 0) + err = crw.StoreCatchpoint(ctx, round, "", "", 0) if err != nil { return fmt.Errorf("unable to delete old catchpoint entry '%s' : %v", fileToDelete, err) } @@ -1274,8 +1255,11 @@ func (ct *catchpointTracker) GetCatchpointStream(round basics.Round) (ReadCloseS fileSize := int64(0) start := time.Now() ledgerGetcatchpointCount.Inc(nil) + // TODO: we need to generalize this, check @cce PoC PR, he has something + // somewhat broken for some KVs.. err := ct.dbs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - dbFileName, _, fileSize, err = getCatchpoint(ctx, tx, round) + crw := store.NewCatchpointSQLReaderWriter(tx) + dbFileName, _, fileSize, err = crw.GetCatchpoint(ctx, round) return }) ledgerGetcatchpointMicros.AddMicrosecondsSince(start, nil) @@ -1308,7 +1292,7 @@ func (ct *catchpointTracker) GetCatchpointStream(round basics.Round) (ReadCloseS // if the database doesn't know about that round, see if we have that file anyway: relCatchpointFilePath := - filepath.Join(CatchpointDirName, makeCatchpointFilePath(round)) + filepath.Join(store.CatchpointDirName, store.MakeCatchpointFilePath(round)) absCatchpointFilePath := filepath.Join(ct.dbDirectory, relCatchpointFilePath) file, err := os.OpenFile(absCatchpointFilePath, os.O_RDONLY, 0666) if err == nil && file != nil { @@ -1330,189 +1314,6 @@ func (ct *catchpointTracker) GetCatchpointStream(round basics.Round) (ReadCloseS return nil, ledgercore.ErrNoEntry{} } -// 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 deleteStoredCatchpoints(ctx context.Context, e db.Executable, dbDirectory string) (err error) { - catchpointsFilesChunkSize := 50 - for { - fileNames, err := getOldestCatchpointFiles(ctx, e, catchpointsFilesChunkSize, 0) - if err != nil { - return err - } - if len(fileNames) == 0 { - break - } - - for round, fileName := range fileNames { - err = removeSingleCatchpointFileFromDisk(dbDirectory, fileName) - if err != nil { - return err - } - // clear the entry from the database - err = storeCatchpoint(ctx, e, round, "", "", 0) - if err != nil { - return err - } - } - } - return nil -} - -// This function remove a single catchpoint file from the disk. this function does not leave empty directories -func removeSingleCatchpointFileFromDisk(dbDirectory, fileToDelete string) (err error) { - absCatchpointFileName := filepath.Join(dbDirectory, fileToDelete) - err = os.Remove(absCatchpointFileName) - if err == nil || os.IsNotExist(err) { - // it's ok if the file doesn't exist. - err = nil - } else { - // we can't delete the file, abort - - return fmt.Errorf("unable to delete old catchpoint file '%s' : %v", absCatchpointFileName, err) - } - splitedDirName := strings.Split(fileToDelete, string(os.PathSeparator)) - - var subDirectoriesToScan []string - //build a list of all the subdirs - currentSubDir := "" - for _, element := range splitedDirName { - currentSubDir = filepath.Join(currentSubDir, element) - subDirectoriesToScan = append(subDirectoriesToScan, currentSubDir) - } - - // iterating over the list of directories. starting from the sub dirs and moving up. - // skipping the file itself. - for i := len(subDirectoriesToScan) - 2; i >= 0; i-- { - absSubdir := filepath.Join(dbDirectory, subDirectoriesToScan[i]) - if _, err := os.Stat(absSubdir); os.IsNotExist(err) { - continue - } - - isEmpty, err := isDirEmpty(absSubdir) - if err != nil { - return fmt.Errorf("unable to read old catchpoint directory '%s' : %v", subDirectoriesToScan[i], err) - } - if isEmpty { - err = os.Remove(absSubdir) - if err != nil { - if os.IsNotExist(err) { - continue - } - return fmt.Errorf("unable to delete old catchpoint directory '%s' : %v", subDirectoriesToScan[i], err) - } - } - } - - return nil -} - -func hashBufV6(affinity uint64, kind hashKind) []byte { - hash := make([]byte, 4+crypto.DigestSize) - // write out the lowest 32 bits of the affinity value. This should improve - // the caching of the trie by allowing recent updates to be in-cache, and - // "older" nodes will be left alone. - for i, prefix := 3, affinity; i >= 0; i, prefix = i-1, prefix>>8 { - // the following takes the prefix & 255 -> hash[i] - hash[i] = byte(prefix) - } - hash[hashKindEncodingIndex] = byte(kind) - return hash -} - -func finishV6(v6hash []byte, prehash []byte) []byte { - entryHash := crypto.Hash(prehash) - copy(v6hash[5:], entryHash[1:]) - return v6hash[:] - -} - -// accountHashBuilderV6 calculates the hash key used for the trie by combining the account address and the account data -func accountHashBuilderV6(addr basics.Address, accountData *baseAccountData, encodedAccountData []byte) []byte { - hashIntPrefix := accountData.UpdateRound - if hashIntPrefix == 0 { - hashIntPrefix = accountData.RewardsBase - } - hash := hashBufV6(hashIntPrefix, accountHK) - // write out the lowest 32 bits of the reward base. This should improve the caching of the trie by allowing - // recent updated to be in-cache, and "older" nodes will be left alone. - - prehash := make([]byte, crypto.DigestSize+len(encodedAccountData)) - copy(prehash[:], addr[:]) - copy(prehash[crypto.DigestSize:], encodedAccountData[:]) - - return finishV6(hash, prehash) -} - -// hashKind enumerates the possible data types hashed into a catchpoint merkle -// trie. Each merkle trie hash includes the hashKind byte at a known-offset. -// By encoding hashKind at a known-offset, it's possible for hash readers to -// disambiguate the hashed resource. -//go:generate stringer -type=hashKind -type hashKind byte - -// Defines known kinds of hashes. Changing an enum ordinal value is a -// breaking change. -const ( - accountHK hashKind = iota - assetHK - appHK - kvHK -) - -// hashKindEncodingIndex defines the []byte offset where the hash kind is -// encoded. -const hashKindEncodingIndex = 4 - -func rdGetCreatableHashKind(rd *resourcesData, a basics.Address, ci basics.CreatableIndex) (hashKind, error) { - if rd.IsAsset() { - return assetHK, nil - } else if rd.IsApp() { - return appHK, nil - } - return accountHK, fmt.Errorf("unknown creatable for addr %s, aidx %d, data %v", a.String(), ci, rd) -} - -// resourcesHashBuilderV6 calculates the hash key used for the trie by combining the creatable's resource data and its index -func resourcesHashBuilderV6(rd *resourcesData, addr basics.Address, cidx basics.CreatableIndex, updateRound uint64, encodedResourceData []byte) ([]byte, error) { - hk, err := rdGetCreatableHashKind(rd, addr, cidx) - if err != nil { - return nil, err - } - - hash := hashBufV6(updateRound, hk) - - prehash := make([]byte, 8+crypto.DigestSize+len(encodedResourceData)) - copy(prehash[:], addr[:]) - binary.LittleEndian.PutUint64(prehash[crypto.DigestSize:], uint64(cidx)) - copy(prehash[crypto.DigestSize+8:], encodedResourceData[:]) - - return finishV6(hash, prehash), nil -} - -// kvHashBuilderV6 calculates the hash key used for the trie by combining the key and value -func kvHashBuilderV6(key string, value []byte) []byte { - hash := hashBufV6(0, kvHK) - - prehash := make([]byte, len(key)+len(value)) - copy(prehash[:], key) - copy(prehash[len(key):], value) - - return finishV6(hash, prehash) -} - -// accountHashBuilder calculates the hash key used for the trie by combining the account address and the account data -func accountHashBuilder(addr basics.Address, accountData basics.AccountData, encodedAccountData []byte) []byte { - hash := make([]byte, 4+crypto.DigestSize) - // write out the lowest 32 bits of the reward base. This should improve the caching of the trie by allowing - // recent updated to be in-cache, and "older" nodes will be left alone. - for i, rewards := 3, accountData.RewardsBase; i >= 0; i, rewards = i-1, rewards>>8 { - // the following takes the rewards & 255 -> hash[i] - hash[i] = byte(rewards) - } - entryHash := crypto.Hash(append(addr[:], encodedAccountData[:]...)) - copy(hash[4:], entryHash[:]) - return hash[:] -} - func (ct *catchpointTracker) catchpointEnabled() bool { return ct.catchpointInterval != 0 } @@ -1520,7 +1321,8 @@ func (ct *catchpointTracker) catchpointEnabled() bool { // initializeHashes initializes account/resource/kv hashes. // as part of the initialization, it tests if a hash table matches to account base and updates the former. func (ct *catchpointTracker) initializeHashes(ctx context.Context, tx *sql.Tx, rnd basics.Round) error { - hashRound, err := accountsHashRound(ctx, tx) + arw := store.NewAccountsSQLReaderWriter(tx) + hashRound, err := arw.AccountsHashRound(ctx) if err != nil { return err } @@ -1528,7 +1330,7 @@ func (ct *catchpointTracker) initializeHashes(ctx context.Context, tx *sql.Tx, r if hashRound != rnd { // if the hashed round is different then the base round, something was modified, and the accounts aren't in sync // with the hashes. - err = resetAccountHashes(ctx, tx) + err = arw.ResetAccountHashes(ctx) if err != nil { return err } @@ -1539,12 +1341,12 @@ func (ct *catchpointTracker) initializeHashes(ctx context.Context, tx *sql.Tx, r } // create the merkle trie for the balances - committer, err := MakeMerkleCommitter(tx, false) + committer, err := store.MakeMerkleCommitter(tx, false) if err != nil { return fmt.Errorf("initializeHashes was unable to makeMerkleCommitter: %v", err) } - trie, err := merkletrie.MakeTrie(committer, TrieMemoryConfig) + trie, err := merkletrie.MakeTrie(committer, store.TrieMemoryConfig) if err != nil { return fmt.Errorf("initializeHashes was unable to MakeTrie: %v", err) } @@ -1585,7 +1387,7 @@ func (ct *catchpointTracker) initializeHashes(ctx context.Context, tx *sql.Tx, r if !added { // we need to translate the "addrid" into actual account address so that // we can report the failure. - addr, err := lookupAccountAddressFromAddressID(ctx, tx, acct.addrid) + addr, err := arw.LookupAccountAddressFromAddressID(ctx, acct.addrid) if err != nil { ct.log.Warnf("initializeHashes attempted to add duplicate acct hash '%s' to merkle trie for account id %d : %v", hex.EncodeToString(acct.digest), acct.addrid, err) } else { @@ -1641,7 +1443,7 @@ func (ct *catchpointTracker) initializeHashes(ctx context.Context, tx *sql.Tx, r if err != nil { return err } - hash := kvHashBuilderV6(string(k), v) + hash := store.KvHashBuilderV6(string(k), v) trieHashCount++ pendingTrieHashes++ added, err := trie.Add(hash) @@ -1671,7 +1473,7 @@ func (ct *catchpointTracker) initializeHashes(ctx context.Context, tx *sql.Tx, r } // we've just updated the merkle trie, update the hashRound to reflect that. - err = updateAccountsHashRound(ctx, tx, rnd) + err = arw.UpdateAccountsHashRound(ctx, rnd) if err != nil { return fmt.Errorf("initializeHashes was unable to update the account hash round to %d: %v", rnd, err) } diff --git a/ledger/catchpointtracker_test.go b/ledger/catchpointtracker_test.go index 7e9b4d19a4..82a34888ec 100644 --- a/ledger/catchpointtracker_test.go +++ b/ledger/catchpointtracker_test.go @@ -39,6 +39,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -92,7 +93,7 @@ func TestGetCatchpointStream(t *testing.T) { filesToCreate := 4 temporaryDirectory := t.TempDir() - catchpointsDirectory := filepath.Join(temporaryDirectory, CatchpointDirName) + catchpointsDirectory := filepath.Join(temporaryDirectory, store.CatchpointDirName) err := os.Mkdir(catchpointsDirectory, 0777) require.NoError(t, err) @@ -100,13 +101,13 @@ func TestGetCatchpointStream(t *testing.T) { // Create the catchpoint files with dummy data for i := 0; i < filesToCreate; i++ { - fileName := filepath.Join(CatchpointDirName, fmt.Sprintf("%d.catchpoint", i)) + fileName := filepath.Join(store.CatchpointDirName, fmt.Sprintf("%d.catchpoint", i)) data := []byte{byte(i), byte(i + 1), byte(i + 2)} err = os.WriteFile(filepath.Join(temporaryDirectory, fileName), data, 0666) require.NoError(t, err) // Store the catchpoint into the database - err := storeCatchpoint(context.Background(), ml.dbs.Wdb.Handle, basics.Round(i), fileName, "", int64(len(data))) + err := ct.catchpointStore.StoreCatchpoint(context.Background(), basics.Round(i), fileName, "", int64(len(data))) require.NoError(t, err) } @@ -126,14 +127,14 @@ func TestGetCatchpointStream(t *testing.T) { require.Equal(t, int64(3), len) // File deleted, but record in the database - err = os.Remove(filepath.Join(temporaryDirectory, CatchpointDirName, "2.catchpoint")) + err = os.Remove(filepath.Join(temporaryDirectory, store.CatchpointDirName, "2.catchpoint")) require.NoError(t, err) reader, err = ct.GetCatchpointStream(basics.Round(2)) require.Equal(t, ledgercore.ErrNoEntry{}, err) require.Nil(t, reader) // File on disk, but database lost the record - err = storeCatchpoint(context.Background(), ml.dbs.Wdb.Handle, basics.Round(3), "", "", 0) + err = ct.catchpointStore.StoreCatchpoint(context.Background(), basics.Round(3), "", "", 0) require.NoError(t, err) reader, err = ct.GetCatchpointStream(basics.Round(3)) require.NoError(t, err) @@ -143,7 +144,7 @@ func TestGetCatchpointStream(t *testing.T) { outData = []byte{3, 4, 5} require.Equal(t, outData, dataRead) - err = deleteStoredCatchpoints(context.Background(), ml.dbs.Wdb.Handle, ct.dbDirectory) + err = ct.catchpointStore.DeleteStoredCatchpoints(context.Background(), ct.dbDirectory) require.NoError(t, err) } @@ -172,7 +173,7 @@ func TestAcctUpdatesDeleteStoredCatchpoints(t *testing.T) { dummyCatchpointFiles := make([]string, dummyCatchpointFilesToCreate) for i := 0; i < dummyCatchpointFilesToCreate; i++ { file := fmt.Sprintf("%s%c%d%c%d%cdummy_catchpoint_file-%d", - CatchpointDirName, os.PathSeparator, + store.CatchpointDirName, os.PathSeparator, i/10, os.PathSeparator, i/2, os.PathSeparator, i) @@ -184,11 +185,11 @@ func TestAcctUpdatesDeleteStoredCatchpoints(t *testing.T) { require.NoError(t, err) err = f.Close() require.NoError(t, err) - err = storeCatchpoint(context.Background(), ml.dbs.Wdb.Handle, basics.Round(i), file, "", 0) + err = ct.catchpointStore.StoreCatchpoint(context.Background(), basics.Round(i), file, "", 0) require.NoError(t, err) } - err := deleteStoredCatchpoints(context.Background(), ml.dbs.Wdb.Handle, ct.dbDirectory) + err := ct.catchpointStore.DeleteStoredCatchpoints(context.Background(), ct.dbDirectory) require.NoError(t, err) // ensure that all the files were deleted. @@ -196,7 +197,7 @@ func TestAcctUpdatesDeleteStoredCatchpoints(t *testing.T) { _, err := os.Open(file) require.True(t, os.IsNotExist(err)) } - fileNames, err := getOldestCatchpointFiles(context.Background(), ml.dbs.Rdb.Handle, dummyCatchpointFilesToCreate, 0) + fileNames, err := ct.catchpointStore.GetOldestCatchpointFiles(context.Background(), dummyCatchpointFilesToCreate, 0) require.NoError(t, err) require.Equal(t, 0, len(fileNames)) } @@ -208,11 +209,11 @@ func TestSchemaUpdateDeleteStoredCatchpoints(t *testing.T) { partitiontest.PartitionTest(t) // we don't want to run this test before the binary is compiled against the latest database upgrade schema. - if accountDBVersion < 6 { + if store.AccountDBVersion < 6 { return } temporaryDirectroy := t.TempDir() - tempCatchpointDir := filepath.Join(temporaryDirectroy, CatchpointDirName) + tempCatchpointDir := filepath.Join(temporaryDirectroy, store.CatchpointDirName) // creating empty catchpoint directories emptyDirPath := path.Join(tempCatchpointDir, "2f", "e1") @@ -249,7 +250,7 @@ func TestSchemaUpdateDeleteStoredCatchpoints(t *testing.T) { _, err = trackerDBInitialize(ml, true, ct.dbDirectory) require.NoError(t, err) - emptyDirs, err := getEmptyDirs(tempCatchpointDir) + emptyDirs, err := store.GetEmptyDirs(tempCatchpointDir) require.NoError(t, err) onlyTempDirEmpty := len(emptyDirs) == 0 require.Equal(t, onlyTempDirEmpty, true) @@ -301,7 +302,7 @@ func TestRecordCatchpointFile(t *testing.T) { context.Background(), accountsRound, time.Second) require.NoError(t, err) - err = ct.createCatchpoint(context.Background(), accountsRound, round, catchpointFirstStageInfo{BiggestChunkLen: biggestChunkLen}, crypto.Digest{}) + err = ct.createCatchpoint(context.Background(), accountsRound, round, store.CatchpointFirstStageInfo{BiggestChunkLen: biggestChunkLen}, crypto.Digest{}) require.NoError(t, err) } @@ -309,7 +310,7 @@ func TestRecordCatchpointFile(t *testing.T) { require.NoError(t, err) require.Equal(t, conf.CatchpointFileHistoryLength, numberOfCatchpointFiles) - emptyDirs, err := getEmptyDirs(temporaryDirectory) + emptyDirs, err := store.GetEmptyDirs(temporaryDirectory) require.NoError(t, err) onlyCatchpointDirEmpty := len(emptyDirs) == 0 || (len(emptyDirs) == 1 && emptyDirs[0] == temporaryDirectory) @@ -340,7 +341,7 @@ func BenchmarkLargeCatchpointDataWriting(b *testing.B) { ct.initialize(cfg, ".") temporaryDirectroy := b.TempDir() - catchpointsDirectory := filepath.Join(temporaryDirectroy, CatchpointDirName) + catchpointsDirectory := filepath.Join(temporaryDirectroy, store.CatchpointDirName) err := os.Mkdir(catchpointsDirectory, 0777) require.NoError(b, err) @@ -353,11 +354,13 @@ func BenchmarkLargeCatchpointDataWriting(b *testing.B) { // at this point, the database was created. We want to fill the accounts data accountsNumber := 6000000 * b.N err = ml.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + arw := store.NewAccountsSQLReaderWriter(tx) + for i := 0; i < accountsNumber-5-2; { // subtract the account we've already created above, plus the sink/reward var updates compactAccountDeltas for k := 0; i < accountsNumber-5-2 && k < 1024; k++ { addr := ledgertesting.RandomAddress() - acctData := baseAccountData{} + acctData := store.BaseAccountData{} acctData.MicroAlgos.Raw = 1 updates.upsert(addr, accountDelta{newAcct: acctData}) i++ @@ -369,7 +372,7 @@ func BenchmarkLargeCatchpointDataWriting(b *testing.B) { } } - return updateAccountsHashRound(ctx, tx, 1) + return arw.UpdateAccountsHashRound(ctx, 1) }) require.NoError(b, err) @@ -864,7 +867,7 @@ func TestFirstStageInfoPruning(t *testing.T) { defer ct.close() temporaryDirectory := t.TempDir() - catchpointsDirectory := filepath.Join(temporaryDirectory, CatchpointDirName) + catchpointsDirectory := filepath.Join(temporaryDirectory, store.CatchpointDirName) err := os.Mkdir(catchpointsDirectory, 0777) require.NoError(t, err) @@ -910,8 +913,7 @@ func TestFirstStageInfoPruning(t *testing.T) { numEntries := uint64(0) i -= basics.Round(cfg.MaxAcctLookback) for i > 0 { - _, recordExists, err := selectCatchpointFirstStageInfo( - context.Background(), ct.dbs.Rdb.Handle, i) + _, recordExists, err := ct.catchpointStore.SelectCatchpointFirstStageInfo(context.Background(), i) require.NoError(t, err) catchpointDataFilePath := @@ -953,7 +955,7 @@ func TestFirstStagePersistence(t *testing.T) { defer ml.Close() tempDirectory := t.TempDir() - catchpointsDirectory := filepath.Join(tempDirectory, CatchpointDirName) + catchpointsDirectory := filepath.Join(tempDirectory, store.CatchpointDirName) cfg := config.GetDefaultLocal() cfg.CatchpointInterval = 4 @@ -1000,14 +1002,15 @@ func TestFirstStagePersistence(t *testing.T) { defer ml2.Close() ml.Close() + cps2 := store.NewCatchpointSQLReaderWriter(ml2.dbs.Wdb.Handle) + // Insert unfinished first stage record. - err = writeCatchpointStateUint64( - context.Background(), ml2.dbs.Wdb.Handle, catchpointStateWritingFirstStageInfo, 1) + err = cps2.WriteCatchpointStateUint64( + context.Background(), store.CatchpointStateWritingFirstStageInfo, 1) require.NoError(t, err) // Delete the database record. - err = deleteOldCatchpointFirstStageInfo( - context.Background(), ml2.dbs.Wdb.Handle, firstStageRound) + err = cps2.DeleteOldCatchpointFirstStageInfo(context.Background(), firstStageRound) require.NoError(t, err) // Create a catchpoint tracker and let it restart catchpoint's first stage. @@ -1021,14 +1024,13 @@ func TestFirstStagePersistence(t *testing.T) { require.Greater(t, info.Size(), int64(1)) // Check that the database record exists. - _, exists, err := selectCatchpointFirstStageInfo( - context.Background(), ml2.dbs.Rdb.Handle, firstStageRound) + _, exists, err := ct2.catchpointStore.SelectCatchpointFirstStageInfo(context.Background(), firstStageRound) require.NoError(t, err) require.True(t, exists) // Check that the unfinished first stage record is deleted. - v, err := readCatchpointStateUint64( - context.Background(), ml2.dbs.Rdb.Handle, catchpointStateWritingFirstStageInfo) + v, err := ct2.catchpointStore.ReadCatchpointStateUint64( + context.Background(), store.CatchpointStateWritingFirstStageInfo) require.NoError(t, err) require.Zero(t, v) } @@ -1055,7 +1057,7 @@ func TestSecondStagePersistence(t *testing.T) { defer ml.Close() tempDirectory := t.TempDir() - catchpointsDirectory := filepath.Join(tempDirectory, CatchpointDirName) + catchpointsDirectory := filepath.Join(tempDirectory, store.CatchpointDirName) cfg := config.GetDefaultLocal() cfg.CatchpointInterval = 4 @@ -1069,7 +1071,7 @@ func TestSecondStagePersistence(t *testing.T) { firstStageRound := secondStageRound - basics.Round(protoParams.CatchpointLookback) catchpointDataFilePath := filepath.Join(catchpointsDirectory, makeCatchpointDataFilePath(firstStageRound)) - var firstStageInfo catchpointFirstStageInfo + var firstStageInfo store.CatchpointFirstStageInfo var catchpointData []byte // Add blocks until the first catchpoint round. @@ -1078,8 +1080,7 @@ func TestSecondStagePersistence(t *testing.T) { // Save first stage info and data file. var exists bool var err error - firstStageInfo, exists, err = selectCatchpointFirstStageInfo( - context.Background(), ml.dbs.Rdb.Handle, firstStageRound) + firstStageInfo, exists, err = ct.catchpointStore.SelectCatchpointFirstStageInfo(context.Background(), firstStageRound) require.NoError(t, err) require.True(t, exists) @@ -1111,7 +1112,7 @@ func TestSecondStagePersistence(t *testing.T) { // Check that the data file exists. catchpointFilePath := - filepath.Join(catchpointsDirectory, makeCatchpointFilePath(secondStageRound)) + filepath.Join(catchpointsDirectory, store.MakeCatchpointFilePath(secondStageRound)) info, err := os.Stat(catchpointFilePath) require.NoError(t, err) @@ -1130,19 +1131,20 @@ func TestSecondStagePersistence(t *testing.T) { err = os.WriteFile(catchpointDataFilePath, catchpointData, 0644) require.NoError(t, err) + cps2 := store.NewCatchpointSQLReaderWriter(ml2.dbs.Wdb.Handle) + // Restore the first stage database record. - err = insertOrReplaceCatchpointFirstStageInfo( - context.Background(), ml2.dbs.Wdb.Handle, firstStageRound, &firstStageInfo) + err = cps2.InsertOrReplaceCatchpointFirstStageInfo(context.Background(), firstStageRound, &firstStageInfo) require.NoError(t, err) // Insert unfinished catchpoint record. - err = insertUnfinishedCatchpoint( - context.Background(), ml2.dbs.Wdb.Handle, secondStageRound, crypto.Digest{}) + err = cps2.InsertUnfinishedCatchpoint( + context.Background(), secondStageRound, crypto.Digest{}) require.NoError(t, err) // Delete the catchpoint file database record. - err = storeCatchpoint( - context.Background(), ml2.dbs.Wdb.Handle, secondStageRound, "", "", 0) + err = cps2.StoreCatchpoint( + context.Background(), secondStageRound, "", "", 0) require.NoError(t, err) // Create a catchpoint tracker and let it restart catchpoint's second stage. @@ -1156,14 +1158,14 @@ func TestSecondStagePersistence(t *testing.T) { require.Greater(t, info.Size(), int64(1)) // Check that the database record exists. - filename, _, _, err := getCatchpoint( - context.Background(), ml2.dbs.Rdb.Handle, secondStageRound) + filename, _, _, err := ct2.catchpointStore.GetCatchpoint( + context.Background(), secondStageRound) require.NoError(t, err) require.NotEmpty(t, filename) // Check that the unfinished catchpoint database record is deleted. - unfinishedCatchpoints, err := selectUnfinishedCatchpoints( - context.Background(), ml2.dbs.Rdb.Handle) + unfinishedCatchpoints, err := ct2.catchpointStore.SelectUnfinishedCatchpoints( + context.Background()) require.NoError(t, err) require.Empty(t, unfinishedCatchpoints) } @@ -1252,8 +1254,8 @@ func TestSecondStageDeletesUnfinishedCatchpointRecord(t *testing.T) { ml2.trackers.waitAccountsWriting() // Check that the unfinished catchpoint database record is deleted. - unfinishedCatchpoints, err := selectUnfinishedCatchpoints( - context.Background(), ml2.dbs.Rdb.Handle) + unfinishedCatchpoints, err := ct2.catchpointStore.SelectUnfinishedCatchpoints( + context.Background()) require.NoError(t, err) require.Empty(t, unfinishedCatchpoints) } @@ -1320,15 +1322,16 @@ func TestSecondStageDeletesUnfinishedCatchpointRecordAfterRestart(t *testing.T) defer ml2.Close() ml.Close() + cps2 := store.NewCatchpointSQLReaderWriter(ml2.dbs.Wdb.Handle) + // Sanity check: first stage record should be deleted. - _, exists, err := selectCatchpointFirstStageInfo( - context.Background(), ml2.dbs.Rdb.Handle, firstStageRound) + _, exists, err := cps2.SelectCatchpointFirstStageInfo(context.Background(), firstStageRound) require.NoError(t, err) require.False(t, exists) // Insert unfinished catchpoint record. - err = insertUnfinishedCatchpoint( - context.Background(), ml2.dbs.Wdb.Handle, secondStageRound, crypto.Digest{}) + err = cps2.InsertUnfinishedCatchpoint( + context.Background(), secondStageRound, crypto.Digest{}) require.NoError(t, err) // Create a catchpoint tracker and let it restart catchpoint's second stage. @@ -1336,8 +1339,8 @@ func TestSecondStageDeletesUnfinishedCatchpointRecordAfterRestart(t *testing.T) defer ct2.close() // Check that the unfinished catchpoint database record is deleted. - unfinishedCatchpoints, err := selectUnfinishedCatchpoints( - context.Background(), ml2.dbs.Rdb.Handle) + unfinishedCatchpoints, err := ct2.catchpointStore.SelectUnfinishedCatchpoints( + context.Background()) require.NoError(t, err) require.Empty(t, unfinishedCatchpoints) } @@ -1350,7 +1353,7 @@ func TestSecondStageDeletesUnfinishedCatchpointRecordAfterRestart(t *testing.T) // before the change != hash calculated now. Accepting the new hash risks // breaking backwards compatibility. // -// The test also confirms each hashKind has at least 1 test case. The check +// The test also confirms each HashKind has at least 1 test case. The check // defends against the addition of a hashed data type without test coverage. func TestHashContract(t *testing.T) { partitiontest.PartitionTest(t) @@ -1359,30 +1362,30 @@ func TestHashContract(t *testing.T) { type testCase struct { genHash func() []byte expectedHex string - expectedHashKind hashKind + expectedHashKind store.HashKind } accountCase := func(genHash func() []byte, expectedHex string) testCase { return testCase{ - genHash, expectedHex, accountHK, + genHash, expectedHex, store.AccountHK, } } resourceAssetCase := func(genHash func() []byte, expectedHex string) testCase { return testCase{ - genHash, expectedHex, assetHK, + genHash, expectedHex, store.AssetHK, } } resourceAppCase := func(genHash func() []byte, expectedHex string) testCase { return testCase{ - genHash, expectedHex, appHK, + genHash, expectedHex, store.AppHK, } } kvCase := func(genHash func() []byte, expectedHex string) testCase { return testCase{ - genHash, expectedHex, kvHK, + genHash, expectedHex, store.KvHK, } } @@ -1391,19 +1394,19 @@ func TestHashContract(t *testing.T) { accounts := []testCase{ accountCase( func() []byte { - b := baseAccountData{ + b := store.BaseAccountData{ UpdateRound: 1024, } - return accountHashBuilderV6(a, &b, protocol.Encode(&b)) + return store.AccountHashBuilderV6(a, &b, protocol.Encode(&b)) }, "0000040000c3c39a72c146dc6bcb87b499b63ef730145a8fe4a187c96e9a52f74ef17f54", ), accountCase( func() []byte { - b := baseAccountData{ + b := store.BaseAccountData{ RewardsBase: 10000, } - return accountHashBuilderV6(a, &b, protocol.Encode(&b)) + return store.AccountHashBuilderV6(a, &b, protocol.Encode(&b)) }, "0000271000804b58bcc81190c3c7343c1db9c737621ff0438104bdd20a25d12aa4e9b6e5", ), @@ -1412,14 +1415,14 @@ func TestHashContract(t *testing.T) { resourceAssets := []testCase{ resourceAssetCase( func() []byte { - r := resourcesData{ + r := store.ResourcesData{ Amount: 1000, Decimals: 3, AssetName: "test", Manager: a, } - bytes, err := resourcesHashBuilderV6(&r, a, 7, 1024, protocol.Encode(&r)) + bytes, err := store.ResourcesHashBuilderV6(&r, a, 7, 1024, protocol.Encode(&r)) require.NoError(t, err) return bytes }, @@ -1430,14 +1433,14 @@ func TestHashContract(t *testing.T) { resourceApps := []testCase{ resourceAppCase( func() []byte { - r := resourcesData{ + r := store.ResourcesData{ ApprovalProgram: []byte{1, 3, 10, 15}, ClearStateProgram: []byte{15, 10, 3, 1}, LocalStateSchemaNumUint: 2, GlobalStateSchemaNumUint: 2, } - bytes, err := resourcesHashBuilderV6(&r, a, 7, 1024, protocol.Encode(&r)) + bytes, err := store.ResourcesHashBuilderV6(&r, a, 7, 1024, protocol.Encode(&r)) require.NoError(t, err) return bytes }, @@ -1448,7 +1451,7 @@ func TestHashContract(t *testing.T) { kvs := []testCase{ kvCase( func() []byte { - return kvHashBuilderV6("sample key", []byte("sample value")) + return store.KvHashBuilderV6("sample key", []byte("sample value")) }, "0000000003cca3d1a8d7d724daa445c795ad277a7a64b351b4b9407f738841282f9c348b", ), @@ -1458,12 +1461,12 @@ func TestHashContract(t *testing.T) { for i, tc := range allCases { t.Run(fmt.Sprintf("index=%d", i), func(t *testing.T) { h := tc.genHash() - require.Equal(t, byte(tc.expectedHashKind), h[hashKindEncodingIndex]) + require.Equal(t, byte(tc.expectedHashKind), h[store.HashKindEncodingIndex]) require.Equal(t, tc.expectedHex, hex.EncodeToString(h)) }) } - hasTestCoverageForKind := func(hk hashKind) bool { + hasTestCoverageForKind := func(hk store.HashKind) bool { for _, c := range allCases { if c.expectedHashKind == hk { return true @@ -1472,9 +1475,10 @@ func TestHashContract(t *testing.T) { return false } + require.True(t, strings.HasPrefix(store.HashKind(255).String(), "HashKind(")) for i := byte(0); i < 255; i++ { - if !strings.HasPrefix(hashKind(i).String(), "hashKind(") { - require.True(t, hasTestCoverageForKind(hashKind(i)), fmt.Sprintf("Missing test coverage for hashKind ordinal value = %d", i)) + if !strings.HasPrefix(store.HashKind(i).String(), "HashKind(") { + require.True(t, hasTestCoverageForKind(store.HashKind(i)), fmt.Sprintf("Missing test coverage for HashKind ordinal value = %d", i)) } } } diff --git a/ledger/catchpointwriter.go b/ledger/catchpointwriter.go index c7f87961b2..e204a8ae74 100644 --- a/ledger/catchpointwriter.go +++ b/ledger/catchpointwriter.go @@ -28,6 +28,7 @@ import ( "github.com/algorand/msgp/msgp" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/protocol" ) @@ -130,12 +131,14 @@ func (chunk catchpointFileChunkV6) empty() bool { } func makeCatchpointWriter(ctx context.Context, filePath string, tx *sql.Tx, maxResourcesPerChunk int) (*catchpointWriter, error) { - totalAccounts, err := totalAccounts(ctx, tx) + arw := store.NewAccountsSQLReaderWriter(tx) + + totalAccounts, err := arw.TotalAccounts(ctx) if err != nil { return nil, err } - totalKVs, err := totalKVs(ctx, tx) + totalKVs, err := arw.TotalKVs(ctx) if err != nil { return nil, err } diff --git a/ledger/catchpointwriter_test.go b/ledger/catchpointwriter_test.go index 5b7563b6e7..0bdac4a2a1 100644 --- a/ledger/catchpointwriter_test.go +++ b/ledger/catchpointwriter_test.go @@ -28,6 +28,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "testing" "github.com/stretchr/testify/require" @@ -40,6 +41,7 @@ import ( "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -195,6 +197,8 @@ func testWriteCatchpoint(t *testing.T, rdb db.Accessor, datapath string, filepat err := rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { writer, err := makeCatchpointWriter(context.Background(), datapath, tx, maxResourcesPerChunk) + arw := store.NewAccountsSQLReaderWriter(tx) + if err != nil { return err } @@ -208,11 +212,11 @@ func testWriteCatchpoint(t *testing.T, rdb db.Accessor, datapath string, filepat totalAccounts = writer.totalAccounts totalChunks = writer.chunkNum biggestChunkLen = writer.biggestChunkLen - accountsRnd, err = accountsRound(tx) + accountsRnd, err = arw.AccountsRound() if err != nil { return } - totals, err = accountsTotals(ctx, tx, false) + totals, err = arw.AccountsTotals(ctx, false) return }) require.NoError(t, err) @@ -234,7 +238,7 @@ func testWriteCatchpoint(t *testing.T, rdb db.Accessor, datapath string, filepat datapath, filepath) require.NoError(t, err) - l := testNewLedgerFromCatchpoint(t, filepath) + l := testNewLedgerFromCatchpoint(t, rdb, filepath) defer l.Close() return catchpointFileHeader @@ -371,7 +375,8 @@ func TestCatchpointReadDatabaseOverflowAccounts(t *testing.T) { readDb := ml.trackerDB().Rdb err = readDb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - expectedTotalAccounts, err := totalAccounts(ctx, tx) + arw := store.NewAccountsSQLReaderWriter(tx) + expectedTotalAccounts, err := arw.TotalAccounts(ctx) if err != nil { return err } @@ -441,7 +446,7 @@ func TestFullCatchpointWriterOverflowAccounts(t *testing.T) { const maxResourcesPerChunk = 5 testWriteCatchpoint(t, ml.trackerDB().Rdb, catchpointDataFilePath, catchpointFilePath, maxResourcesPerChunk) - l := testNewLedgerFromCatchpoint(t, catchpointFilePath) + l := testNewLedgerFromCatchpoint(t, ml.trackerDB().Rdb, catchpointFilePath) defer l.Close() // verify that the account data aligns with what we originally stored : @@ -462,10 +467,12 @@ func TestFullCatchpointWriterOverflowAccounts(t *testing.T) { require.NoError(t, err) defer tx.Rollback() + arw := store.NewAccountsSQLReaderWriter(tx) + // save the existing hash - committer, err := MakeMerkleCommitter(tx, false) + committer, err := store.MakeMerkleCommitter(tx, false) require.NoError(t, err) - trie, err := merkletrie.MakeTrie(committer, TrieMemoryConfig) + trie, err := merkletrie.MakeTrie(committer, store.TrieMemoryConfig) require.NoError(t, err) h1, err := trie.RootHash() @@ -473,13 +480,13 @@ func TestFullCatchpointWriterOverflowAccounts(t *testing.T) { require.NotEmpty(t, h1) // reset hashes - err = resetAccountHashes(ctx, tx) + err = arw.ResetAccountHashes(ctx) require.NoError(t, err) // rebuild the MT - committer, err = MakeMerkleCommitter(tx, false) + committer, err = store.MakeMerkleCommitter(tx, false) require.NoError(t, err) - trie, err = merkletrie.MakeTrie(committer, TrieMemoryConfig) + trie, err = merkletrie.MakeTrie(committer, store.TrieMemoryConfig) require.NoError(t, err) h, err := trie.RootHash() @@ -514,7 +521,7 @@ func TestFullCatchpointWriterOverflowAccounts(t *testing.T) { require.Equal(t, h1, h2) } -func testNewLedgerFromCatchpoint(t *testing.T, filepath string) *Ledger { +func testNewLedgerFromCatchpoint(t *testing.T, catchpointWriterReadAccess db.Accessor, filepath string) *Ledger { // create a ledger. var initState ledgercore.InitState initState.Block.CurrentProtocol = protocol.ConsensusCurrentVersion @@ -567,10 +574,41 @@ func testNewLedgerFromCatchpoint(t *testing.T, filepath string) *Ledger { require.NoError(t, err) err = l.trackerDBs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - err := applyCatchpointStagingBalances(ctx, tx, 0, 0) + crw := store.NewCatchpointSQLReaderWriter(tx) + err := crw.ApplyCatchpointStagingBalances(ctx, 0, 0) return err }) require.NoError(t, err) + + balanceTrieStats := func(db db.Accessor) merkletrie.Stats { + var stats merkletrie.Stats + err = db.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + committer, err := store.MakeMerkleCommitter(tx, false) + if err != nil { + return err + } + trie, err := merkletrie.MakeTrie(committer, store.TrieMemoryConfig) + if err != nil { + return err + } + stats, err = trie.GetStats() + if err != nil { + return err + } + return nil + }) + + require.NoError(t, err) + return stats + } + + ws := balanceTrieStats(catchpointWriterReadAccess) + // Skip invariant check for tests using mocks that do _not_ update + // balancesTrie by checking for zero value stats. + if ws != (merkletrie.Stats{}) { + require.Equal(t, ws, balanceTrieStats(l.trackerDBs.Rdb), "Invariant broken - Catchpoint writer and reader merkle tries should _always_ agree") + } + return l } @@ -604,7 +642,7 @@ func TestFullCatchpointWriter(t *testing.T) { catchpointFilePath := filepath.Join(temporaryDirectory, "15.catchpoint") testWriteCatchpoint(t, ml.trackerDB().Rdb, catchpointDataFilePath, catchpointFilePath, 0) - l := testNewLedgerFromCatchpoint(t, catchpointFilePath) + l := testNewLedgerFromCatchpoint(t, ml.trackerDB().Rdb, catchpointFilePath) defer l.Close() // verify that the account data aligns with what we originally stored : for addr, acct := range accts { @@ -653,10 +691,12 @@ func TestExactAccountChunk(t *testing.T) { cph := testWriteCatchpoint(t, dl.validator.trackerDB().Rdb, catchpointDataFilePath, catchpointFilePath, 0) require.EqualValues(t, cph.TotalChunks, 1) - l := testNewLedgerFromCatchpoint(t, catchpointFilePath) + l := testNewLedgerFromCatchpoint(t, dl.generator.trackerDB().Rdb, catchpointFilePath) defer l.Close() } +// Exercises interactions between transaction evaluation and catchpoint +// generation to confirm catchpoints include expected transactions. func TestCatchpointAfterTxns(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -702,7 +742,7 @@ func TestCatchpointAfterTxns(t *testing.T) { cph := testWriteCatchpoint(t, dl.validator.trackerDB().Rdb, catchpointDataFilePath, catchpointFilePath, 0) require.EqualValues(t, 2, cph.TotalChunks) - l := testNewLedgerFromCatchpoint(t, catchpointFilePath) + l := testNewLedgerFromCatchpoint(t, dl.validator.trackerDB().Rdb, catchpointFilePath) defer l.Close() values, err := l.LookupKeysByPrefix(l.Latest(), "bx:", 10) require.NoError(t, err) @@ -720,7 +760,7 @@ func TestCatchpointAfterTxns(t *testing.T) { // Drive home the point that `last` is _not_ included in the catchpoint by inspecting balance read from catchpoint. { - l = testNewLedgerFromCatchpoint(t, catchpointFilePath) + l = testNewLedgerFromCatchpoint(t, dl.validator.trackerDB().Rdb, catchpointFilePath) defer l.Close() _, _, algos, err := l.LookupLatest(last) require.NoError(t, err) @@ -734,11 +774,14 @@ func TestCatchpointAfterTxns(t *testing.T) { cph = testWriteCatchpoint(t, dl.validator.trackerDB().Rdb, catchpointDataFilePath, catchpointFilePath, 0) require.EqualValues(t, cph.TotalChunks, 3) - l = testNewLedgerFromCatchpoint(t, catchpointFilePath) + l = testNewLedgerFromCatchpoint(t, dl.validator.trackerDB().Rdb, catchpointFilePath) defer l.Close() values, err = l.LookupKeysByPrefix(l.Latest(), "bx:", 10) require.NoError(t, err) require.Len(t, values, 1) + v, err := l.LookupKv(l.Latest(), logic.MakeBoxKey(boxApp, "xxx")) + require.NoError(t, err) + require.Equal(t, strings.Repeat("\x00", 24), string(v)) // Confirm `last` balance is now available in the catchpoint. { @@ -750,6 +793,87 @@ func TestCatchpointAfterTxns(t *testing.T) { } } +// Exercises a sequence of box modifications that caused a bug in +// catchpoint writes. +// +// The problematic sequence of values is: v1 -> v2 -> v1. Where each +// box value is: +// * Part of a transaction that does _not_ modify global/local state. +// * Written to `balancesTrie`. +func TestCatchpointAfterBoxTxns(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + genBalances, addrs, _ := ledgertesting.NewTestGenesis() + dl := NewDoubleLedger(t, genBalances, protocol.ConsensusFuture) + defer dl.Close() + + boxApp := dl.fundedApp(addrs[1], 1_000_000, boxAppSource) + callBox := txntest.Txn{ + Type: "appl", + Sender: addrs[2], + ApplicationID: boxApp, + } + + makeBox := callBox.Args("create", "xxx") + makeBox.Boxes = []transactions.BoxRef{{Index: 0, Name: []byte("xxx")}} + dl.fullBlock(makeBox) + + setBox := callBox.Args("set", "xxx", strings.Repeat("f", 24)) + setBox.Boxes = []transactions.BoxRef{{Index: 0, Name: []byte("xxx")}} + dl.fullBlock(setBox) + + pay := txntest.Txn{ + Type: "pay", + Sender: addrs[0], + Receiver: addrs[1], + Amount: 100000, + } + + // There are 12 accounts in the NewTestGenesis, plus 1 app account, so we + // create more so that we have exactly one chunk's worth, to make sure that + // works without an empty chunk between accounts and kvstore. + for i := 0; i < (BalancesPerCatchpointFileChunk - 13); i++ { + newacctpay := pay + newacctpay.Receiver = ledgertesting.RandomAddress() + dl.fullBlock(&newacctpay) + } + for i := 0; i < 40; i++ { + dl.fullBlock(pay.Noted(strconv.Itoa(i))) + } + + resetBox := callBox.Args("set", "xxx", strings.Repeat("z", 24)) + resetBox.Boxes = []transactions.BoxRef{{Index: 0, Name: []byte("xxx")}} + dl.fullBlock(resetBox) + + for i := 0; i < 40; i++ { + dl.fullBlock(pay.Noted(strconv.Itoa(i))) + } + + dl.fullBlock(setBox.Noted("reset back to f's")) + for i := 0; i < 40; i++ { + dl.fullBlock(pay.Noted(strconv.Itoa(i))) + } + + tempDir := t.TempDir() + + catchpointDataFilePath := filepath.Join(tempDir, t.Name()+".data") + catchpointFilePath := filepath.Join(tempDir, t.Name()+".catchpoint.tar.gz") + + cph := testWriteCatchpoint(t, dl.generator.trackerDB().Rdb, catchpointDataFilePath, catchpointFilePath, 0) + require.EqualValues(t, 2, cph.TotalChunks) + + l := testNewLedgerFromCatchpoint(t, dl.generator.trackerDB().Rdb, catchpointFilePath) + defer l.Close() + + values, err := l.LookupKeysByPrefix(l.Latest(), "bx:", 10) + require.NoError(t, err) + require.Len(t, values, 1) + v, err := l.LookupKv(l.Latest(), logic.MakeBoxKey(boxApp, "xxx")) + require.NoError(t, err) + require.Equal(t, strings.Repeat("f", 24), string(v)) +} + func TestEncodedKVRecordV6Allocbounds(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() diff --git a/ledger/catchupaccessor.go b/ledger/catchupaccessor.go index 5ca08bff73..cbe18ba243 100644 --- a/ledger/catchupaccessor.go +++ b/ledger/catchupaccessor.go @@ -32,6 +32,8 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" + "github.com/algorand/go-algorand/ledger/store/blockdb" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" @@ -92,9 +94,9 @@ type CatchpointCatchupAccessor interface { } type stagingWriter interface { - writeBalances(context.Context, []normalizedAccountBalance) error - writeCreatables(context.Context, []normalizedAccountBalance) error - writeHashes(context.Context, []normalizedAccountBalance) error + writeBalances(context.Context, []store.NormalizedAccountBalance) error + writeCreatables(context.Context, []store.NormalizedAccountBalance) error + writeHashes(context.Context, []store.NormalizedAccountBalance) error writeKVs(context.Context, []encodedKVRecordV6) error isShared() bool } @@ -103,27 +105,41 @@ type stagingWriterImpl struct { wdb db.Accessor } -func (w *stagingWriterImpl) writeBalances(ctx context.Context, balances []normalizedAccountBalance) error { +func (w *stagingWriterImpl) writeBalances(ctx context.Context, balances []store.NormalizedAccountBalance) error { return w.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - return writeCatchpointStagingBalances(ctx, tx, balances) + crw := store.NewCatchpointSQLReaderWriter(tx) + return crw.WriteCatchpointStagingBalances(ctx, balances) }) } func (w *stagingWriterImpl) writeKVs(ctx context.Context, kvrs []encodedKVRecordV6) error { return w.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - return writeCatchpointStagingKVs(ctx, tx, kvrs) + crw := store.NewCatchpointSQLReaderWriter(tx) + + keys := make([][]byte, len(kvrs)) + values := make([][]byte, len(kvrs)) + hashes := make([][]byte, len(kvrs)) + for i := 0; i < len(kvrs); i++ { + keys[i] = kvrs[i].Key + values[i] = kvrs[i].Value + hashes[i] = store.KvHashBuilderV6(string(keys[i]), values[i]) + } + + return crw.WriteCatchpointStagingKVs(ctx, keys, values, hashes) }) } -func (w *stagingWriterImpl) writeCreatables(ctx context.Context, balances []normalizedAccountBalance) error { +func (w *stagingWriterImpl) writeCreatables(ctx context.Context, balances []store.NormalizedAccountBalance) error { return w.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - return writeCatchpointStagingCreatable(ctx, tx, balances) + crw := store.NewCatchpointSQLReaderWriter(tx) + return crw.WriteCatchpointStagingCreatable(ctx, balances) }) } -func (w *stagingWriterImpl) writeHashes(ctx context.Context, balances []normalizedAccountBalance) error { +func (w *stagingWriterImpl) writeHashes(ctx context.Context, balances []store.NormalizedAccountBalance) error { return w.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - err := writeCatchpointStagingHashes(ctx, tx, balances) + crw := store.NewCatchpointSQLReaderWriter(tx) + err := crw.WriteCatchpointStagingHashes(ctx, balances) return err }) } @@ -134,7 +150,8 @@ func (w *stagingWriterImpl) isShared() bool { // catchpointCatchupAccessorImpl is the concrete implementation of the CatchpointCatchupAccessor interface type catchpointCatchupAccessorImpl struct { - ledger *Ledger + ledger *Ledger + catchpointStore catchpointStore stagingWriter stagingWriter @@ -179,18 +196,19 @@ type CatchupAccessorClientLedger interface { // MakeCatchpointCatchupAccessor creates a CatchpointCatchupAccessor given a ledger func MakeCatchpointCatchupAccessor(ledger *Ledger, log logging.Logger) CatchpointCatchupAccessor { return &catchpointCatchupAccessorImpl{ - ledger: ledger, - stagingWriter: &stagingWriterImpl{wdb: ledger.trackerDB().Wdb}, - log: log, + ledger: ledger, + catchpointStore: store.NewCatchpointSQLReaderWriter(ledger.trackerDB().Wdb.Handle), + stagingWriter: &stagingWriterImpl{wdb: ledger.trackerDB().Wdb}, + log: log, } } // GetState returns the current state of the catchpoint catchup func (c *catchpointCatchupAccessorImpl) GetState(ctx context.Context) (state CatchpointCatchupState, err error) { var istate uint64 - istate, err = readCatchpointStateUint64(ctx, c.ledger.trackerDB().Rdb.Handle, catchpointStateCatchupState) + istate, err = c.catchpointStore.ReadCatchpointStateUint64(ctx, store.CatchpointStateCatchupState) if err != nil { - return 0, fmt.Errorf("unable to read catchpoint catchup state '%s': %v", catchpointStateCatchupState, err) + return 0, fmt.Errorf("unable to read catchpoint catchup state '%s': %v", store.CatchpointStateCatchupState, err) } state = CatchpointCatchupState(istate) return @@ -201,18 +219,18 @@ func (c *catchpointCatchupAccessorImpl) SetState(ctx context.Context, state Catc if state < CatchpointCatchupStateInactive || state > catchpointCatchupStateLast { return fmt.Errorf("invalid catchpoint catchup state provided : %d", state) } - err = writeCatchpointStateUint64(ctx, c.ledger.trackerDB().Wdb.Handle, catchpointStateCatchupState, uint64(state)) + err = c.catchpointStore.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupState, uint64(state)) if err != nil { - return fmt.Errorf("unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupState, err) + return fmt.Errorf("unable to write catchpoint catchup state '%s': %v", store.CatchpointStateCatchupState, err) } return } // GetLabel returns the current catchpoint catchup label func (c *catchpointCatchupAccessorImpl) GetLabel(ctx context.Context) (label string, err error) { - label, err = readCatchpointStateString(ctx, c.ledger.trackerDB().Rdb.Handle, catchpointStateCatchupLabel) + label, err = c.catchpointStore.ReadCatchpointStateString(ctx, store.CatchpointStateCatchupLabel) if err != nil { - return "", fmt.Errorf("unable to read catchpoint catchup state '%s': %v", catchpointStateCatchupLabel, err) + return "", fmt.Errorf("unable to read catchpoint catchup state '%s': %v", store.CatchpointStateCatchupLabel, err) } return } @@ -224,9 +242,9 @@ func (c *catchpointCatchupAccessorImpl) SetLabel(ctx context.Context, label stri if err != nil { return } - err = writeCatchpointStateString(ctx, c.ledger.trackerDB().Wdb.Handle, catchpointStateCatchupLabel, label) + err = c.catchpointStore.WriteCatchpointStateString(ctx, store.CatchpointStateCatchupLabel, label) if err != nil { - return fmt.Errorf("unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupLabel, err) + return fmt.Errorf("unable to write catchpoint catchup state '%s': %v", store.CatchpointStateCatchupLabel, err) } return } @@ -240,28 +258,29 @@ func (c *catchpointCatchupAccessorImpl) ResetStagingBalances(ctx context.Context start := time.Now() ledgerResetstagingbalancesCount.Inc(nil) err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - err = resetCatchpointStagingBalances(ctx, tx, newCatchup) + crw := store.NewCatchpointSQLReaderWriter(tx) + err = crw.ResetCatchpointStagingBalances(ctx, newCatchup) if err != nil { return fmt.Errorf("unable to reset catchpoint catchup balances : %v", err) } if !newCatchup { - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupBalancesRound, 0) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupBalancesRound, 0) if err != nil { return err } - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupBlockRound, 0) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupBlockRound, 0) if err != nil { return err } - err = writeCatchpointStateString(ctx, tx, catchpointStateCatchupLabel, "") + err = crw.WriteCatchpointStateString(ctx, store.CatchpointStateCatchupLabel, "") if err != nil { return err } - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupState, 0) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupState, 0) if err != nil { - return fmt.Errorf("unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupState, err) + return fmt.Errorf("unable to write catchpoint catchup state '%s': %v", store.CatchpointStateCatchupState, err) } } return @@ -329,17 +348,20 @@ func (c *catchpointCatchupAccessorImpl) processStagingContent(ctx context.Contex start := time.Now() ledgerProcessstagingcontentCount.Inc(nil) err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupBlockRound, uint64(fileHeader.BlocksRound)) + crw := store.NewCatchpointSQLReaderWriter(tx) + arw := store.NewAccountsSQLReaderWriter(tx) + + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupBlockRound, uint64(fileHeader.BlocksRound)) if err != nil { - return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupBlockRound, err) + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to write catchpoint catchup state '%s': %v", store.CatchpointStateCatchupBlockRound, err) } if fileHeader.Version == CatchpointFileVersionV6 { - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupHashRound, uint64(fileHeader.BlocksRound)) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupHashRound, uint64(fileHeader.BlocksRound)) if err != nil { - return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupHashRound, err) + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to write catchpoint catchup state '%s': %v", store.CatchpointStateCatchupHashRound, err) } } - err = accountsPutTotals(tx, fileHeader.Totals, true) + err = arw.AccountsPutTotals(fileHeader.Totals, true) return }) ledgerProcessstagingcontentMicros.AddMicrosecondsSince(start, nil) @@ -364,7 +386,7 @@ func (c *catchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte start := time.Now() ledgerProcessstagingbalancesCount.Inc(nil) - var normalizedAccountBalances []normalizedAccountBalance + var normalizedAccountBalances []store.NormalizedAccountBalance var expectingMoreEntries []bool var chunkKVs []encodedKVRecordV6 @@ -416,11 +438,11 @@ func (c *catchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte // keep track of number of resources processed for each account for i, balance := range normalizedAccountBalances { // missing resources for this account - if expectingSpecificAccount && balance.address != nextExpectedAccount { + if expectingSpecificAccount && balance.Address != nextExpectedAccount { return fmt.Errorf("processStagingBalances received incomplete chunks for account %v", nextExpectedAccount) } - for _, resData := range balance.resources { + for _, resData := range balance.Resources { if resData.IsApp() && resData.IsOwning() { c.acctResCnt.totalAppParams++ } @@ -436,43 +458,43 @@ func (c *catchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte } // check that counted resources adds up for this account if !expectingMoreEntries[i] { - if c.acctResCnt.totalAppParams != balance.accountData.TotalAppParams { + if c.acctResCnt.totalAppParams != balance.AccountData.TotalAppParams { return fmt.Errorf( "processStagingBalances received %d appParams for account %v, expected %d", c.acctResCnt.totalAppParams, - balance.address, - balance.accountData.TotalAppParams, + balance.Address, + balance.AccountData.TotalAppParams, ) } - if c.acctResCnt.totalAppLocalStates != balance.accountData.TotalAppLocalStates { + if c.acctResCnt.totalAppLocalStates != balance.AccountData.TotalAppLocalStates { return fmt.Errorf( "processStagingBalances received %d appLocalStates for account %v, expected %d", c.acctResCnt.totalAppParams, - balance.address, - balance.accountData.TotalAppLocalStates, + balance.Address, + balance.AccountData.TotalAppLocalStates, ) } - if c.acctResCnt.totalAssetParams != balance.accountData.TotalAssetParams { + if c.acctResCnt.totalAssetParams != balance.AccountData.TotalAssetParams { return fmt.Errorf( "processStagingBalances received %d assetParams for account %v, expected %d", c.acctResCnt.totalAppParams, - balance.address, - balance.accountData.TotalAssetParams, + balance.Address, + balance.AccountData.TotalAssetParams, ) } - if c.acctResCnt.totalAssets != balance.accountData.TotalAssets { + if c.acctResCnt.totalAssets != balance.AccountData.TotalAssets { return fmt.Errorf( "processStagingBalances received %d assets for account %v, expected %d", c.acctResCnt.totalAppParams, - balance.address, - balance.accountData.TotalAssets, + balance.Address, + balance.AccountData.TotalAssets, ) } c.acctResCnt = catchpointAccountResourceCounter{} nextExpectedAccount = basics.Address{} expectingSpecificAccount = false } else { - nextExpectedAccount = balance.address + nextExpectedAccount = balance.Address expectingSpecificAccount = true } } @@ -508,7 +530,7 @@ func (c *catchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte defer wg.Done() hasCreatables := false for _, accBal := range normalizedAccountBalances { - for _, res := range accBal.resources { + for _, res := range accBal.Resources { if res.IsOwning() { hasCreatables = true break @@ -575,8 +597,8 @@ func (c *catchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte progress.ProcessedBytes += uint64(len(bytes)) progress.ProcessedKVs += uint64(len(chunkKVs)) for _, acctBal := range normalizedAccountBalances { - progress.TotalAccountHashes += uint64(len(acctBal.accountHashes)) - if !acctBal.partialBalance { + progress.TotalAccountHashes += uint64(len(acctBal.AccountHashes)) + if !acctBal.PartialBalance { progress.ProcessedAccounts++ } } @@ -600,7 +622,7 @@ func (c *catchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte // The function is _not_ a general purpose way to count hashes by hash kind. func countHashes(hashes [][]byte) (accountCount, kvCount uint64) { for _, hash := range hashes { - if hash[hashKindEncodingIndex] == byte(kvHK) { + if hash[store.HashKindEncodingIndex] == byte(store.KvHK) { kvCount++ } else { accountCount++ @@ -614,9 +636,10 @@ func (c *catchpointCatchupAccessorImpl) BuildMerkleTrie(ctx context.Context, pro wdb := c.ledger.trackerDB().Wdb rdb := c.ledger.trackerDB().Rdb err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + crw := store.NewCatchpointSQLReaderWriter(tx) // creating the index can take a while, so ensure we don't generate false alerts for no good reason. db.ResetTransactionWarnDeadline(ctx, tx, time.Now().Add(120*time.Second)) - return createCatchpointStagingHashesIndex(ctx, tx) + return crw.CreateCatchpointStagingHashesIndex(ctx) }) if err != nil { return @@ -671,16 +694,16 @@ func (c *catchpointCatchupAccessorImpl) BuildMerkleTrie(ctx context.Context, pro uncommitedHashesCount := 0 keepWriting := true accountHashesWritten, kvHashesWritten := uint64(0), uint64(0) - var mc *MerkleCommitter + var mc *store.MerkleCommitter err := wdb.Atomic(func(transactionCtx context.Context, tx *sql.Tx) (err error) { // create the merkle trie for the balances - mc, err = MakeMerkleCommitter(tx, true) + mc, err = store.MakeMerkleCommitter(tx, true) if err != nil { return } - trie, err = merkletrie.MakeTrie(mc, TrieMemoryConfig) + trie, err = merkletrie.MakeTrie(mc, store.TrieMemoryConfig) return err }) if err != nil { @@ -703,7 +726,7 @@ func (c *catchpointCatchupAccessorImpl) BuildMerkleTrie(ctx context.Context, pro } err = rdb.Atomic(func(transactionCtx context.Context, tx *sql.Tx) (err error) { - mc, err = MakeMerkleCommitter(tx, true) + mc, err = store.MakeMerkleCommitter(tx, true) if err != nil { return } @@ -712,7 +735,7 @@ func (c *catchpointCatchupAccessorImpl) BuildMerkleTrie(ctx context.Context, pro var added bool added, err = trie.Add(hash) if !added { - return fmt.Errorf("CatchpointCatchupAccessorImpl::BuildMerkleTrie: The provided catchpoint file contained the same account more than once. hash = '%s' hash kind = %s", hex.EncodeToString(hash), hashKind(hash[hashKindEncodingIndex])) + return fmt.Errorf("CatchpointCatchupAccessorImpl::BuildMerkleTrie: The provided catchpoint file contained the same account more than once. hash = '%s' hash kind = %s", hex.EncodeToString(hash), store.HashKind(hash[store.HashKindEncodingIndex])) } if err != nil { return @@ -735,7 +758,7 @@ func (c *catchpointCatchupAccessorImpl) BuildMerkleTrie(ctx context.Context, pro err = wdb.Atomic(func(transactionCtx context.Context, tx *sql.Tx) (err error) { // set a long 30-second window for the evict before warning is generated. db.ResetTransactionWarnDeadline(transactionCtx, tx, time.Now().Add(30*time.Second)) - mc, err = MakeMerkleCommitter(tx, true) + mc, err = store.MakeMerkleCommitter(tx, true) if err != nil { return } @@ -765,7 +788,7 @@ func (c *catchpointCatchupAccessorImpl) BuildMerkleTrie(ctx context.Context, pro err = wdb.Atomic(func(transactionCtx context.Context, tx *sql.Tx) (err error) { // set a long 30-second window for the evict before warning is generated. db.ResetTransactionWarnDeadline(transactionCtx, tx, time.Now().Add(30*time.Second)) - mc, err = MakeMerkleCommitter(tx, true) + mc, err = store.MakeMerkleCommitter(tx, true) if err != nil { return } @@ -794,9 +817,9 @@ func (c *catchpointCatchupAccessorImpl) BuildMerkleTrie(ctx context.Context, pro // GetCatchupBlockRound returns the latest block round matching the current catchpoint func (c *catchpointCatchupAccessorImpl) GetCatchupBlockRound(ctx context.Context) (round basics.Round, err error) { var iRound uint64 - iRound, err = readCatchpointStateUint64(ctx, c.ledger.trackerDB().Rdb.Handle, catchpointStateCatchupBlockRound) + iRound, err = c.catchpointStore.ReadCatchpointStateUint64(ctx, store.CatchpointStateCatchupBlockRound) if err != nil { - return 0, fmt.Errorf("unable to read catchpoint catchup state '%s': %v", catchpointStateCatchupBlockRound, err) + return 0, fmt.Errorf("unable to read catchpoint catchup state '%s': %v", store.CatchpointStateCatchpointLookback, err) } return basics.Round(iRound), nil } @@ -809,28 +832,29 @@ func (c *catchpointCatchupAccessorImpl) VerifyCatchpoint(ctx context.Context, bl var totals ledgercore.AccountTotals var catchpointLabel string - catchpointLabel, err = readCatchpointStateString(ctx, rdb.Handle, catchpointStateCatchupLabel) + catchpointLabel, err = c.catchpointStore.ReadCatchpointStateString(ctx, store.CatchpointStateCatchupLabel) if err != nil { - return fmt.Errorf("unable to read catchpoint catchup state '%s': %v", catchpointStateCatchupLabel, err) + return fmt.Errorf("unable to read catchpoint catchup state '%s': %v", store.CatchpointStateCatchupLabel, err) } var iRound uint64 - iRound, err = readCatchpointStateUint64(ctx, rdb.Handle, catchpointStateCatchupBlockRound) + iRound, err = c.catchpointStore.ReadCatchpointStateUint64(ctx, store.CatchpointStateCatchupBlockRound) if err != nil { - return fmt.Errorf("unable to read catchpoint catchup state '%s': %v", catchpointStateCatchupBlockRound, err) + return fmt.Errorf("unable to read catchpoint catchup state '%s': %v", store.CatchpointStateCatchupBlockRound, err) } blockRound = basics.Round(iRound) start := time.Now() ledgerVerifycatchpointCount.Inc(nil) err = rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + arw := store.NewAccountsSQLReaderWriter(tx) // create the merkle trie for the balances - mc, err0 := MakeMerkleCommitter(tx, true) + mc, err0 := store.MakeMerkleCommitter(tx, true) if err0 != nil { return fmt.Errorf("unable to make MerkleCommitter: %v", err0) } var trie *merkletrie.Trie - trie, err = merkletrie.MakeTrie(mc, TrieMemoryConfig) + trie, err = merkletrie.MakeTrie(mc, store.TrieMemoryConfig) if err != nil { return fmt.Errorf("unable to make trie: %v", err) } @@ -840,7 +864,7 @@ func (c *catchpointCatchupAccessorImpl) VerifyCatchpoint(ctx context.Context, bl return fmt.Errorf("unable to get trie root hash: %v", err) } - totals, err = accountsTotals(ctx, tx, true) + totals, err = arw.AccountsTotals(ctx, true) if err != nil { return fmt.Errorf("unable to get accounts totals: %v", err) } @@ -851,7 +875,7 @@ func (c *catchpointCatchupAccessorImpl) VerifyCatchpoint(ctx context.Context, bl return err } if blockRound != blk.Round() { - return fmt.Errorf("block round in block header doesn't match block round in catchpoint") + return fmt.Errorf("block round in block header doesn't match block round in catchpoint: %d != %d", blockRound, blk.Round()) } catchpointLabelMaker := ledgercore.MakeCatchpointLabel(blockRound, blk.Digest(), balancesHash, totals) @@ -876,9 +900,10 @@ func (c *catchpointCatchupAccessorImpl) StoreBalancesRound(ctx context.Context, start := time.Now() ledgerStorebalancesroundCount.Inc(nil) err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupBalancesRound, uint64(balancesRound)) + crw := store.NewCatchpointSQLReaderWriter(tx) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupBalancesRound, uint64(balancesRound)) if err != nil { - return fmt.Errorf("CatchpointCatchupAccessorImpl::StoreBalancesRound: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupBalancesRound, err) + return fmt.Errorf("CatchpointCatchupAccessorImpl::StoreBalancesRound: unable to write catchpoint catchup state '%s': %v", store.CatchpointStateCatchupBalancesRound, err) } return }) @@ -892,7 +917,7 @@ func (c *catchpointCatchupAccessorImpl) StoreFirstBlock(ctx context.Context, blk start := time.Now() ledgerStorefirstblockCount.Inc(nil) err = blockDbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - return blockStartCatchupStaging(tx, *blk) + return blockdb.BlockStartCatchupStaging(tx, *blk) }) ledgerStorefirstblockMicros.AddMicrosecondsSince(start, nil) if err != nil { @@ -907,7 +932,7 @@ func (c *catchpointCatchupAccessorImpl) StoreBlock(ctx context.Context, blk *boo start := time.Now() ledgerCatchpointStoreblockCount.Inc(nil) err = blockDbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - return blockPutStaging(tx, *blk) + return blockdb.BlockPutStaging(tx, *blk) }) ledgerCatchpointStoreblockMicros.AddMicrosecondsSince(start, nil) if err != nil { @@ -923,10 +948,10 @@ func (c *catchpointCatchupAccessorImpl) FinishBlocks(ctx context.Context, applyC ledgerCatchpointFinishblocksCount.Inc(nil) err = blockDbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { if applyChanges { - return blockCompleteCatchup(tx) + return blockdb.BlockCompleteCatchup(tx) } // TODO: unused, either actually implement cleanup on catchpoint failure, or delete this - return blockAbortCatchup(tx) + return blockdb.BlockAbortCatchup(tx) }) ledgerCatchpointFinishblocksMicros.AddMicrosecondsSince(start, nil) if err != nil { @@ -941,7 +966,7 @@ func (c *catchpointCatchupAccessorImpl) EnsureFirstBlock(ctx context.Context) (b start := time.Now() ledgerCatchpointEnsureblock1Count.Inc(nil) err = blockDbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - blk, err = blockEnsureSingleBlock(tx) + blk, err = blockdb.BlockEnsureSingleBlock(tx) return }) ledgerCatchpointEnsureblock1Micros.AddMicrosecondsSince(start, nil) @@ -972,26 +997,29 @@ func (c *catchpointCatchupAccessorImpl) finishBalances(ctx context.Context) (err start := time.Now() ledgerCatchpointFinishBalsCount.Inc(nil) err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + crw := store.NewCatchpointSQLReaderWriter(tx) + arw := store.NewAccountsSQLReaderWriter(tx) + var balancesRound, hashRound uint64 var totals ledgercore.AccountTotals - balancesRound, err = readCatchpointStateUint64(ctx, tx, catchpointStateCatchupBalancesRound) + balancesRound, err = crw.ReadCatchpointStateUint64(ctx, store.CatchpointStateCatchupBalancesRound) if err != nil { return err } - hashRound, err = readCatchpointStateUint64(ctx, tx, catchpointStateCatchupHashRound) + hashRound, err = crw.ReadCatchpointStateUint64(ctx, store.CatchpointStateCatchupHashRound) if err != nil { return err } - totals, err = accountsTotals(ctx, tx, true) + totals, err = arw.AccountsTotals(ctx, true) if err != nil { return err } if hashRound == 0 { - err = resetAccountHashes(ctx, tx) + err = arw.ResetAccountHashes(ctx) if err != nil { return err } @@ -1003,66 +1031,66 @@ func (c *catchpointCatchupAccessorImpl) finishBalances(ctx context.Context) (err // it might be necessary to restore it into the latest database version. To do that, one // will need to run the 6->7 migration code manually here or in a similar function to create // onlineaccounts and other V7 tables. - err = accountsReset(ctx, tx) + err = arw.AccountsReset(ctx) if err != nil { return err } { - tp := trackerDBParams{ - initAccounts: c.ledger.GenesisAccounts(), - initProto: c.ledger.GenesisProtoVersion(), - genesisHash: c.ledger.GenesisHash(), - fromCatchpoint: true, - catchpointEnabled: c.ledger.catchpoint.catchpointEnabled(), - dbPathPrefix: c.ledger.catchpoint.dbDirectory, - blockDb: c.ledger.blockDBs, + tp := store.TrackerDBParams{ + InitAccounts: c.ledger.GenesisAccounts(), + InitProto: c.ledger.GenesisProtoVersion(), + GenesisHash: c.ledger.GenesisHash(), + FromCatchpoint: true, + CatchpointEnabled: c.ledger.catchpoint.catchpointEnabled(), + DbPathPrefix: c.ledger.catchpoint.dbDirectory, + BlockDb: c.ledger.blockDBs, } - _, err = runMigrations(ctx, tx, tp, c.ledger.log, 6 /*target database version*/) + _, err = store.RunMigrations(ctx, tx, tp, c.ledger.log, 6 /*target database version*/) if err != nil { return err } } - err = applyCatchpointStagingBalances(ctx, tx, basics.Round(balancesRound), basics.Round(hashRound)) + err = crw.ApplyCatchpointStagingBalances(ctx, basics.Round(balancesRound), basics.Round(hashRound)) if err != nil { return err } - err = accountsPutTotals(tx, totals, false) + err = arw.AccountsPutTotals(totals, false) if err != nil { return err } - err = resetCatchpointStagingBalances(ctx, tx, false) + err = crw.ResetCatchpointStagingBalances(ctx, false) if err != nil { return err } - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupBalancesRound, 0) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupBalancesRound, 0) if err != nil { return err } - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupBlockRound, 0) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupBlockRound, 0) if err != nil { return err } - err = writeCatchpointStateString(ctx, tx, catchpointStateCatchupLabel, "") + err = crw.WriteCatchpointStateString(ctx, store.CatchpointStateCatchupLabel, "") if err != nil { return err } if hashRound != 0 { - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupHashRound, 0) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupHashRound, 0) if err != nil { return err } } - err = writeCatchpointStateUint64(ctx, tx, catchpointStateCatchupState, 0) + err = crw.WriteCatchpointStateUint64(ctx, store.CatchpointStateCatchupState, 0) if err != nil { - return fmt.Errorf("unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupState, err) + return fmt.Errorf("unable to write catchpoint catchup state '%s': %v", store.CatchpointStateCatchupState, err) } return diff --git a/ledger/catchupaccessor_test.go b/ledger/catchupaccessor_test.go index b95a924ce3..d79d636e3c 100644 --- a/ledger/catchupaccessor_test.go +++ b/ledger/catchupaccessor_test.go @@ -33,6 +33,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -59,7 +60,7 @@ func createTestingEncodedChunks(accountsCount uint64) (encodedAccountChunks [][] chunk.Balances = make([]encodedBalanceRecordV6, chunkSize) for i := uint64(0); i < chunkSize; i++ { var randomAccount encodedBalanceRecordV6 - accountData := baseAccountData{} + accountData := store.BaseAccountData{} accountData.MicroAlgos.Raw = crypto.RandUint63() randomAccount.AccountData = protocol.Encode(&accountData) // have the first account be the zero address @@ -407,7 +408,7 @@ func TestCatchupAccessorResourceCountMismatch(t *testing.T) { var balances catchpointFileChunkV6 balances.Balances = make([]encodedBalanceRecordV6, 1) var randomAccount encodedBalanceRecordV6 - accountData := baseAccountData{} + accountData := store.BaseAccountData{} accountData.MicroAlgos.Raw = crypto.RandUint63() accountData.TotalAppParams = 1 randomAccount.AccountData = protocol.Encode(&accountData) @@ -426,11 +427,11 @@ type testStagingWriter struct { hashes map[[4 + crypto.DigestSize]byte]int } -func (w *testStagingWriter) writeBalances(ctx context.Context, balances []normalizedAccountBalance) error { +func (w *testStagingWriter) writeBalances(ctx context.Context, balances []store.NormalizedAccountBalance) error { return nil } -func (w *testStagingWriter) writeCreatables(ctx context.Context, balances []normalizedAccountBalance) error { +func (w *testStagingWriter) writeCreatables(ctx context.Context, balances []store.NormalizedAccountBalance) error { return nil } @@ -438,9 +439,9 @@ func (w *testStagingWriter) writeKVs(ctx context.Context, kvrs []encodedKVRecord return nil } -func (w *testStagingWriter) writeHashes(ctx context.Context, balances []normalizedAccountBalance) error { +func (w *testStagingWriter) writeHashes(ctx context.Context, balances []store.NormalizedAccountBalance) error { for _, bal := range balances { - for _, hash := range bal.accountHashes { + for _, hash := range bal.AccountHashes { var key [4 + crypto.DigestSize]byte require.Len(w.t, hash, 4+crypto.DigestSize) copy(key[:], hash) @@ -475,8 +476,8 @@ func TestCatchupAccessorProcessStagingBalances(t *testing.T) { } catchpointAccessor := makeTestCatchpointCatchupAccessor(&l, log, writer) - randomSimpleBaseAcct := func() baseAccountData { - accountData := baseAccountData{ + randomSimpleBaseAcct := func() store.BaseAccountData { + accountData := store.BaseAccountData{ RewardsBase: crypto.RandUint63(), MicroAlgos: basics.MicroAlgos{Raw: crypto.RandUint63()}, AuthAddr: ledgertesting.RandomAddress(), @@ -484,7 +485,7 @@ func TestCatchupAccessorProcessStagingBalances(t *testing.T) { return accountData } - encodedBalanceRecordFromBase := func(addr basics.Address, base baseAccountData, resources map[uint64]msgp.Raw, more bool) encodedBalanceRecordV6 { + encodedBalanceRecordFromBase := func(addr basics.Address, base store.BaseAccountData, resources map[uint64]msgp.Raw, more bool) encodedBalanceRecordV6 { ebr := encodedBalanceRecordV6{ Address: addr, AccountData: protocol.Encode(&base), @@ -516,7 +517,7 @@ func TestCatchupAccessorProcessStagingBalances(t *testing.T) { acctX.TotalAssets = acctXNumRes acctXRes1 := make(map[uint64]msgp.Raw, acctXNumRes/2+1) acctXRes2 := make(map[uint64]msgp.Raw, acctXNumRes/2) - emptyRes := resourcesData{ResourceFlags: resourceFlagsEmptyAsset} + emptyRes := store.ResourcesData{ResourceFlags: store.ResourceFlagsEmptyAsset} emptyResEnc := protocol.Encode(&emptyRes) for i := 0; i < acctXNumRes; i++ { if i <= acctXNumRes/2 { diff --git a/ledger/hashkind_string.go b/ledger/hashkind_string.go deleted file mode 100644 index 6549ae63b6..0000000000 --- a/ledger/hashkind_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=hashKind"; DO NOT EDIT. - -package ledger - -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[accountHK-0] - _ = x[assetHK-1] - _ = x[appHK-2] - _ = x[kvHK-3] -} - -const _hashKind_name = "accountHKassetHKappHKkvHK" - -var _hashKind_index = [...]uint8{0, 9, 16, 21, 25} - -func (i hashKind) String() string { - if i >= hashKind(len(_hashKind_index)-1) { - return "hashKind(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _hashKind_name[_hashKind_index[i]:_hashKind_index[i+1]] -} diff --git a/ledger/internal/appcow.go b/ledger/internal/appcow.go index 05d3d3df7d..d38fbfacc6 100644 --- a/ledger/internal/appcow.go +++ b/ledger/internal/appcow.go @@ -456,6 +456,8 @@ func MakeDebugBalances(l LedgerForCowBase, round basics.Round, proto protocol.Co func (cb *roundCowState) StatefulEval(gi int, params *logic.EvalParams, aidx basics.AppIndex, program []byte) (pass bool, evalDelta transactions.EvalDelta, err error) { // Make a child cow to eval our program in calf := cb.child(1) + defer calf.recycle() + params.Ledger = calf // Eval the program diff --git a/ledger/internal/cow.go b/ledger/internal/cow.go index d329bfb176..e27dd777f5 100644 --- a/ledger/internal/cow.go +++ b/ledger/internal/cow.go @@ -19,6 +19,7 @@ package internal import ( "errors" "fmt" + "sync" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" @@ -66,6 +67,7 @@ type roundCowParent interface { kvGet(key string) ([]byte, bool, error) } +// When adding new fields make sure to clear them in the roundCowState.recycle() as well to avoid dirty state type roundCowState struct { lookupParent roundCowParent commitParent *roundCowState @@ -93,6 +95,12 @@ type roundCowState struct { prevTotals ledgercore.AccountTotals } +var childPool = sync.Pool{ + New: func() interface{} { + return &roundCowState{} + }, +} + func makeRoundCowState(b roundCowParent, hdr bookkeeping.BlockHeader, proto config.ConsensusParams, prevTimestamp int64, prevTotals ledgercore.AccountTotals, hint int) *roundCowState { cb := roundCowState{ lookupParent: b, @@ -209,13 +217,13 @@ func (cb *roundCowState) lookupAssetHolding(addr basics.Address, aidx basics.Ass func (cb *roundCowState) checkDup(firstValid, lastValid basics.Round, txid transactions.Txid, txl ledgercore.Txlease) error { _, present := cb.mods.Txids[txid] if present { - return &ledgercore.TransactionInLedgerError{Txid: txid} + return &ledgercore.TransactionInLedgerError{Txid: txid, InBlockEvaluator: true} } if cb.proto.SupportTransactionLeases && (txl.Lease != [32]byte{}) { expires, ok := cb.mods.Txleases[txl] if ok && cb.mods.Hdr.Round <= expires { - return ledgercore.MakeLeaseInLedgerError(txid, txl) + return ledgercore.MakeLeaseInLedgerError(txid, txl, true) } } @@ -258,19 +266,23 @@ func (cb *roundCowState) SetStateProofNextRound(rnd basics.Round) { } func (cb *roundCowState) child(hint int) *roundCowState { - ch := roundCowState{ - lookupParent: cb, - commitParent: cb, - proto: cb.proto, - mods: ledgercore.MakeStateDelta(cb.mods.Hdr, cb.mods.PrevTimestamp, hint, cb.mods.StateProofNext), - sdeltas: make(map[basics.Address]map[storagePtr]*storageDelta), + ch := childPool.Get().(*roundCowState) + ch.lookupParent = cb + ch.commitParent = cb + ch.proto = cb.proto + ch.mods.PopulateStateDelta(cb.mods.Hdr, cb.mods.PrevTimestamp, hint, cb.mods.StateProofNext) + + if ch.sdeltas == nil { + ch.sdeltas = make(map[basics.Address]map[storagePtr]*storageDelta) } if cb.compatibilityMode { ch.compatibilityMode = cb.compatibilityMode - ch.compatibilityGetKeyCache = make(map[basics.Address]map[storagePtr]uint64) + if ch.compatibilityGetKeyCache == nil { + ch.compatibilityGetKeyCache = make(map[basics.Address]map[storagePtr]uint64) + } } - return &ch + return ch } func (cb *roundCowState) commitToParent() { @@ -313,6 +325,29 @@ func (cb *roundCowState) modifiedAccounts() []basics.Address { return cb.mods.Accts.ModifiedAccounts() } +// reset resets the roundcowstate +func (cb *roundCowState) reset() { + cb.lookupParent = nil + cb.commitParent = nil + cb.proto = config.ConsensusParams{} + cb.mods.Reset() + cb.txnCount = 0 + for addr := range cb.sdeltas { + delete(cb.sdeltas, addr) + } + cb.compatibilityMode = false + for addr := range cb.compatibilityGetKeyCache { + delete(cb.compatibilityGetKeyCache, addr) + } + cb.prevTotals = ledgercore.AccountTotals{} +} + +// recycle resets the roundcowstate and returns it to the sync.Pool +func (cb *roundCowState) recycle() { + cb.reset() + childPool.Put(cb) +} + // errUnsupportedChildCowTotalCalculation is returned by CalculateTotals when called by a child roundCowState instance var errUnsupportedChildCowTotalCalculation = errors.New("the method CalculateTotals should be called only on a top-level roundCowState") diff --git a/ledger/internal/cow_test.go b/ledger/internal/cow_test.go index bd942d63ee..14eb748fb6 100644 --- a/ledger/internal/cow_test.go +++ b/ledger/internal/cow_test.go @@ -17,6 +17,7 @@ package internal import ( + "reflect" "testing" "github.com/stretchr/testify/require" @@ -190,3 +191,58 @@ func TestCowBalance(t *testing.T) { c1.commitToParent() checkCowByUpdate(t, c0, updates2) } + +func BenchmarkCowChild(b *testing.B) { + b.ReportAllocs() + cow := makeRoundCowState(nil, bookkeeping.BlockHeader{}, config.ConsensusParams{}, 10000, ledgercore.AccountTotals{}, 16) + for i := 0; i < b.N; i++ { + cow.child(16) + cow.recycle() + } +} + +// Ideally we'd be able to randomize the roundCowState but can't do it via reflection +// since it' can't set unexported fields. This test just makes sure that all of the existing +// fields are correctly reset but won't be able to catch any new fields added. +func TestCowChildReset(t *testing.T) { + partitiontest.PartitionTest(t) + cow := makeRoundCowState(nil, bookkeeping.BlockHeader{}, config.ConsensusParams{}, 10000, ledgercore.AccountTotals{}, 16) + calf := cow.child(16) + require.NotEmpty(t, calf) + calf.compatibilityMode = true + calf.reset() + // simple fields + require.Zero(t, calf.commitParent) + require.Zero(t, calf.proto) + require.Zero(t, calf.txnCount) + require.Zero(t, calf.compatibilityMode) + require.Zero(t, calf.prevTotals) + + // alloced map + require.NotZero(t, calf.sdeltas) + require.Empty(t, calf.sdeltas) +} + +func TestCowChildReflect(t *testing.T) { + partitiontest.PartitionTest(t) + + cowFieldNames := map[string]struct{}{ + "lookupParent": {}, + "commitParent": {}, + "proto": {}, + "mods": {}, + "txnCount": {}, + "sdeltas": {}, + "compatibilityMode": {}, + "compatibilityGetKeyCache": {}, + "prevTotals": {}, + } + + cow := roundCowState{} + v := reflect.ValueOf(cow) + st := v.Type() + for i := 0; i < v.NumField(); i++ { + reflectedCowName := st.Field(i).Name + require.Containsf(t, cowFieldNames, reflectedCowName, "new field:\"%v\" added to roundCowState, please update roundCowState.reset() to handle it before fixing the test", reflectedCowName) + } +} diff --git a/ledger/internal/eval.go b/ledger/internal/eval.go index b42d24f6b5..5fc3c51fa7 100644 --- a/ledger/internal/eval.go +++ b/ledger/internal/eval.go @@ -835,7 +835,10 @@ func (eval *BlockEvaluator) TestTransactionGroup(txgroup []transactions.SignedTx } if len(txgroup) > eval.proto.MaxTxGroupSize { - return fmt.Errorf("group size %d exceeds maximum %d", len(txgroup), eval.proto.MaxTxGroupSize) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("group size %d exceeds maximum %d", len(txgroup), eval.proto.MaxTxGroupSize), + Reason: ledgercore.TxGroupMalformedErrorReasonExceedMaxSize, + } } var group transactions.TxGroup @@ -847,8 +850,11 @@ func (eval *BlockEvaluator) TestTransactionGroup(txgroup []transactions.SignedTx // Make sure all transactions in group have the same group value if txn.Txn.Group != txgroup[0].Txn.Group { - return fmt.Errorf("transactionGroup: inconsistent group values: %v != %v", - txn.Txn.Group, txgroup[0].Txn.Group) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: inconsistent group values: %v != %v", + txn.Txn.Group, txgroup[0].Txn.Group), + Reason: ledgercore.TxGroupMalformedErrorReasonInconsistentGroupID, + } } if !txn.Txn.Group.IsZero() { @@ -857,15 +863,21 @@ func (eval *BlockEvaluator) TestTransactionGroup(txgroup []transactions.SignedTx group.TxGroupHashes = append(group.TxGroupHashes, crypto.Digest(txWithoutGroup.ID())) } else if len(txgroup) > 1 { - return fmt.Errorf("transactionGroup: [%d] had zero Group but was submitted in a group of %d", gi, len(txgroup)) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: [%d] had zero Group but was submitted in a group of %d", gi, len(txgroup)), + Reason: ledgercore.TxGroupMalformedErrorReasonEmptyGroupID, + } } } // If we had a non-zero Group value, check that all group members are present. if group.TxGroupHashes != nil { if txgroup[0].Txn.Group != crypto.HashObj(group) { - return fmt.Errorf("transactionGroup: incomplete group: %v != %v (%v)", - txgroup[0].Txn.Group, crypto.HashObj(group), group) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: incomplete group: %v != %v (%v)", + txgroup[0].Txn.Group, crypto.HashObj(group), group), + Reason: ledgercore.TxGroupMalformedErrorReasonIncompleteGroup, + } } } @@ -884,7 +896,8 @@ func (eval *BlockEvaluator) TestTransaction(txn transactions.SignedTxn) error { err = txn.Txn.WellFormed(eval.specials, eval.proto) if err != nil { - return fmt.Errorf("transaction %v: malformed: %v", txn.ID(), err) + txnErr := ledgercore.TxnNotWellFormedError(fmt.Sprintf("transaction %v: malformed: %v", txn.ID(), err)) + return &txnErr } // Transaction already in the ledger? @@ -926,7 +939,10 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit } if len(txgroup) > eval.proto.MaxTxGroupSize { - return fmt.Errorf("group size %d exceeds maximum %d", len(txgroup), eval.proto.MaxTxGroupSize) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("group size %d exceeds maximum %d", len(txgroup), eval.proto.MaxTxGroupSize), + Reason: ledgercore.TxGroupMalformedErrorReasonExceedMaxSize, + } } var txibs []transactions.SignedTxnInBlock @@ -934,6 +950,8 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit var groupTxBytes int cow := eval.state.child(len(txgroup)) + defer cow.recycle() + evalParams := logic.NewEvalParams(txgroup, &eval.proto, &eval.specials) // Evaluate each transaction in the group @@ -957,8 +975,11 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit // Make sure all transactions in group have the same group value if txad.SignedTxn.Txn.Group != txgroup[0].SignedTxn.Txn.Group { - return fmt.Errorf("transactionGroup: inconsistent group values: %v != %v", - txad.SignedTxn.Txn.Group, txgroup[0].SignedTxn.Txn.Group) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: inconsistent group values: %v != %v", + txad.SignedTxn.Txn.Group, txgroup[0].SignedTxn.Txn.Group), + Reason: ledgercore.TxGroupMalformedErrorReasonInconsistentGroupID, + } } if !txad.SignedTxn.Txn.Group.IsZero() { @@ -967,15 +988,21 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit group.TxGroupHashes = append(group.TxGroupHashes, crypto.Digest(txWithoutGroup.ID())) } else if len(txgroup) > 1 { - return fmt.Errorf("transactionGroup: [%d] had zero Group but was submitted in a group of %d", gi, len(txgroup)) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: [%d] had zero Group but was submitted in a group of %d", gi, len(txgroup)), + Reason: ledgercore.TxGroupMalformedErrorReasonEmptyGroupID, + } } } // If we had a non-zero Group value, check that all group members are present. if group.TxGroupHashes != nil { if txgroup[0].SignedTxn.Txn.Group != crypto.HashObj(group) { - return fmt.Errorf("transactionGroup: incomplete group: %v != %v (%v)", - txgroup[0].SignedTxn.Txn.Group, crypto.HashObj(group), group) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: incomplete group: %v != %v (%v)", + txgroup[0].SignedTxn.Txn.Group, crypto.HashObj(group), group), + Reason: ledgercore.TxGroupMalformedErrorReasonIncompleteGroup, + } } } diff --git a/ledger/internal/eval_test.go b/ledger/internal/eval_test.go index 495ff60970..d04294e4cc 100644 --- a/ledger/internal/eval_test.go +++ b/ledger/internal/eval_test.go @@ -676,7 +676,7 @@ func (ledger *evalTestLedger) CheckDup(currentProto config.ConsensusParams, curr } currentTxid := txn.Txn.ID() if bytes.Equal(txid[:], currentTxid[:]) { - return &ledgercore.TransactionInLedgerError{Txid: txid} + return &ledgercore.TransactionInLedgerError{Txid: txid, InBlockEvaluator: false} } } } diff --git a/ledger/internal/prefetcher/prefetcher.go b/ledger/internal/prefetcher/prefetcher.go index b7223806ff..07b95c57d4 100644 --- a/ledger/internal/prefetcher/prefetcher.go +++ b/ledger/internal/prefetcher/prefetcher.go @@ -270,14 +270,18 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { totalBalances := 0 totalResources := 0 - groupsReady := make([]groupTask, len(p.txnGroups)) + // initialize empty groupTasks for groupsReady + groupsReady := make([]*groupTask, len(p.txnGroups)) + for i := range groupsReady { + groupsReady[i] = new(groupTask) // this ensures each allocated groupTask is 64-bit aligned + } // Add fee sink to the first group if len(p.txnGroups) > 0 { // the feeSinkAddr is known to be non-empty feeSinkPreloader := &preloaderTask{ address: &p.feeSinkAddr, - groupTasks: []*groupTask{&groupsReady[0]}, + groupTasks: []*groupTask{groupsReady[0]}, groupTasksIndices: []int{0}, } groupsReady[0].balancesCount = 1 @@ -288,7 +292,7 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { // iterate over the transaction groups and add all their account addresses to the list queue := &tasksQueue for i := range p.txnGroups { - task := &groupsReady[i] + task := groupsReady[i] for j := range p.txnGroups[i] { stxn := &p.txnGroups[i][j] switch stxn.Txn.Type { @@ -380,7 +384,7 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { groupDoneCh := make(chan groupTaskDone, len(groupsReady)) const dependencyFreeGroup = -int64(^uint64(0)/2) - 1 for grpIdx := range groupsReady { - gr := &groupsReady[grpIdx] + gr := groupsReady[grpIdx] gr.groupTaskIndex = int64(grpIdx) gr.incompleteCount = int64(gr.balancesCount + gr.resourcesCount) gr.balances = allBalances[usedBalances : usedBalances+gr.balancesCount] diff --git a/ledger/ledger.go b/ledger/ledger.go index 09ec1b3cd3..12c4148cf7 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -35,6 +35,7 @@ import ( "github.com/algorand/go-algorand/ledger/apply" "github.com/algorand/go-algorand/ledger/internal" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store/blockdb" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" @@ -230,7 +231,7 @@ func (l *Ledger) reloadLedger() error { } // post-init actions - if trackerDBInitParams.vacuumOnStartup || l.cfg.OptimizeAccountsDatabaseOnStartup { + if trackerDBInitParams.VacuumOnStartup || l.cfg.OptimizeAccountsDatabaseOnStartup { err = l.accts.vacuumDatabase(context.Background()) if err != nil { return err @@ -251,12 +252,12 @@ func (l *Ledger) verifyMatchingGenesisHash() (err error) { start := time.Now() ledgerVerifygenhashCount.Inc(nil) err = l.blockDBs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - latest, err := blockLatest(tx) + latest, err := blockdb.BlockLatest(tx) if err != nil { return err } - hdr, err := blockGetHdr(tx, latest) + hdr, err := blockdb.BlockGetHdr(tx, latest) if err != nil { return err } @@ -340,7 +341,7 @@ func (l *Ledger) setSynchronousMode(ctx context.Context, synchronousMode db.Sync // - creates and populates it with genesis blocks // - ensures DB is in good shape for archival mode and resets it if not func initBlocksDB(tx *sql.Tx, l *Ledger, initBlocks []bookkeeping.Block, isArchival bool) (err error) { - err = blockInit(tx, initBlocks) + err = blockdb.BlockInit(tx, initBlocks) if err != nil { err = fmt.Errorf("initBlocksDB.blockInit %v", err) return err @@ -348,7 +349,7 @@ func initBlocksDB(tx *sql.Tx, l *Ledger, initBlocks []bookkeeping.Block, isArchi // in archival mode check if DB contains all blocks up to the latest if isArchival { - earliest, err := blockEarliest(tx) + earliest, err := blockdb.BlockEarliest(tx) if err != nil { err = fmt.Errorf("initBlocksDB.blockEarliest %v", err) return err @@ -358,12 +359,12 @@ func initBlocksDB(tx *sql.Tx, l *Ledger, initBlocks []bookkeeping.Block, isArchi // So reset the DB and init it again if earliest != basics.Round(0) { l.log.Warnf("resetting blocks DB (earliest block is %v)", earliest) - err := blockResetDB(tx) + err := blockdb.BlockResetDB(tx) if err != nil { err = fmt.Errorf("initBlocksDB.blockResetDB %v", err) return err } - err = blockInit(tx, initBlocks) + err = blockdb.BlockInit(tx, initBlocks) if err != nil { err = fmt.Errorf("initBlocksDB.blockInit 2 %v", err) return err @@ -443,6 +444,13 @@ func (l *Ledger) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableTy return l.accts.GetCreatorForRound(l.blockQ.latest(), cidx, ctype) } +// GetStateDeltaForRound retrieves a ledgercore.StateDelta from the accountUpdates cache for the requested rnd +func (l *Ledger) GetStateDeltaForRound(rnd basics.Round) (ledgercore.StateDelta, error) { + l.trackerMu.RLock() + defer l.trackerMu.RUnlock() + return l.accts.lookupStateDelta(rnd) +} + // VotersForStateProof returns the top online accounts at round rnd. // The result might be nil, even with err=nil, if there are no voters // for that round because state proofs were not enabled. diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index 7482a668df..785281eade 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -41,6 +41,7 @@ import ( "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/transactions/verify" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -2223,35 +2224,26 @@ func TestLedgerReloadTxTailHistoryAccess(t *testing.T) { // reset tables and re-init again, similary to the catchpount apply code // since the ledger has only genesis accounts, this recreates them err = l.trackerDBs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - err0 := accountsReset(ctx, tx) + arw := store.NewAccountsSQLReaderWriter(tx) + err0 := arw.AccountsReset(ctx) if err0 != nil { return err0 } - tp := trackerDBParams{ - initAccounts: l.GenesisAccounts(), - initProto: l.GenesisProtoVersion(), - genesisHash: l.GenesisHash(), - fromCatchpoint: true, - catchpointEnabled: l.catchpoint.catchpointEnabled(), - dbPathPrefix: l.catchpoint.dbDirectory, - blockDb: l.blockDBs, + tp := store.TrackerDBParams{ + InitAccounts: l.GenesisAccounts(), + InitProto: l.GenesisProtoVersion(), + GenesisHash: l.GenesisHash(), + FromCatchpoint: true, + CatchpointEnabled: l.catchpoint.catchpointEnabled(), + DbPathPrefix: l.catchpoint.dbDirectory, + BlockDb: l.blockDBs, } - _, err0 = runMigrations(ctx, tx, tp, l.log, preReleaseDBVersion /*target database version*/) + _, err0 = store.RunMigrations(ctx, tx, tp, l.log, preReleaseDBVersion /*target database version*/) if err0 != nil { return err0 } - // trackers need new talbes, create in order to allow commits - if err0 := accountsCreateOnlineAccountsTable(ctx, tx); err0 != nil { - return err0 - } - if err0 := accountsCreateTxTailTable(ctx, tx); err0 != nil { - return err0 - } - if err0 := accountsCreateOnlineRoundParamsTable(ctx, tx); err0 != nil { - return err0 - } - if err0 := accountsCreateCatchpointFirstStageInfoTable(ctx, tx); err0 != nil { + if err0 := store.AccountsUpdateSchemaTest(ctx, tx); err != nil { return err0 } @@ -2384,10 +2376,10 @@ int %d // 10001000 func TestLedgerMigrateV6ShrinkDeltas(t *testing.T) { partitiontest.PartitionTest(t) - prevAccountDBVersion := accountDBVersion - accountDBVersion = 6 + prevAccountDBVersion := store.AccountDBVersion + store.AccountDBVersion = 6 defer func() { - accountDBVersion = prevAccountDBVersion + store.AccountDBVersion = prevAccountDBVersion }() dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) testProtocolVersion := protocol.ConsensusVersion("test-protocol-migrate-shrink-deltas") @@ -2411,21 +2403,7 @@ func TestLedgerMigrateV6ShrinkDeltas(t *testing.T) { }() // create tables so online accounts can still be written err = trackerDB.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - if err := accountsCreateOnlineAccountsTable(ctx, tx); err != nil { - return err - } - if err := accountsCreateTxTailTable(ctx, tx); err != nil { - return err - } - if err := accountsCreateOnlineRoundParamsTable(ctx, tx); err != nil { - return err - } - if err := accountsCreateCatchpointFirstStageInfoTable(ctx, tx); err != nil { - return err - } - // this line creates kvstore table, even if it is not required in accountDBVersion 6 -> 7 - // or in later version where we need kvstore table, this test will fail - if err := accountsCreateBoxTable(ctx, tx); err != nil { + if err := store.AccountsUpdateSchemaTest(ctx, tx); err != nil { return err } return nil @@ -2599,7 +2577,7 @@ func TestLedgerMigrateV6ShrinkDeltas(t *testing.T) { l.Close() cfg.MaxAcctLookback = shorterLookback - accountDBVersion = 7 + store.AccountDBVersion = 7 // delete tables since we want to check they can be made from other data err = trackerDB.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { if _, err := tx.ExecContext(ctx, "DROP TABLE IF EXISTS onlineaccounts"); err != nil { diff --git a/ledger/ledgercore/error.go b/ledger/ledgercore/error.go index 5ce0898b7e..28311973c0 100644 --- a/ledger/ledgercore/error.go +++ b/ledger/ledgercore/error.go @@ -27,9 +27,19 @@ import ( // ErrNoSpace indicates insufficient space for transaction in block var ErrNoSpace = errors.New("block does not have space for transaction") -// TransactionInLedgerError is returned when a transaction cannot be added because it has already been done +// TxnNotWellFormedError indicates a transaction was not well-formed when evaluated by the BlockEvaluator +//msgp:ignore TxnNotWellFormedError +type TxnNotWellFormedError string + +func (err *TxnNotWellFormedError) Error() string { + return string(*err) +} + +// TransactionInLedgerError is returned when a transaction cannot be added because it has already been committed, either +// to the blockchain's ledger or to the history of changes tracked by a BlockEvaluator. type TransactionInLedgerError struct { - Txid transactions.Txid + Txid transactions.Txid + InBlockEvaluator bool } // Error satisfies builtin interface `error` @@ -39,15 +49,17 @@ func (tile TransactionInLedgerError) Error() string { // LeaseInLedgerError is returned when a transaction cannot be added because it has a lease that already being used in the relevant rounds type LeaseInLedgerError struct { - txid transactions.Txid - lease Txlease + txid transactions.Txid + lease Txlease + InBlockEvaluator bool } // MakeLeaseInLedgerError builds a LeaseInLedgerError object -func MakeLeaseInLedgerError(txid transactions.Txid, lease Txlease) *LeaseInLedgerError { +func MakeLeaseInLedgerError(txid transactions.Txid, lease Txlease, inBlockEvaluator bool) *LeaseInLedgerError { return &LeaseInLedgerError{ - txid: txid, - lease: lease, + txid: txid, + lease: lease, + InBlockEvaluator: inBlockEvaluator, } } @@ -107,3 +119,30 @@ type ErrNonSequentialBlockEval struct { func (err ErrNonSequentialBlockEval) Error() string { return fmt.Sprintf("block evaluation for round %d requires sequential evaluation while the latest round is %d", err.EvaluatorRound, err.LatestRound) } + +// TxGroupMalformedErrorReasonCode is a reason code for TxGroupMalformed +//msgp:ignore TxGroupMalformedErrorReasonCode +type TxGroupMalformedErrorReasonCode int + +const ( + // TxGroupMalformedErrorReasonGeneric is a generic (not specific) reason code + TxGroupMalformedErrorReasonGeneric TxGroupMalformedErrorReasonCode = iota + // TxGroupMalformedErrorReasonExceedMaxSize indicates too large txgroup + TxGroupMalformedErrorReasonExceedMaxSize + // TxGroupMalformedErrorReasonInconsistentGroupID indicates different group IDs in a txgroup + TxGroupMalformedErrorReasonInconsistentGroupID + // TxGroupMalformedErrorReasonEmptyGroupID is for empty group ID but multiple transactions in a txgroup + TxGroupMalformedErrorReasonEmptyGroupID + // TxGroupMalformedErrorReasonIncompleteGroup indicates expected group ID does not match to provided + TxGroupMalformedErrorReasonIncompleteGroup +) + +// TxGroupMalformedError indicates txgroup has group ID problems or too large +type TxGroupMalformedError struct { + Msg string + Reason TxGroupMalformedErrorReasonCode +} + +func (e *TxGroupMalformedError) Error() string { + return e.Msg +} diff --git a/ledger/ledgercore/statedelta.go b/ledger/ledgercore/statedelta.go index 6df155a0b4..688c8d093b 100644 --- a/ledger/ledgercore/statedelta.go +++ b/ledger/ledgercore/statedelta.go @@ -85,6 +85,9 @@ type KvValueDelta struct { } // StateDelta describes the delta between a given round to the previous round +// If adding a new field not explicitly allocated by PopulateStateDelta, make sure to reset +// it in .ReuseStateDelta to avoid dirty memory errors. +// If adding fields make sure to add them to the .Reset() method to avoid dirty state type StateDelta struct { // modified new accounts Accts AccountDeltas @@ -115,7 +118,7 @@ type StateDelta struct { PrevTimestamp int64 // initial hint for allocating data structures for StateDelta - initialTransactionsCount int + initialHint int // The account totals reflecting the changes in this StateDelta object. Totals AccountTotals @@ -173,6 +176,7 @@ type AssetResourceRecord struct { // do that, each of the arrays here is constructed as a pair of (slice, map). // The map would point the address/address+creatable id onto the index of the // element within the slice. +// If adding fields make sure to add them to the .reset() method to avoid dirty state type AccountDeltas struct { // Actual data. If an account is deleted, `Accts` contains the BalanceRecord // with an empty `AccountData` and a populated `Addr`. @@ -191,22 +195,31 @@ type AccountDeltas struct { assetResourcesCache map[AccountAsset]int } -// MakeStateDelta creates a new instance of StateDelta. +// MakeStateDelta creates a new instance of StateDelta // hint is amount of transactions for evaluation, 2 * hint is for sender and receiver balance records. // This does not play well for AssetConfig and ApplicationCall transactions on scale -func MakeStateDelta(hdr *bookkeeping.BlockHeader, prevTimestamp int64, hint int, stateProofNext basics.Round) StateDelta { - return StateDelta{ - Accts: MakeAccountDeltas(hint), - Txids: make(map[transactions.Txid]IncludedTransactions, hint), - // asset or application creation are considered as rare events so do not pre-allocate space for them - Hdr: hdr, - StateProofNext: stateProofNext, - PrevTimestamp: prevTimestamp, - initialTransactionsCount: hint, +func MakeStateDelta(hdr *bookkeeping.BlockHeader, prevTimestamp int64, hint int, stateProofNext basics.Round) (sd StateDelta) { + sd.PopulateStateDelta(hdr, prevTimestamp, hint, stateProofNext) + return +} + +// PopulateStateDelta populates an existing StateDelta struct. +// Used as a helper for MakeStateDelta as well as for re-using already allocated structs from sync.Pool +func (sd *StateDelta) PopulateStateDelta(hdr *bookkeeping.BlockHeader, prevTimestamp int64, hint int, stateProofNext basics.Round) { + if sd.Txids == nil { + sd.Txids = make(map[transactions.Txid]IncludedTransactions, hint) + } + if sd.Accts.notAllocated() { + sd.Accts = MakeAccountDeltas(hint) + sd.initialHint = hint } + sd.Hdr = hdr + sd.StateProofNext = stateProofNext + sd.PrevTimestamp = prevTimestamp } // MakeAccountDeltas creates account delta +// if adding new fields make sure to add them to the .reset() and .isEmpty() methods func MakeAccountDeltas(hint int) AccountDeltas { return AccountDeltas{ Accts: make([]BalanceRecord, 0, hint*2), @@ -214,6 +227,54 @@ func MakeAccountDeltas(hint int) AccountDeltas { } } +// Reset resets the StateDelta for re-use with sync.Pool +func (sd *StateDelta) Reset() { + sd.Accts.reset() + for txid := range sd.Txids { + delete(sd.Txids, txid) + } + for txLease := range sd.Txleases { + delete(sd.Txleases, txLease) + } + for creatableIndex := range sd.Creatables { + delete(sd.Creatables, creatableIndex) + } + for key := range sd.KvMods { + delete(sd.KvMods, key) + } + sd.Totals = AccountTotals{} + + // these fields are going to be populated on next use but resetting them anyway for safety. + // we are not resetting sd.initialHint since it should only be reset if reallocating AccountDeltas + sd.Hdr = nil + sd.StateProofNext = basics.Round(0) + sd.PrevTimestamp = 0 +} + +// reset clears out allocated slices from AccountDeltas struct for reuse with sync.Pool +func (ad *AccountDeltas) reset() { + // reset the slices + ad.Accts = ad.Accts[:0] + ad.AppResources = ad.AppResources[:0] + ad.AssetResources = ad.AssetResources[:0] + + // reset the maps + for address := range ad.acctsCache { + delete(ad.acctsCache, address) + } + for aApp := range ad.appResourcesCache { + delete(ad.appResourcesCache, aApp) + } + for aAsset := range ad.assetResourcesCache { + delete(ad.assetResourcesCache, aAsset) + } +} + +// notAllocated returns true if any of the fields allocated by MakeAccountDeltas is nil +func (ad *AccountDeltas) notAllocated() bool { + return ad.Accts == nil || ad.acctsCache == nil +} + // GetData lookups AccountData by address func (ad AccountDeltas) GetData(addr basics.Address) (AccountData, bool) { idx, ok := ad.acctsCache[addr] @@ -439,8 +500,8 @@ func (sd *StateDelta) OptimizeAllocatedMemory(maxBalLookback uint64) { // acctsCache takes up 64 bytes per entry, and is saved for 320 rounds // realloc if original allocation capacity greater than length of data, and space difference is significant - if 2*sd.initialTransactionsCount > len(sd.Accts.acctsCache) && - uint64(2*sd.initialTransactionsCount-len(sd.Accts.acctsCache))*accountMapCacheEntrySize*maxBalLookback > stateDeltaTargetOptimizationThreshold { + if 2*sd.initialHint > len(sd.Accts.acctsCache) && + uint64(2*sd.initialHint-len(sd.Accts.acctsCache))*accountMapCacheEntrySize*maxBalLookback > stateDeltaTargetOptimizationThreshold { acctsCache := make(map[basics.Address]int, len(sd.Accts.acctsCache)) for k, v := range sd.Accts.acctsCache { acctsCache[k] = v diff --git a/ledger/ledgercore/statedelta_test.go b/ledger/ledgercore/statedelta_test.go index 3947c8e91f..aeae35b36c 100644 --- a/ledger/ledgercore/statedelta_test.go +++ b/ledger/ledgercore/statedelta_test.go @@ -17,12 +17,15 @@ package ledgercore import ( + "reflect" "testing" "github.com/stretchr/testify/require" "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/test/partitiontest" ) @@ -125,6 +128,104 @@ func TestMakeStateDeltaMaps(t *testing.T) { } +func TestStateDeltaReset(t *testing.T) { + partitiontest.PartitionTest(t) + + txid := transactions.Transaction{}.ID() + sd := MakeStateDelta(&bookkeeping.BlockHeader{}, 123, 456, basics.Round(789)) + // populate StateDelta maps with some data + sd.Txids[txid] = IncludedTransactions{LastValid: basics.Round(30)} + sd.AddTxLease(Txlease{}, basics.Round(10)) + sd.AddCreatable(basics.CreatableIndex(5), ModifiedCreatable{}) + sd.AddKvMod("key", KvValueDelta{Data: []byte("value")}) + + // populate AccountDelta maps with some data + sd.Accts.acctsCache[randomAddress()] = 1 + sd.Accts.appResourcesCache = make(map[AccountApp]int) + sd.Accts.appResourcesCache[AccountApp{Address: randomAddress()}] = 2 + sd.Accts.assetResourcesCache = make(map[AccountAsset]int) + sd.Accts.assetResourcesCache[AccountAsset{Address: randomAddress()}] = 3 + + sd.Reset() + + // StateDeltas simple fields + require.Zero(t, sd.Hdr) + require.Zero(t, sd.StateProofNext) + require.Zero(t, sd.PrevTimestamp) + require.Zero(t, sd.Totals) + + // required allocated maps + require.NotZero(t, sd.Txids) + require.Empty(t, sd.Txids) + + // optional allocated maps + require.Empty(t, sd.Txleases) + require.Empty(t, sd.KvMods) + require.Empty(t, sd.Creatables) + + // check AccountDeltas + require.NotZero(t, sd.Accts) + + // required AccountDeltas fields + require.NotZero(t, sd.Accts.Accts) + require.Empty(t, sd.Accts.Accts) + require.NotZero(t, sd.Accts.acctsCache) + require.Empty(t, sd.Accts.acctsCache) + + // optional AccountDeltas fields + require.Empty(t, sd.Accts.AppResources) + require.Empty(t, sd.Accts.AssetResources) + require.Empty(t, sd.Accts.assetResourcesCache) + require.Empty(t, sd.Accts.appResourcesCache) + +} + +func TestStateDeltaReflect(t *testing.T) { + partitiontest.PartitionTest(t) + + stateDeltaFieldNames := map[string]struct{}{ + "Accts": {}, + "KvMods": {}, + "Txids": {}, + "Txleases": {}, + "Creatables": {}, + "Hdr": {}, + "StateProofNext": {}, + "PrevTimestamp": {}, + "initialHint": {}, + "Totals": {}, + } + + sd := StateDelta{} + v := reflect.ValueOf(sd) + st := v.Type() + for i := 0; i < v.NumField(); i++ { + reflectedStateDeltaName := st.Field(i).Name + require.Containsf(t, stateDeltaFieldNames, reflectedStateDeltaName, "new field:\"%v\" added to StateDelta, please update StateDelta.Reset() to handle it before fixing the test", reflectedStateDeltaName) + } +} + +func TestAccountDeltaReflect(t *testing.T) { + partitiontest.PartitionTest(t) + + AccountDeltaFieldNames := map[string]struct{}{ + "Accts": {}, + "acctsCache": {}, + "AppResources": {}, + "appResourcesCache": {}, + "AssetResources": {}, + "assetResourcesCache": {}, + } + + sd := AccountDeltas{} + v := reflect.ValueOf(sd) + st := v.Type() + for i := 0; i < v.NumField(); i++ { + reflectedAccountDeltaName := st.Field(i).Name + require.Containsf(t, AccountDeltaFieldNames, reflectedAccountDeltaName, "new field:\"%v\" added to AccountDeltas, please update AccountDeltas.reset() to handle it before fixing the test", reflectedAccountDeltaName) + } +} + func BenchmarkMakeStateDelta(b *testing.B) { hint := 23000 b.ReportAllocs() diff --git a/ledger/lruaccts.go b/ledger/lruaccts.go index f698f9de76..13e639a95a 100644 --- a/ledger/lruaccts.go +++ b/ledger/lruaccts.go @@ -18,6 +18,7 @@ package ledger import ( "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" ) @@ -32,7 +33,7 @@ type lruAccounts struct { accounts map[basics.Address]*persistedAccountDataListNode // pendingAccounts are used as a way to avoid taking a write-lock. When the caller needs to "materialize" these, // it would call flushPendingWrites and these would be merged into the accounts/accountsList - pendingAccounts chan persistedAccountData + pendingAccounts chan store.PersistedAccountData // log interface; used for logging the threshold event. log logging.Logger // pendingWritesWarnThreshold is the threshold beyond we would write a warning for exceeding the number of pendingAccounts entries @@ -47,7 +48,7 @@ type lruAccounts struct { func (m *lruAccounts) init(log logging.Logger, pendingWrites int, pendingWritesWarnThreshold int) { m.accountsList = newPersistedAccountList().allocateFreeNodes(pendingWrites) m.accounts = make(map[basics.Address]*persistedAccountDataListNode, pendingWrites) - m.pendingAccounts = make(chan persistedAccountData, pendingWrites) + m.pendingAccounts = make(chan store.PersistedAccountData, pendingWrites) m.notFound = make(map[basics.Address]struct{}, pendingWrites) m.pendingNotFound = make(chan basics.Address, pendingWrites) m.log = log @@ -56,11 +57,11 @@ func (m *lruAccounts) init(log logging.Logger, pendingWrites int, pendingWritesW // read the persistedAccountData object that the lruAccounts has for the given address. // thread locking semantics : read lock -func (m *lruAccounts) read(addr basics.Address) (data persistedAccountData, has bool) { +func (m *lruAccounts) read(addr basics.Address) (data store.PersistedAccountData, has bool) { if el := m.accounts[addr]; el != nil { return *el.Value, true } - return persistedAccountData{}, false + return store.PersistedAccountData{}, false } // readNotFound returns whether we have attempted to read this address but it did not exist in the db. @@ -103,7 +104,7 @@ outer2: // writePending write a single persistedAccountData entry to the pendingAccounts buffer. // the function doesn't block, and in case of a buffer overflow the entry would not be added. // thread locking semantics : no lock is required. -func (m *lruAccounts) writePending(acct persistedAccountData) { +func (m *lruAccounts) writePending(acct store.PersistedAccountData) { select { case m.pendingAccounts <- acct: default: @@ -125,17 +126,17 @@ func (m *lruAccounts) writeNotFoundPending(addr basics.Address) { // version of what's already on the cache or not. In all cases, the entry is going // to be promoted to the front of the list. // thread locking semantics : write lock -func (m *lruAccounts) write(acctData persistedAccountData) { - if el := m.accounts[acctData.addr]; el != nil { +func (m *lruAccounts) write(acctData store.PersistedAccountData) { + if el := m.accounts[acctData.Addr]; el != nil { // already exists; is it a newer ? - if el.Value.before(&acctData) { + if el.Value.Before(&acctData) { // we update with a newer version. el.Value = &acctData } m.accountsList.moveToFront(el) } else { // new entry. - m.accounts[acctData.addr] = m.accountsList.pushFront(&acctData) + m.accounts[acctData.Addr] = m.accountsList.pushFront(&acctData) } } @@ -148,7 +149,7 @@ func (m *lruAccounts) prune(newSize int) (removed int) { break } back := m.accountsList.back() - delete(m.accounts, back.Value.addr) + delete(m.accounts, back.Value.Addr) m.accountsList.remove(back) removed++ } diff --git a/ledger/lruaccts_test.go b/ledger/lruaccts_test.go index a5f8509013..30f1f082c6 100644 --- a/ledger/lruaccts_test.go +++ b/ledger/lruaccts_test.go @@ -25,6 +25,7 @@ import ( "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -38,11 +39,11 @@ func TestLRUBasicAccounts(t *testing.T) { accountsNum := 50 // write 50 accounts for i := 0; i < accountsNum; i++ { - acct := persistedAccountData{ - addr: basics.Address(crypto.Hash([]byte{byte(i)})), - round: basics.Round(i), - rowid: int64(i), - accountData: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + acct := store.PersistedAccountData{ + Addr: basics.Address(crypto.Hash([]byte{byte(i)})), + Round: basics.Round(i), + Rowid: int64(i), + AccountData: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } baseAcct.write(acct) } @@ -52,10 +53,10 @@ func TestLRUBasicAccounts(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) acct, has := baseAcct.read(addr) require.True(t, has) - require.Equal(t, basics.Round(i), acct.round) - require.Equal(t, addr, acct.addr) - require.Equal(t, uint64(i), acct.accountData.MicroAlgos.Raw) - require.Equal(t, int64(i), acct.rowid) + require.Equal(t, basics.Round(i), acct.Round) + require.Equal(t, addr, acct.Addr) + require.Equal(t, uint64(i), acct.AccountData.MicroAlgos.Raw) + require.Equal(t, int64(i), acct.Rowid) } // verify expected missing entries @@ -63,7 +64,7 @@ func TestLRUBasicAccounts(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) acct, has := baseAcct.read(addr) require.False(t, has) - require.Equal(t, persistedAccountData{}, acct) + require.Equal(t, store.PersistedAccountData{}, acct) } baseAcct.prune(accountsNum / 2) @@ -76,13 +77,13 @@ func TestLRUBasicAccounts(t *testing.T) { if i >= accountsNum/2 && i < accountsNum { // expected to have it. require.True(t, has) - require.Equal(t, basics.Round(i), acct.round) - require.Equal(t, addr, acct.addr) - require.Equal(t, uint64(i), acct.accountData.MicroAlgos.Raw) - require.Equal(t, int64(i), acct.rowid) + require.Equal(t, basics.Round(i), acct.Round) + require.Equal(t, addr, acct.Addr) + require.Equal(t, uint64(i), acct.AccountData.MicroAlgos.Raw) + require.Equal(t, int64(i), acct.Rowid) } else { require.False(t, has) - require.Equal(t, persistedAccountData{}, acct) + require.Equal(t, store.PersistedAccountData{}, acct) } } } @@ -97,11 +98,11 @@ func TestLRUAccountsPendingWrites(t *testing.T) { for i := 0; i < accountsNum; i++ { go func(i int) { time.Sleep(time.Duration((crypto.RandUint64() % 50)) * time.Millisecond) - acct := persistedAccountData{ - addr: basics.Address(crypto.Hash([]byte{byte(i)})), - round: basics.Round(i), - rowid: int64(i), - accountData: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + acct := store.PersistedAccountData{ + Addr: basics.Address(crypto.Hash([]byte{byte(i)})), + Round: basics.Round(i), + Rowid: int64(i), + AccountData: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } baseAcct.writePending(acct) }(i) @@ -149,11 +150,11 @@ func TestLRUAccountsPendingWritesWarning(t *testing.T) { baseAcct.init(log, pendingWritesBuffer, pendingWritesThreshold) for j := 0; j < 50; j++ { for i := 0; i < j; i++ { - acct := persistedAccountData{ - addr: basics.Address(crypto.Hash([]byte{byte(i)})), - round: basics.Round(i), - rowid: int64(i), - accountData: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + acct := store.PersistedAccountData{ + Addr: basics.Address(crypto.Hash([]byte{byte(i)})), + Round: basics.Round(i), + Rowid: int64(i), + AccountData: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } baseAcct.writePending(acct) } @@ -175,11 +176,11 @@ func TestLRUAccountsOmittedPendingWrites(t *testing.T) { baseAcct.init(log, pendingWritesBuffer, pendingWritesThreshold) for i := 0; i < pendingWritesBuffer*2; i++ { - acct := persistedAccountData{ - addr: basics.Address(crypto.Hash([]byte{byte(i)})), - round: basics.Round(i), - rowid: int64(i), - accountData: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + acct := store.PersistedAccountData{ + Addr: basics.Address(crypto.Hash([]byte{byte(i)})), + Round: basics.Round(i), + Rowid: int64(i), + AccountData: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } baseAcct.writePending(acct) } @@ -191,10 +192,10 @@ func TestLRUAccountsOmittedPendingWrites(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) acct, has := baseAcct.read(addr) require.True(t, has) - require.Equal(t, basics.Round(i), acct.round) - require.Equal(t, addr, acct.addr) - require.Equal(t, uint64(i), acct.accountData.MicroAlgos.Raw) - require.Equal(t, int64(i), acct.rowid) + require.Equal(t, basics.Round(i), acct.Round) + require.Equal(t, addr, acct.Addr) + require.Equal(t, uint64(i), acct.AccountData.MicroAlgos.Raw) + require.Equal(t, int64(i), acct.Rowid) } // verify expected missing entries @@ -202,7 +203,7 @@ func TestLRUAccountsOmittedPendingWrites(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) acct, has := baseAcct.read(addr) require.False(t, has) - require.Equal(t, persistedAccountData{}, acct) + require.Equal(t, store.PersistedAccountData{}, acct) } } @@ -215,7 +216,7 @@ func BenchmarkLRUAccountsWrite(b *testing.B) { benchLruWrite(b, fillerAccounts, accounts) } -func benchLruWrite(b *testing.B, fillerAccounts []persistedAccountData, accounts []persistedAccountData) { +func benchLruWrite(b *testing.B, fillerAccounts []store.PersistedAccountData, accounts []store.PersistedAccountData) { b.ResetTimer() b.StopTimer() var baseAcct lruAccounts @@ -231,26 +232,26 @@ func benchLruWrite(b *testing.B, fillerAccounts []persistedAccountData, accounts } } -func fillLRUAccounts(baseAcct lruAccounts, fillerAccounts []persistedAccountData) lruAccounts { +func fillLRUAccounts(baseAcct lruAccounts, fillerAccounts []store.PersistedAccountData) lruAccounts { for _, account := range fillerAccounts { baseAcct.write(account) } return baseAcct } -func generatePersistedAccountData(startRound, endRound int) []persistedAccountData { - accounts := make([]persistedAccountData, endRound-startRound) +func generatePersistedAccountData(startRound, endRound int) []store.PersistedAccountData { + accounts := make([]store.PersistedAccountData, endRound-startRound) buffer := make([]byte, 4) for i := startRound; i < endRound; i++ { binary.BigEndian.PutUint32(buffer, uint32(i)) digest := crypto.Hash(buffer) - accounts[i-startRound] = persistedAccountData{ - addr: basics.Address(digest), - round: basics.Round(i + startRound), - rowid: int64(i), - accountData: baseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + accounts[i-startRound] = store.PersistedAccountData{ + Addr: basics.Address(digest), + Round: basics.Round(i + startRound), + Rowid: int64(i), + AccountData: store.BaseAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } } return accounts diff --git a/ledger/lrukv.go b/ledger/lrukv.go index 45f4f5027d..30530fe6bd 100644 --- a/ledger/lrukv.go +++ b/ledger/lrukv.go @@ -17,12 +17,13 @@ package ledger import ( + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" ) //msgp:ignore cachedKVData type cachedKVData struct { - persistedKVData + store.PersistedKVData // kv key key string @@ -62,11 +63,11 @@ func (m *lruKV) init(log logging.Logger, pendingWrites int, pendingWritesWarnThr // read the persistedKVData object that the lruKV has for the given key. // thread locking semantics : read lock -func (m *lruKV) read(key string) (data persistedKVData, has bool) { +func (m *lruKV) read(key string) (data store.PersistedKVData, has bool) { if el := m.kvs[key]; el != nil { - return el.Value.persistedKVData, true + return el.Value.PersistedKVData, true } - return persistedKVData{}, false + return store.PersistedKVData{}, false } // flushPendingWrites flushes the pending writes to the main lruKV cache. @@ -79,7 +80,7 @@ func (m *lruKV) flushPendingWrites() { for ; pendingEntriesCount > 0; pendingEntriesCount-- { select { case pendingKVData := <-m.pendingKVs: - m.write(pendingKVData.persistedKVData, pendingKVData.key) + m.write(pendingKVData.PersistedKVData, pendingKVData.key) default: return } @@ -89,9 +90,9 @@ func (m *lruKV) flushPendingWrites() { // writePending write a single persistedKVData entry to the pendingKVs buffer. // the function doesn't block, and in case of a buffer overflow the entry would not be added. // thread locking semantics : no lock is required. -func (m *lruKV) writePending(kv persistedKVData, key string) { +func (m *lruKV) writePending(kv store.PersistedKVData, key string) { select { - case m.pendingKVs <- cachedKVData{persistedKVData: kv, key: key}: + case m.pendingKVs <- cachedKVData{PersistedKVData: kv, key: key}: default: } } @@ -101,17 +102,17 @@ func (m *lruKV) writePending(kv persistedKVData, key string) { // version of what's already on the cache or not. In all cases, the entry is going // to be promoted to the front of the list. // thread locking semantics : write lock -func (m *lruKV) write(kvData persistedKVData, key string) { +func (m *lruKV) write(kvData store.PersistedKVData, key string) { if el := m.kvs[key]; el != nil { // already exists; is it a newer ? - if el.Value.before(&kvData) { + if el.Value.Before(&kvData) { // we update with a newer version. - el.Value = &cachedKVData{persistedKVData: kvData, key: key} + el.Value = &cachedKVData{PersistedKVData: kvData, key: key} } m.kvList.moveToFront(el) } else { // new entry. - m.kvs[key] = m.kvList.pushFront(&cachedKVData{persistedKVData: kvData, key: key}) + m.kvs[key] = m.kvList.pushFront(&cachedKVData{PersistedKVData: kvData, key: key}) } } diff --git a/ledger/lrukv_test.go b/ledger/lrukv_test.go index d266167310..01ed002065 100644 --- a/ledger/lrukv_test.go +++ b/ledger/lrukv_test.go @@ -25,6 +25,7 @@ import ( "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -39,9 +40,9 @@ func TestLRUBasicKV(t *testing.T) { // write 50 KVs for i := 0; i < kvNum; i++ { kvValue := fmt.Sprintf("kv %d value", i) - kv := persistedKVData{ - value: []byte(kvValue), - round: basics.Round(i), + kv := store.PersistedKVData{ + Value: []byte(kvValue), + Round: basics.Round(i), } baseKV.write(kv, fmt.Sprintf("key%d", i)) } @@ -50,15 +51,15 @@ func TestLRUBasicKV(t *testing.T) { for i := 0; i < kvNum; i++ { kv, has := baseKV.read(fmt.Sprintf("key%d", i)) require.True(t, has) - require.Equal(t, basics.Round(i), kv.round) - require.Equal(t, fmt.Sprintf("kv %d value", i), string(kv.value)) + require.Equal(t, basics.Round(i), kv.Round) + require.Equal(t, fmt.Sprintf("kv %d value", i), string(kv.Value)) } // verify expected missing entries for i := kvNum; i < kvNum*2; i++ { kv, has := baseKV.read(fmt.Sprintf("key%d", i)) require.False(t, has) - require.Equal(t, persistedKVData{}, kv) + require.Equal(t, store.PersistedKVData{}, kv) } baseKV.prune(kvNum / 2) @@ -70,11 +71,11 @@ func TestLRUBasicKV(t *testing.T) { if i >= kvNum/2 && i < kvNum { // expected to have it. require.True(t, has) - require.Equal(t, basics.Round(i), kv.round) - require.Equal(t, fmt.Sprintf("kv %d value", i), string(kv.value)) + require.Equal(t, basics.Round(i), kv.Round) + require.Equal(t, fmt.Sprintf("kv %d value", i), string(kv.Value)) } else { require.False(t, has) - require.Equal(t, persistedKVData{}, kv) + require.Equal(t, store.PersistedKVData{}, kv) } } } @@ -90,9 +91,9 @@ func TestLRUKVPendingWrites(t *testing.T) { go func(i int) { time.Sleep(time.Duration((crypto.RandUint64() % 50)) * time.Millisecond) kvValue := fmt.Sprintf("kv %d value", i) - kv := persistedKVData{ - value: []byte(kvValue), - round: basics.Round(i), + kv := store.PersistedKVData{ + Value: []byte(kvValue), + Round: basics.Round(i), } baseKV.writePending(kv, fmt.Sprintf("key%d", i)) }(i) @@ -141,9 +142,9 @@ func TestLRUKVPendingWritesWarning(t *testing.T) { for j := 0; j < 50; j++ { for i := 0; i < j; i++ { kvValue := fmt.Sprintf("kv %d value", i) - kv := persistedKVData{ - value: []byte(kvValue), - round: basics.Round(i), + kv := store.PersistedKVData{ + Value: []byte(kvValue), + Round: basics.Round(i), } baseKV.writePending(kv, fmt.Sprintf("key%d", i)) } @@ -166,9 +167,9 @@ func TestLRUKVOmittedPendingWrites(t *testing.T) { for i := 0; i < pendingWritesBuffer*2; i++ { kvValue := fmt.Sprintf("kv %d value", i) - kv := persistedKVData{ - value: []byte(kvValue), - round: basics.Round(i), + kv := store.PersistedKVData{ + Value: []byte(kvValue), + Round: basics.Round(i), } baseKV.writePending(kv, fmt.Sprintf("key%d", i)) } @@ -179,15 +180,15 @@ func TestLRUKVOmittedPendingWrites(t *testing.T) { for i := 0; i < pendingWritesBuffer; i++ { kv, has := baseKV.read(fmt.Sprintf("key%d", i)) require.True(t, has) - require.Equal(t, basics.Round(i), kv.round) - require.Equal(t, fmt.Sprintf("kv %d value", i), string(kv.value)) + require.Equal(t, basics.Round(i), kv.Round) + require.Equal(t, fmt.Sprintf("kv %d value", i), string(kv.Value)) } // verify expected missing entries for i := pendingWritesBuffer; i < pendingWritesBuffer*2; i++ { kv, has := baseKV.read(fmt.Sprintf("key%d", i)) require.False(t, has) - require.Equal(t, persistedKVData{}, kv) + require.Equal(t, store.PersistedKVData{}, kv) } } @@ -218,7 +219,7 @@ func benchLruWriteKVs(b *testing.B, fillerKVs []cachedKVData, kvs []cachedKVData func fillLRUKV(baseKV lruKV, fillerKVs []cachedKVData) lruKV { for _, entry := range fillerKVs { - baseKV.write(entry.persistedKVData, entry.key) + baseKV.write(entry.PersistedKVData, entry.key) } return baseKV } @@ -229,9 +230,9 @@ func generatePersistedKVData(startRound, endRound int) []cachedKVData { kvValue := fmt.Sprintf("kv %d value", i) kvs[i-startRound] = cachedKVData{ - persistedKVData: persistedKVData{ - value: []byte(kvValue), - round: basics.Round(i + startRound), + PersistedKVData: store.PersistedKVData{ + Value: []byte(kvValue), + Round: basics.Round(i + startRound), }, key: fmt.Sprintf("key%d", i), } diff --git a/ledger/lruonlineaccts.go b/ledger/lruonlineaccts.go index ae05c497c5..bc8ccedfd4 100644 --- a/ledger/lruonlineaccts.go +++ b/ledger/lruonlineaccts.go @@ -18,6 +18,7 @@ package ledger import ( "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" ) @@ -32,7 +33,7 @@ type lruOnlineAccounts struct { accounts map[basics.Address]*persistedOnlineAccountDataListNode // pendingAccounts are used as a way to avoid taking a write-lock. When the caller needs to "materialize" these, // it would call flushPendingWrites and these would be merged into the accounts/accountsList - pendingAccounts chan persistedOnlineAccountData + pendingAccounts chan store.PersistedOnlineAccountData // log interface; used for logging the threshold event. log logging.Logger // pendingWritesWarnThreshold is the threshold beyond we would write a warning for exceeding the number of pendingAccounts entries @@ -44,18 +45,18 @@ type lruOnlineAccounts struct { func (m *lruOnlineAccounts) init(log logging.Logger, pendingWrites int, pendingWritesWarnThreshold int) { m.accountsList = newPersistedOnlineAccountList().allocateFreeNodes(pendingWrites) m.accounts = make(map[basics.Address]*persistedOnlineAccountDataListNode, pendingWrites) - m.pendingAccounts = make(chan persistedOnlineAccountData, pendingWrites) + m.pendingAccounts = make(chan store.PersistedOnlineAccountData, pendingWrites) m.log = log m.pendingWritesWarnThreshold = pendingWritesWarnThreshold } // read the persistedAccountData object that the lruAccounts has for the given address. // thread locking semantics : read lock -func (m *lruOnlineAccounts) read(addr basics.Address) (data persistedOnlineAccountData, has bool) { +func (m *lruOnlineAccounts) read(addr basics.Address) (data store.PersistedOnlineAccountData, has bool) { if el := m.accounts[addr]; el != nil { return *el.Value, true } - return persistedOnlineAccountData{}, false + return store.PersistedOnlineAccountData{}, false } // flushPendingWrites flushes the pending writes to the main lruAccounts cache. @@ -78,7 +79,7 @@ func (m *lruOnlineAccounts) flushPendingWrites() { // writePending write a single persistedOnlineAccountData entry to the pendingAccounts buffer. // the function doesn't block, and in case of a buffer overflow the entry would not be added. // thread locking semantics : no lock is required. -func (m *lruOnlineAccounts) writePending(acct persistedOnlineAccountData) { +func (m *lruOnlineAccounts) writePending(acct store.PersistedOnlineAccountData) { select { case m.pendingAccounts <- acct: default: @@ -90,17 +91,17 @@ func (m *lruOnlineAccounts) writePending(acct persistedOnlineAccountData) { // version of what's already on the cache or not. In all cases, the entry is going // to be promoted to the front of the list. // thread locking semantics : write lock -func (m *lruOnlineAccounts) write(acctData persistedOnlineAccountData) { - if el := m.accounts[acctData.addr]; el != nil { +func (m *lruOnlineAccounts) write(acctData store.PersistedOnlineAccountData) { + if el := m.accounts[acctData.Addr]; el != nil { // already exists; is it a newer ? - if el.Value.before(&acctData) { + if el.Value.Before(&acctData) { // we update with a newer version. el.Value = &acctData } m.accountsList.moveToFront(el) } else { // new entry. - m.accounts[acctData.addr] = m.accountsList.pushFront(&acctData) + m.accounts[acctData.Addr] = m.accountsList.pushFront(&acctData) } } @@ -113,7 +114,7 @@ func (m *lruOnlineAccounts) prune(newSize int) (removed int) { break } back := m.accountsList.back() - delete(m.accounts, back.Value.addr) + delete(m.accounts, back.Value.Addr) m.accountsList.remove(back) removed++ } diff --git a/ledger/lruonlineaccts_test.go b/ledger/lruonlineaccts_test.go index ca846f6c59..2b9994480e 100644 --- a/ledger/lruonlineaccts_test.go +++ b/ledger/lruonlineaccts_test.go @@ -24,6 +24,7 @@ import ( "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -37,11 +38,11 @@ func TestLRUOnlineAccountsBasic(t *testing.T) { accountsNum := 50 // write 50 accounts for i := 0; i < accountsNum; i++ { - acct := persistedOnlineAccountData{ - addr: basics.Address(crypto.Hash([]byte{byte(i)})), - round: basics.Round(i), - rowid: int64(i), - accountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + acct := store.PersistedOnlineAccountData{ + Addr: basics.Address(crypto.Hash([]byte{byte(i)})), + Round: basics.Round(i), + Rowid: int64(i), + AccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } baseOnlineAcct.write(acct) } @@ -51,10 +52,10 @@ func TestLRUOnlineAccountsBasic(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) acct, has := baseOnlineAcct.read(addr) require.True(t, has) - require.Equal(t, basics.Round(i), acct.round) - require.Equal(t, addr, acct.addr) - require.Equal(t, uint64(i), acct.accountData.MicroAlgos.Raw) - require.Equal(t, int64(i), acct.rowid) + require.Equal(t, basics.Round(i), acct.Round) + require.Equal(t, addr, acct.Addr) + require.Equal(t, uint64(i), acct.AccountData.MicroAlgos.Raw) + require.Equal(t, int64(i), acct.Rowid) } // verify expected missing entries @@ -62,7 +63,7 @@ func TestLRUOnlineAccountsBasic(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) acct, has := baseOnlineAcct.read(addr) require.False(t, has) - require.Equal(t, persistedOnlineAccountData{}, acct) + require.Equal(t, store.PersistedOnlineAccountData{}, acct) } baseOnlineAcct.prune(accountsNum / 2) @@ -75,13 +76,13 @@ func TestLRUOnlineAccountsBasic(t *testing.T) { if i >= accountsNum/2 && i < accountsNum { // expected to have it. require.True(t, has) - require.Equal(t, basics.Round(i), acct.round) - require.Equal(t, addr, acct.addr) - require.Equal(t, uint64(i), acct.accountData.MicroAlgos.Raw) - require.Equal(t, int64(i), acct.rowid) + require.Equal(t, basics.Round(i), acct.Round) + require.Equal(t, addr, acct.Addr) + require.Equal(t, uint64(i), acct.AccountData.MicroAlgos.Raw) + require.Equal(t, int64(i), acct.Rowid) } else { require.False(t, has) - require.Equal(t, persistedOnlineAccountData{}, acct) + require.Equal(t, store.PersistedOnlineAccountData{}, acct) } } } @@ -96,11 +97,11 @@ func TestLRUOnlineAccountsPendingWrites(t *testing.T) { for i := 0; i < accountsNum; i++ { go func(i int) { time.Sleep(time.Duration((crypto.RandUint64() % 50)) * time.Millisecond) - acct := persistedOnlineAccountData{ - addr: basics.Address(crypto.Hash([]byte{byte(i)})), - round: basics.Round(i), - rowid: int64(i), - accountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + acct := store.PersistedOnlineAccountData{ + Addr: basics.Address(crypto.Hash([]byte{byte(i)})), + Round: basics.Round(i), + Rowid: int64(i), + AccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } baseOnlineAcct.writePending(acct) }(i) @@ -138,11 +139,11 @@ func TestLRUOnlineAccountsPendingWritesWarning(t *testing.T) { baseOnlineAcct.init(log, pendingWritesBuffer, pendingWritesThreshold) for j := 0; j < 50; j++ { for i := 0; i < j; i++ { - acct := persistedOnlineAccountData{ - addr: basics.Address(crypto.Hash([]byte{byte(i)})), - round: basics.Round(i), - rowid: int64(i), - accountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + acct := store.PersistedOnlineAccountData{ + Addr: basics.Address(crypto.Hash([]byte{byte(i)})), + Round: basics.Round(i), + Rowid: int64(i), + AccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } baseOnlineAcct.writePending(acct) } @@ -164,11 +165,11 @@ func TestLRUOnlineAccountsOmittedPendingWrites(t *testing.T) { baseOnlineAcct.init(log, pendingWritesBuffer, pendingWritesThreshold) for i := 0; i < pendingWritesBuffer*2; i++ { - acct := persistedOnlineAccountData{ - addr: basics.Address(crypto.Hash([]byte{byte(i)})), - round: basics.Round(i), - rowid: int64(i), - accountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, + acct := store.PersistedOnlineAccountData{ + Addr: basics.Address(crypto.Hash([]byte{byte(i)})), + Round: basics.Round(i), + Rowid: int64(i), + AccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}}, } baseOnlineAcct.writePending(acct) } @@ -180,10 +181,10 @@ func TestLRUOnlineAccountsOmittedPendingWrites(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) acct, has := baseOnlineAcct.read(addr) require.True(t, has) - require.Equal(t, basics.Round(i), acct.round) - require.Equal(t, addr, acct.addr) - require.Equal(t, uint64(i), acct.accountData.MicroAlgos.Raw) - require.Equal(t, int64(i), acct.rowid) + require.Equal(t, basics.Round(i), acct.Round) + require.Equal(t, addr, acct.Addr) + require.Equal(t, uint64(i), acct.AccountData.MicroAlgos.Raw) + require.Equal(t, int64(i), acct.Rowid) } // verify expected missing entries @@ -191,6 +192,6 @@ func TestLRUOnlineAccountsOmittedPendingWrites(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) acct, has := baseOnlineAcct.read(addr) require.False(t, has) - require.Equal(t, persistedOnlineAccountData{}, acct) + require.Equal(t, store.PersistedOnlineAccountData{}, acct) } } diff --git a/ledger/lruresources.go b/ledger/lruresources.go index 70a2a4c14e..b2f9a63b04 100644 --- a/ledger/lruresources.go +++ b/ledger/lruresources.go @@ -18,12 +18,13 @@ package ledger import ( "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" ) //msgp:ignore cachedResourceData type cachedResourceData struct { - persistedResourcesData + store.PersistedResourcesData address basics.Address } @@ -67,11 +68,11 @@ func (m *lruResources) init(log logging.Logger, pendingWrites int, pendingWrites // read the persistedResourcesData object that the lruResources has for the given address and creatable index. // thread locking semantics : read lock -func (m *lruResources) read(addr basics.Address, aidx basics.CreatableIndex) (data persistedResourcesData, has bool) { +func (m *lruResources) read(addr basics.Address, aidx basics.CreatableIndex) (data store.PersistedResourcesData, has bool) { if el := m.resources[accountCreatable{address: addr, index: aidx}]; el != nil { - return el.Value.persistedResourcesData, true + return el.Value.PersistedResourcesData, true } - return persistedResourcesData{}, false + return store.PersistedResourcesData{}, false } // readNotFound returns whether we have attempted to read this address but it did not exist in the db. @@ -83,10 +84,10 @@ func (m *lruResources) readNotFound(addr basics.Address, idx basics.CreatableInd // read the persistedResourcesData object that the lruResources has for the given address. // thread locking semantics : read lock -func (m *lruResources) readAll(addr basics.Address) (ret []persistedResourcesData) { +func (m *lruResources) readAll(addr basics.Address) (ret []store.PersistedResourcesData) { for ac, pd := range m.resources { if ac.address == addr { - ret = append(ret, pd.Value.persistedResourcesData) + ret = append(ret, pd.Value.PersistedResourcesData) } } return @@ -104,7 +105,7 @@ outer: for ; pendingEntriesCount > 0; pendingEntriesCount-- { select { case pendingResourceData := <-m.pendingResources: - m.write(pendingResourceData.persistedResourcesData, pendingResourceData.address) + m.write(pendingResourceData.PersistedResourcesData, pendingResourceData.address) default: break outer } @@ -125,9 +126,9 @@ outer2: // writePending write a single persistedAccountData entry to the pendingResources buffer. // the function doesn't block, and in case of a buffer overflow the entry would not be added. // thread locking semantics : no lock is required. -func (m *lruResources) writePending(acct persistedResourcesData, addr basics.Address) { +func (m *lruResources) writePending(acct store.PersistedResourcesData, addr basics.Address) { select { - case m.pendingResources <- cachedResourceData{persistedResourcesData: acct, address: addr}: + case m.pendingResources <- cachedResourceData{PersistedResourcesData: acct, address: addr}: default: } } @@ -147,17 +148,17 @@ func (m *lruResources) writeNotFoundPending(addr basics.Address, idx basics.Crea // version of what's already on the cache or not. In all cases, the entry is going // to be promoted to the front of the list. // thread locking semantics : write lock -func (m *lruResources) write(resData persistedResourcesData, addr basics.Address) { - if el := m.resources[accountCreatable{address: addr, index: resData.aidx}]; el != nil { +func (m *lruResources) write(resData store.PersistedResourcesData, addr basics.Address) { + if el := m.resources[accountCreatable{address: addr, index: resData.Aidx}]; el != nil { // already exists; is it a newer ? - if el.Value.before(&resData) { + if el.Value.Before(&resData) { // we update with a newer version. - el.Value = &cachedResourceData{persistedResourcesData: resData, address: addr} + el.Value = &cachedResourceData{PersistedResourcesData: resData, address: addr} } m.resourcesList.moveToFront(el) } else { // new entry. - m.resources[accountCreatable{address: addr, index: resData.aidx}] = m.resourcesList.pushFront(&cachedResourceData{persistedResourcesData: resData, address: addr}) + m.resources[accountCreatable{address: addr, index: resData.Aidx}] = m.resourcesList.pushFront(&cachedResourceData{PersistedResourcesData: resData, address: addr}) } } @@ -170,7 +171,7 @@ func (m *lruResources) prune(newSize int) (removed int) { break } back := m.resourcesList.back() - delete(m.resources, accountCreatable{address: back.Value.address, index: back.Value.aidx}) + delete(m.resources, accountCreatable{address: back.Value.address, index: back.Value.Aidx}) m.resourcesList.remove(back) removed++ } diff --git a/ledger/lruresources_test.go b/ledger/lruresources_test.go index a389bc516c..695d1f026b 100644 --- a/ledger/lruresources_test.go +++ b/ledger/lruresources_test.go @@ -25,6 +25,7 @@ import ( "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -39,11 +40,11 @@ func TestLRUBasicResources(t *testing.T) { // write 50 resources for i := 0; i < resourcesNum; i++ { addr := basics.Address(crypto.Hash([]byte{byte(i)})) - res := persistedResourcesData{ - addrid: int64(i), - aidx: basics.CreatableIndex(i), - round: basics.Round(i), - data: resourcesData{Total: uint64(i)}, + res := store.PersistedResourcesData{ + Addrid: int64(i), + Aidx: basics.CreatableIndex(i), + Round: basics.Round(i), + Data: store.ResourcesData{Total: uint64(i)}, } baseRes.write(res, addr) } @@ -53,10 +54,10 @@ func TestLRUBasicResources(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) res, has := baseRes.read(addr, basics.CreatableIndex(i)) require.True(t, has) - require.Equal(t, basics.Round(i), res.round) - require.Equal(t, int64(i), res.addrid) - require.Equal(t, uint64(i), res.data.Total) - require.Equal(t, basics.CreatableIndex(i), res.aidx) + require.Equal(t, basics.Round(i), res.Round) + require.Equal(t, int64(i), res.Addrid) + require.Equal(t, uint64(i), res.Data.Total) + require.Equal(t, basics.CreatableIndex(i), res.Aidx) } // verify expected missing entries @@ -64,7 +65,7 @@ func TestLRUBasicResources(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) res, has := baseRes.read(addr, basics.CreatableIndex(i%resourcesNum)) require.False(t, has) - require.Equal(t, persistedResourcesData{}, res) + require.Equal(t, store.PersistedResourcesData{}, res) } baseRes.prune(resourcesNum / 2) @@ -77,13 +78,13 @@ func TestLRUBasicResources(t *testing.T) { if i >= resourcesNum/2 && i < resourcesNum { // expected to have it. require.True(t, has) - require.Equal(t, basics.Round(i), res.round) - require.Equal(t, int64(i), res.addrid) - require.Equal(t, uint64(i), res.data.Total) - require.Equal(t, basics.CreatableIndex(i), res.aidx) + require.Equal(t, basics.Round(i), res.Round) + require.Equal(t, int64(i), res.Addrid) + require.Equal(t, uint64(i), res.Data.Total) + require.Equal(t, basics.CreatableIndex(i), res.Aidx) } else { require.False(t, has) - require.Equal(t, persistedResourcesData{}, res) + require.Equal(t, store.PersistedResourcesData{}, res) } } } @@ -99,11 +100,11 @@ func TestLRUResourcesPendingWrites(t *testing.T) { go func(i int) { time.Sleep(time.Duration((crypto.RandUint64() % 50)) * time.Millisecond) addr := basics.Address(crypto.Hash([]byte{byte(i)})) - res := persistedResourcesData{ - addrid: int64(i), - aidx: basics.CreatableIndex(i), - round: basics.Round(i), - data: resourcesData{Total: uint64(i)}, + res := store.PersistedResourcesData{ + Addrid: int64(i), + Aidx: basics.CreatableIndex(i), + Round: basics.Round(i), + Data: store.ResourcesData{Total: uint64(i)}, } baseRes.writePending(res, addr) }(i) @@ -152,11 +153,11 @@ func TestLRUResourcesPendingWritesWarning(t *testing.T) { for j := 0; j < 50; j++ { for i := 0; i < j; i++ { addr := basics.Address(crypto.Hash([]byte{byte(i)})) - res := persistedResourcesData{ - addrid: int64(i), - aidx: basics.CreatableIndex(i), - round: basics.Round(i), - data: resourcesData{Total: uint64(i)}, + res := store.PersistedResourcesData{ + Addrid: int64(i), + Aidx: basics.CreatableIndex(i), + Round: basics.Round(i), + Data: store.ResourcesData{Total: uint64(i)}, } baseRes.writePending(res, addr) } @@ -179,11 +180,11 @@ func TestLRUResourcesOmittedPendingWrites(t *testing.T) { for i := 0; i < pendingWritesBuffer*2; i++ { addr := basics.Address(crypto.Hash([]byte{byte(i)})) - res := persistedResourcesData{ - addrid: int64(i), - aidx: basics.CreatableIndex(i), - round: basics.Round(i), - data: resourcesData{Total: uint64(i)}, + res := store.PersistedResourcesData{ + Addrid: int64(i), + Aidx: basics.CreatableIndex(i), + Round: basics.Round(i), + Data: store.ResourcesData{Total: uint64(i)}, } baseRes.writePending(res, addr) } @@ -195,10 +196,10 @@ func TestLRUResourcesOmittedPendingWrites(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) res, has := baseRes.read(addr, basics.CreatableIndex(i)) require.True(t, has) - require.Equal(t, basics.Round(i), res.round) - require.Equal(t, int64(i), res.addrid) - require.Equal(t, uint64(i), res.data.Total) - require.Equal(t, basics.CreatableIndex(i), res.aidx) + require.Equal(t, basics.Round(i), res.Round) + require.Equal(t, int64(i), res.Addrid) + require.Equal(t, uint64(i), res.Data.Total) + require.Equal(t, basics.CreatableIndex(i), res.Aidx) } // verify expected missing entries @@ -206,7 +207,7 @@ func TestLRUResourcesOmittedPendingWrites(t *testing.T) { addr := basics.Address(crypto.Hash([]byte{byte(i)})) res, has := baseRes.read(addr, basics.CreatableIndex(i)) require.False(t, has) - require.Equal(t, persistedResourcesData{}, res) + require.Equal(t, store.PersistedResourcesData{}, res) } } @@ -237,7 +238,7 @@ func benchLruWriteResources(b *testing.B, fillerAccounts []cachedResourceData, a func fillLRUResources(baseRes lruResources, fillerAccounts []cachedResourceData) lruResources { for _, entry := range fillerAccounts { - baseRes.write(entry.persistedResourcesData, entry.address) + baseRes.write(entry.PersistedResourcesData, entry.address) } return baseRes } @@ -251,11 +252,11 @@ func generatePersistedResourcesData(startRound, endRound int) []cachedResourceDa digest := crypto.Hash(buffer) accounts[i-startRound] = cachedResourceData{ - persistedResourcesData: persistedResourcesData{ - addrid: int64(i), - aidx: basics.CreatableIndex(i), - round: basics.Round(i + startRound), - data: resourcesData{Total: uint64(i)}, + PersistedResourcesData: store.PersistedResourcesData{ + Addrid: int64(i), + Aidx: basics.CreatableIndex(i), + Round: basics.Round(i + startRound), + Data: store.ResourcesData{Total: uint64(i)}, }, address: basics.Address(digest), } diff --git a/ledger/msgp_gen.go b/ledger/msgp_gen.go index 9a1be6eb6c..ddd01a87f5 100644 --- a/ledger/msgp_gen.go +++ b/ledger/msgp_gen.go @@ -6,10 +6,6 @@ import ( "sort" "github.com/algorand/msgp/msgp" - - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/data/transactions" ) // The following msgp objects are implemented in this file: @@ -29,30 +25,6 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // -// baseAccountData -// |-----> (*) MarshalMsg -// |-----> (*) CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> (*) Msgsize -// |-----> (*) MsgIsZero -// -// baseOnlineAccountData -// |-----> (*) MarshalMsg -// |-----> (*) CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> (*) Msgsize -// |-----> (*) MsgIsZero -// -// baseVotingData -// |-----> (*) MarshalMsg -// |-----> (*) CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> (*) Msgsize -// |-----> (*) MsgIsZero -// // catchpointFileBalancesChunkV5 // |-----> (*) MarshalMsg // |-----> (*) CanMarshalMsg @@ -69,22 +41,6 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // -// catchpointFirstStageInfo -// |-----> (*) MarshalMsg -// |-----> (*) CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> (*) Msgsize -// |-----> (*) MsgIsZero -// -// catchpointState -// |-----> MarshalMsg -// |-----> CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> Msgsize -// |-----> MsgIsZero -// // encodedBalanceRecordV5 // |-----> (*) MarshalMsg // |-----> (*) CanMarshalMsg @@ -109,46 +65,6 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // -// hashKind -// |-----> MarshalMsg -// |-----> CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> Msgsize -// |-----> MsgIsZero -// -// resourceFlags -// |-----> MarshalMsg -// |-----> CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> Msgsize -// |-----> MsgIsZero -// -// resourcesData -// |-----> (*) MarshalMsg -// |-----> (*) CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> (*) Msgsize -// |-----> (*) MsgIsZero -// -// txTailRound -// |-----> (*) MarshalMsg -// |-----> (*) CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> (*) Msgsize -// |-----> (*) MsgIsZero -// -// txTailRoundLease -// |-----> (*) MarshalMsg -// |-----> (*) CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> (*) Msgsize -// |-----> (*) MsgIsZero -// // MarshalMsg implements msgp.Marshaler func (z CatchpointCatchupState) MarshalMsg(b []byte) (o []byte) { @@ -487,395 +403,169 @@ func (z *CatchpointFileHeader) MsgIsZero() bool { } // MarshalMsg implements msgp.Marshaler -func (z *baseAccountData) MarshalMsg(b []byte) (o []byte) { +func (z *catchpointFileBalancesChunkV5) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0001Len := uint32(21) - var zb0001Mask uint32 /* 23 bits */ - if (*z).baseVotingData.VoteID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x1 - } - if (*z).baseVotingData.SelectionID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x2 - } - if (*z).baseVotingData.VoteFirstValid.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x4 - } - if (*z).baseVotingData.VoteLastValid.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x8 - } - if (*z).baseVotingData.VoteKeyDilution == 0 { - zb0001Len-- - zb0001Mask |= 0x10 - } - if (*z).baseVotingData.StateProofID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x20 - } - if (*z).Status.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x100 - } - if (*z).MicroAlgos.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x200 - } - if (*z).RewardsBase == 0 { - zb0001Len-- - zb0001Mask |= 0x400 - } - if (*z).RewardedMicroAlgos.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x800 - } - if (*z).AuthAddr.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x1000 - } - if (*z).TotalAppSchemaNumUint == 0 { - zb0001Len-- - zb0001Mask |= 0x2000 - } - if (*z).TotalAppSchemaNumByteSlice == 0 { - zb0001Len-- - zb0001Mask |= 0x4000 - } - if (*z).TotalExtraAppPages == 0 { - zb0001Len-- - zb0001Mask |= 0x8000 - } - if (*z).TotalAssetParams == 0 { - zb0001Len-- - zb0001Mask |= 0x10000 - } - if (*z).TotalAssets == 0 { - zb0001Len-- - zb0001Mask |= 0x20000 - } - if (*z).TotalAppParams == 0 { - zb0001Len-- - zb0001Mask |= 0x40000 - } - if (*z).TotalAppLocalStates == 0 { - zb0001Len-- - zb0001Mask |= 0x80000 - } - if (*z).TotalBoxes == 0 { - zb0001Len-- - zb0001Mask |= 0x100000 - } - if (*z).TotalBoxBytes == 0 { - zb0001Len-- - zb0001Mask |= 0x200000 - } - if (*z).UpdateRound == 0 { - zb0001Len-- - zb0001Mask |= 0x400000 + zb0002Len := uint32(1) + var zb0002Mask uint8 /* 2 bits */ + if len((*z).Balances) == 0 { + zb0002Len-- + zb0002Mask |= 0x2 } - // variable map header, size zb0001Len - o = msgp.AppendMapHeader(o, zb0001Len) - if zb0001Len != 0 { - if (zb0001Mask & 0x1) == 0 { // if not empty - // string "A" - o = append(o, 0xa1, 0x41) - o = (*z).baseVotingData.VoteID.MarshalMsg(o) - } - if (zb0001Mask & 0x2) == 0 { // if not empty - // string "B" - o = append(o, 0xa1, 0x42) - o = (*z).baseVotingData.SelectionID.MarshalMsg(o) - } - if (zb0001Mask & 0x4) == 0 { // if not empty - // string "C" - o = append(o, 0xa1, 0x43) - o = (*z).baseVotingData.VoteFirstValid.MarshalMsg(o) - } - if (zb0001Mask & 0x8) == 0 { // if not empty - // string "D" - o = append(o, 0xa1, 0x44) - o = (*z).baseVotingData.VoteLastValid.MarshalMsg(o) - } - if (zb0001Mask & 0x10) == 0 { // if not empty - // string "E" - o = append(o, 0xa1, 0x45) - o = msgp.AppendUint64(o, (*z).baseVotingData.VoteKeyDilution) - } - if (zb0001Mask & 0x20) == 0 { // if not empty - // string "F" - o = append(o, 0xa1, 0x46) - o = (*z).baseVotingData.StateProofID.MarshalMsg(o) - } - if (zb0001Mask & 0x100) == 0 { // if not empty - // string "a" - o = append(o, 0xa1, 0x61) - o = (*z).Status.MarshalMsg(o) - } - if (zb0001Mask & 0x200) == 0 { // if not empty - // string "b" - o = append(o, 0xa1, 0x62) - o = (*z).MicroAlgos.MarshalMsg(o) - } - if (zb0001Mask & 0x400) == 0 { // if not empty - // string "c" - o = append(o, 0xa1, 0x63) - o = msgp.AppendUint64(o, (*z).RewardsBase) - } - if (zb0001Mask & 0x800) == 0 { // if not empty - // string "d" - o = append(o, 0xa1, 0x64) - o = (*z).RewardedMicroAlgos.MarshalMsg(o) - } - if (zb0001Mask & 0x1000) == 0 { // if not empty - // string "e" - o = append(o, 0xa1, 0x65) - o = (*z).AuthAddr.MarshalMsg(o) - } - if (zb0001Mask & 0x2000) == 0 { // if not empty - // string "f" - o = append(o, 0xa1, 0x66) - o = msgp.AppendUint64(o, (*z).TotalAppSchemaNumUint) - } - if (zb0001Mask & 0x4000) == 0 { // if not empty - // string "g" - o = append(o, 0xa1, 0x67) - o = msgp.AppendUint64(o, (*z).TotalAppSchemaNumByteSlice) - } - if (zb0001Mask & 0x8000) == 0 { // if not empty - // string "h" - o = append(o, 0xa1, 0x68) - o = msgp.AppendUint32(o, (*z).TotalExtraAppPages) - } - if (zb0001Mask & 0x10000) == 0 { // if not empty - // string "i" - o = append(o, 0xa1, 0x69) - o = msgp.AppendUint64(o, (*z).TotalAssetParams) - } - if (zb0001Mask & 0x20000) == 0 { // if not empty - // string "j" - o = append(o, 0xa1, 0x6a) - o = msgp.AppendUint64(o, (*z).TotalAssets) - } - if (zb0001Mask & 0x40000) == 0 { // if not empty - // string "k" - o = append(o, 0xa1, 0x6b) - o = msgp.AppendUint64(o, (*z).TotalAppParams) - } - if (zb0001Mask & 0x80000) == 0 { // if not empty - // string "l" - o = append(o, 0xa1, 0x6c) - o = msgp.AppendUint64(o, (*z).TotalAppLocalStates) - } - if (zb0001Mask & 0x100000) == 0 { // if not empty - // string "m" - o = append(o, 0xa1, 0x6d) - o = msgp.AppendUint64(o, (*z).TotalBoxes) - } - if (zb0001Mask & 0x200000) == 0 { // if not empty - // string "n" - o = append(o, 0xa1, 0x6e) - o = msgp.AppendUint64(o, (*z).TotalBoxBytes) - } - if (zb0001Mask & 0x400000) == 0 { // if not empty - // string "z" - o = append(o, 0xa1, 0x7a) - o = msgp.AppendUint64(o, (*z).UpdateRound) + // 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))) + } + 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 = (*z).Balances[zb0001].AccountData.MarshalMsg(o) + } + if (zb0003Mask & 0x4) == 0 { // if not empty + // string "pk" + o = append(o, 0xa2, 0x70, 0x6b) + o = (*z).Balances[zb0001].Address.MarshalMsg(o) + } + } } } return } -func (_ *baseAccountData) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*baseAccountData) +func (_ *catchpointFileBalancesChunkV5) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*catchpointFileBalancesChunkV5) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *baseAccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { +func (z *catchpointFileBalancesChunkV5) 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-- - bts, err = (*z).Status.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Status") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "MicroAlgos") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "RewardsBase") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).RewardedMicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "RewardedMicroAlgos") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).AuthAddr.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "AuthAddr") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalAppSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalAppSchemaNumUint") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalAppSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalAppSchemaNumByteSlice") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalExtraAppPages, bts, err = msgp.ReadUint32Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalExtraAppPages") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalAssetParams, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalAssetParams") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalAssets, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalAssets") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalAppParams, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalAppParams") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalAppLocalStates, 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", "TotalAppLocalStates") + err = msgp.WrapError(err, "struct-from-array", "Balances") return } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalBoxes, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalBoxes") + if zb0004 > BalancesPerCatchpointFileChunk { + err = msgp.ErrOverflow(uint64(zb0004), uint64(BalancesPerCatchpointFileChunk)) + err = msgp.WrapError(err, "struct-from-array", "Balances") return } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalBoxBytes, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalBoxBytes") - return + if zb0005 { + (*z).Balances = nil + } else if (*z).Balances != nil && cap((*z).Balances) >= zb0004 { + (*z).Balances = ((*z).Balances)[:zb0004] + } else { + (*z).Balances = make([]encodedBalanceRecordV5, zb0004) } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.VoteID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteID") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.SelectionID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "SelectionID") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.VoteFirstValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.VoteLastValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).baseVotingData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.StateProofID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "StateProofID") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).UpdateRound, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "UpdateRound") - 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] = encodedBalanceRecordV5{} + } + 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 @@ -886,147 +576,112 @@ func (z *baseAccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0002 { - (*z) = baseAccountData{} + if zb0003 { + (*z) = catchpointFileBalancesChunkV5{} } - 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 "a": - bts, err = (*z).Status.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Status") - return - } - case "b": - bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "MicroAlgos") - return - } - case "c": - (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "RewardsBase") - return - } - case "d": - bts, err = (*z).RewardedMicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "RewardedMicroAlgos") - return - } - case "e": - bts, err = (*z).AuthAddr.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "AuthAddr") - return - } - case "f": - (*z).TotalAppSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalAppSchemaNumUint") - return - } - case "g": - (*z).TotalAppSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalAppSchemaNumByteSlice") - return - } - case "h": - (*z).TotalExtraAppPages, bts, err = msgp.ReadUint32Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalExtraAppPages") - return - } - case "i": - (*z).TotalAssetParams, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalAssetParams") - return - } - case "j": - (*z).TotalAssets, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalAssets") - return - } - case "k": - (*z).TotalAppParams, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalAppParams") - return - } - case "l": - (*z).TotalAppLocalStates, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalAppLocalStates") - return - } - case "m": - (*z).TotalBoxes, 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, "TotalBoxes") + err = msgp.WrapError(err, "Balances") return } - case "n": - (*z).TotalBoxBytes, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalBoxBytes") + if zb0008 > BalancesPerCatchpointFileChunk { + err = msgp.ErrOverflow(uint64(zb0008), uint64(BalancesPerCatchpointFileChunk)) + err = msgp.WrapError(err, "Balances") return } - case "A": - bts, err = (*z).baseVotingData.VoteID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "VoteID") - return + if zb0009 { + (*z).Balances = nil + } else if (*z).Balances != nil && cap((*z).Balances) >= zb0008 { + (*z).Balances = ((*z).Balances)[:zb0008] + } else { + (*z).Balances = make([]encodedBalanceRecordV5, zb0008) } - case "B": - bts, err = (*z).baseVotingData.SelectionID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "SelectionID") - 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] = encodedBalanceRecordV5{} + } + 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 "C": - bts, err = (*z).baseVotingData.VoteFirstValid.UnmarshalMsg(bts) + default: + err = msgp.ErrNoField(string(field)) if err != nil { - err = msgp.WrapError(err, "VoteFirstValid") - return - } - case "D": - bts, err = (*z).baseVotingData.VoteLastValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "VoteLastValid") - return - } - case "E": - (*z).baseVotingData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "VoteKeyDilution") - return - } - case "F": - bts, err = (*z).baseVotingData.StateProofID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "StateProofID") - return - } - case "z": - (*z).UpdateRound, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "UpdateRound") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err) + err = msgp.WrapError(err) return } } @@ -1036,424 +691,274 @@ func (z *baseAccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { return } -func (_ *baseAccountData) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*baseAccountData) +func (_ *catchpointFileBalancesChunkV5) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*catchpointFileBalancesChunkV5) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *baseAccountData) Msgsize() (s int) { - s = 3 + 2 + (*z).Status.Msgsize() + 2 + (*z).MicroAlgos.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).RewardedMicroAlgos.Msgsize() + 2 + (*z).AuthAddr.Msgsize() + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint32Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + (*z).baseVotingData.VoteID.Msgsize() + 2 + (*z).baseVotingData.SelectionID.Msgsize() + 2 + (*z).baseVotingData.VoteFirstValid.Msgsize() + 2 + (*z).baseVotingData.VoteLastValid.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).baseVotingData.StateProofID.Msgsize() + 2 + msgp.Uint64Size +func (z *catchpointFileBalancesChunkV5) 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 *baseAccountData) MsgIsZero() bool { - return ((*z).Status.MsgIsZero()) && ((*z).MicroAlgos.MsgIsZero()) && ((*z).RewardsBase == 0) && ((*z).RewardedMicroAlgos.MsgIsZero()) && ((*z).AuthAddr.MsgIsZero()) && ((*z).TotalAppSchemaNumUint == 0) && ((*z).TotalAppSchemaNumByteSlice == 0) && ((*z).TotalExtraAppPages == 0) && ((*z).TotalAssetParams == 0) && ((*z).TotalAssets == 0) && ((*z).TotalAppParams == 0) && ((*z).TotalAppLocalStates == 0) && ((*z).TotalBoxes == 0) && ((*z).TotalBoxBytes == 0) && ((*z).baseVotingData.VoteID.MsgIsZero()) && ((*z).baseVotingData.SelectionID.MsgIsZero()) && ((*z).baseVotingData.VoteFirstValid.MsgIsZero()) && ((*z).baseVotingData.VoteLastValid.MsgIsZero()) && ((*z).baseVotingData.VoteKeyDilution == 0) && ((*z).baseVotingData.StateProofID.MsgIsZero()) && ((*z).UpdateRound == 0) +func (z *catchpointFileBalancesChunkV5) MsgIsZero() bool { + return (len((*z).Balances) == 0) } // MarshalMsg implements msgp.Marshaler -func (z *baseOnlineAccountData) MarshalMsg(b []byte) (o []byte) { +func (z *catchpointFileChunkV6) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0001Len := uint32(8) - var zb0001Mask uint16 /* 10 bits */ - if (*z).baseVotingData.VoteID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x1 - } - if (*z).baseVotingData.SelectionID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x2 - } - if (*z).baseVotingData.VoteFirstValid.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x4 - } - if (*z).baseVotingData.VoteLastValid.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x8 - } - if (*z).baseVotingData.VoteKeyDilution == 0 { - zb0001Len-- - zb0001Mask |= 0x10 - } - if (*z).baseVotingData.StateProofID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x20 - } - if (*z).MicroAlgos.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x40 + zb0003Len := uint32(2) + var zb0003Mask uint8 /* 4 bits */ + if len((*z).Balances) == 0 { + zb0003Len-- + zb0003Mask |= 0x2 } - if (*z).RewardsBase == 0 { - zb0001Len-- - zb0001Mask |= 0x80 + if len((*z).KVs) == 0 { + zb0003Len-- + zb0003Mask |= 0x4 } - // variable map header, size zb0001Len - o = append(o, 0x80|uint8(zb0001Len)) - if zb0001Len != 0 { - if (zb0001Mask & 0x1) == 0 { // if not empty - // string "A" - o = append(o, 0xa1, 0x41) - o = (*z).baseVotingData.VoteID.MarshalMsg(o) - } - if (zb0001Mask & 0x2) == 0 { // if not empty - // string "B" - o = append(o, 0xa1, 0x42) - o = (*z).baseVotingData.SelectionID.MarshalMsg(o) - } - if (zb0001Mask & 0x4) == 0 { // if not empty - // string "C" - o = append(o, 0xa1, 0x43) - o = (*z).baseVotingData.VoteFirstValid.MarshalMsg(o) - } - if (zb0001Mask & 0x8) == 0 { // if not empty - // string "D" - o = append(o, 0xa1, 0x44) - o = (*z).baseVotingData.VoteLastValid.MarshalMsg(o) - } - if (zb0001Mask & 0x10) == 0 { // if not empty - // string "E" - o = append(o, 0xa1, 0x45) - o = msgp.AppendUint64(o, (*z).baseVotingData.VoteKeyDilution) - } - if (zb0001Mask & 0x20) == 0 { // if not empty - // string "F" - o = append(o, 0xa1, 0x46) - o = (*z).baseVotingData.StateProofID.MarshalMsg(o) - } - if (zb0001Mask & 0x40) == 0 { // if not empty - // string "Y" - o = append(o, 0xa1, 0x59) - o = (*z).MicroAlgos.MarshalMsg(o) + // variable map header, size zb0003Len + o = append(o, 0x80|uint8(zb0003Len)) + if zb0003Len != 0 { + if (zb0003Mask & 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))) + } + for zb0001 := range (*z).Balances { + o = (*z).Balances[zb0001].MarshalMsg(o) + } } - if (zb0001Mask & 0x80) == 0 { // if not empty - // string "Z" - o = append(o, 0xa1, 0x5a) - o = msgp.AppendUint64(o, (*z).RewardsBase) + if (zb0003Mask & 0x4) == 0 { // if not empty + // string "kv" + o = append(o, 0xa2, 0x6b, 0x76) + if (*z).KVs == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).KVs))) + } + for zb0002 := range (*z).KVs { + // omitempty: check for empty values + zb0004Len := uint32(2) + var zb0004Mask uint8 /* 3 bits */ + if len((*z).KVs[zb0002].Key) == 0 { + zb0004Len-- + zb0004Mask |= 0x2 + } + if len((*z).KVs[zb0002].Value) == 0 { + zb0004Len-- + zb0004Mask |= 0x4 + } + // variable map header, size zb0004Len + o = append(o, 0x80|uint8(zb0004Len)) + if (zb0004Mask & 0x2) == 0 { // if not empty + // string "k" + o = append(o, 0xa1, 0x6b) + o = msgp.AppendBytes(o, (*z).KVs[zb0002].Key) + } + if (zb0004Mask & 0x4) == 0 { // if not empty + // string "v" + o = append(o, 0xa1, 0x76) + o = msgp.AppendBytes(o, (*z).KVs[zb0002].Value) + } + } } } return } -func (_ *baseOnlineAccountData) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*baseOnlineAccountData) +func (_ *catchpointFileChunkV6) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*catchpointFileChunkV6) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *baseOnlineAccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { +func (z *catchpointFileChunkV6) 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 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-- - bts, err = (*z).baseVotingData.VoteID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteID") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.SelectionID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "SelectionID") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.VoteFirstValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.VoteLastValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).baseVotingData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if zb0003 > 0 { + zb0003-- + var zb0005 int + var zb0006 bool + zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") + err = msgp.WrapError(err, "struct-from-array", "Balances") return } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).baseVotingData.StateProofID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "StateProofID") + if zb0005 > BalancesPerCatchpointFileChunk { + err = msgp.ErrOverflow(uint64(zb0005), uint64(BalancesPerCatchpointFileChunk)) + err = msgp.WrapError(err, "struct-from-array", "Balances") return } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "MicroAlgos") - return + if zb0006 { + (*z).Balances = nil + } else if (*z).Balances != nil && cap((*z).Balances) >= zb0005 { + (*z).Balances = ((*z).Balances)[:zb0005] + } else { + (*z).Balances = make([]encodedBalanceRecordV6, zb0005) } - } - if zb0001 > 0 { - zb0001-- - (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "RewardsBase") - return + for zb0001 := range (*z).Balances { + bts, err = (*z).Balances[zb0001].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) + return + } } } - if zb0001 > 0 { - err = msgp.ErrTooManyArrayFields(zb0001) + if zb0003 > 0 { + zb0003-- + var zb0007 int + var zb0008 bool + zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array") + err = msgp.WrapError(err, "struct-from-array", "KVs") return } - } - } else { - if err != nil { - err = msgp.WrapError(err) - return - } - if zb0002 { - (*z) = baseOnlineAccountData{} - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) + if zb0007 > BalancesPerCatchpointFileChunk { + err = msgp.ErrOverflow(uint64(zb0007), uint64(BalancesPerCatchpointFileChunk)) + err = msgp.WrapError(err, "struct-from-array", "KVs") return } - switch string(field) { - case "A": - bts, err = (*z).baseVotingData.VoteID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "VoteID") - return - } - case "B": - bts, err = (*z).baseVotingData.SelectionID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "SelectionID") - return - } - case "C": - bts, err = (*z).baseVotingData.VoteFirstValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "VoteFirstValid") - return - } - case "D": - bts, err = (*z).baseVotingData.VoteLastValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "VoteLastValid") - return - } - case "E": - (*z).baseVotingData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "VoteKeyDilution") - return - } - case "F": - bts, err = (*z).baseVotingData.StateProofID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "StateProofID") - return - } - case "Y": - bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "MicroAlgos") - return - } - case "Z": - (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "RewardsBase") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err) - return - } + if zb0008 { + (*z).KVs = nil + } else if (*z).KVs != nil && cap((*z).KVs) >= zb0007 { + (*z).KVs = ((*z).KVs)[:zb0007] + } else { + (*z).KVs = make([]encodedKVRecordV6, zb0007) } - } - } - o = bts - return -} - -func (_ *baseOnlineAccountData) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*baseOnlineAccountData) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *baseOnlineAccountData) Msgsize() (s int) { - s = 1 + 2 + (*z).baseVotingData.VoteID.Msgsize() + 2 + (*z).baseVotingData.SelectionID.Msgsize() + 2 + (*z).baseVotingData.VoteFirstValid.Msgsize() + 2 + (*z).baseVotingData.VoteLastValid.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).baseVotingData.StateProofID.Msgsize() + 2 + (*z).MicroAlgos.Msgsize() + 2 + msgp.Uint64Size - return -} - -// MsgIsZero returns whether this is a zero value -func (z *baseOnlineAccountData) MsgIsZero() bool { - return ((*z).baseVotingData.VoteID.MsgIsZero()) && ((*z).baseVotingData.SelectionID.MsgIsZero()) && ((*z).baseVotingData.VoteFirstValid.MsgIsZero()) && ((*z).baseVotingData.VoteLastValid.MsgIsZero()) && ((*z).baseVotingData.VoteKeyDilution == 0) && ((*z).baseVotingData.StateProofID.MsgIsZero()) && ((*z).MicroAlgos.MsgIsZero()) && ((*z).RewardsBase == 0) -} - -// MarshalMsg implements msgp.Marshaler -func (z *baseVotingData) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0001Len := uint32(6) - var zb0001Mask uint8 /* 7 bits */ - if (*z).VoteID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x1 - } - if (*z).SelectionID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x2 - } - if (*z).VoteFirstValid.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x4 - } - if (*z).VoteLastValid.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x8 - } - if (*z).VoteKeyDilution == 0 { - zb0001Len-- - zb0001Mask |= 0x10 - } - if (*z).StateProofID.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x20 - } - // variable map header, size zb0001Len - o = append(o, 0x80|uint8(zb0001Len)) - if zb0001Len != 0 { - if (zb0001Mask & 0x1) == 0 { // if not empty - // string "A" - o = append(o, 0xa1, 0x41) - o = (*z).VoteID.MarshalMsg(o) - } - if (zb0001Mask & 0x2) == 0 { // if not empty - // string "B" - o = append(o, 0xa1, 0x42) - o = (*z).SelectionID.MarshalMsg(o) - } - if (zb0001Mask & 0x4) == 0 { // if not empty - // string "C" - o = append(o, 0xa1, 0x43) - o = (*z).VoteFirstValid.MarshalMsg(o) - } - if (zb0001Mask & 0x8) == 0 { // if not empty - // string "D" - o = append(o, 0xa1, 0x44) - o = (*z).VoteLastValid.MarshalMsg(o) - } - if (zb0001Mask & 0x10) == 0 { // if not empty - // string "E" - o = append(o, 0xa1, 0x45) - o = msgp.AppendUint64(o, (*z).VoteKeyDilution) - } - if (zb0001Mask & 0x20) == 0 { // if not empty - // string "F" - o = append(o, 0xa1, 0x46) - o = (*z).StateProofID.MarshalMsg(o) - } - } - return -} - -func (_ *baseVotingData) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*baseVotingData) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *baseVotingData) 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-- - bts, err = (*z).VoteID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteID") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).SelectionID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "SelectionID") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).VoteFirstValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).VoteLastValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).StateProofID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "StateProofID") - return + for zb0002 := range (*z).KVs { + 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, "struct-from-array", "KVs", zb0002) + return + } + if zb0009 > 0 { + zb0009-- + var zb0011 int + zb0011, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array", "Key") + return + } + if zb0011 > encodedKVRecordV6MaxKeyLength { + err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedKVRecordV6MaxKeyLength)) + return + } + (*z).KVs[zb0002].Key, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Key) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array", "Key") + return + } + } + if zb0009 > 0 { + zb0009-- + var zb0012 int + zb0012, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array", "Value") + return + } + if zb0012 > encodedKVRecordV6MaxValueLength { + err = msgp.ErrOverflow(uint64(zb0012), uint64(encodedKVRecordV6MaxValueLength)) + return + } + (*z).KVs[zb0002].Value, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Value) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array", "Value") + return + } + } + if zb0009 > 0 { + err = msgp.ErrTooManyArrayFields(zb0009) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002) + return + } + if zb0010 { + (*z).KVs[zb0002] = encodedKVRecordV6{} + } + for zb0009 > 0 { + zb0009-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002) + return + } + switch string(field) { + case "k": + var zb0013 int + zb0013, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "Key") + return + } + if zb0013 > encodedKVRecordV6MaxKeyLength { + err = msgp.ErrOverflow(uint64(zb0013), uint64(encodedKVRecordV6MaxKeyLength)) + return + } + (*z).KVs[zb0002].Key, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Key) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "Key") + return + } + case "v": + var zb0014 int + zb0014, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "Value") + return + } + if zb0014 > encodedKVRecordV6MaxValueLength { + err = msgp.ErrOverflow(uint64(zb0014), uint64(encodedKVRecordV6MaxValueLength)) + return + } + (*z).KVs[zb0002].Value, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Value) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "Value") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002) + return + } + } + } + } } } - 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 @@ -1464,352 +969,169 @@ func (z *baseVotingData) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0002 { - (*z) = baseVotingData{} + if zb0004 { + (*z) = catchpointFileChunkV6{} } - 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": - bts, err = (*z).VoteID.UnmarshalMsg(bts) + case "bl": + var zb0015 int + var zb0016 bool + zb0015, zb0016, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "VoteID") + err = msgp.WrapError(err, "Balances") return } - case "B": - bts, err = (*z).SelectionID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "SelectionID") + if zb0015 > BalancesPerCatchpointFileChunk { + err = msgp.ErrOverflow(uint64(zb0015), uint64(BalancesPerCatchpointFileChunk)) + err = msgp.WrapError(err, "Balances") return } - case "C": - bts, err = (*z).VoteFirstValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "VoteFirstValid") - return + if zb0016 { + (*z).Balances = nil + } else if (*z).Balances != nil && cap((*z).Balances) >= zb0015 { + (*z).Balances = ((*z).Balances)[:zb0015] + } else { + (*z).Balances = make([]encodedBalanceRecordV6, zb0015) } - case "D": - bts, err = (*z).VoteLastValid.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "VoteLastValid") - return + for zb0001 := range (*z).Balances { + bts, err = (*z).Balances[zb0001].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001) + return + } } - case "E": - (*z).VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + case "kv": + var zb0017 int + var zb0018 bool + zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "VoteKeyDilution") + err = msgp.WrapError(err, "KVs") return } - case "F": - bts, err = (*z).StateProofID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "StateProofID") + if zb0017 > BalancesPerCatchpointFileChunk { + err = msgp.ErrOverflow(uint64(zb0017), uint64(BalancesPerCatchpointFileChunk)) + err = msgp.WrapError(err, "KVs") return } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - } - o = bts - return -} - -func (_ *baseVotingData) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*baseVotingData) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *baseVotingData) Msgsize() (s int) { - s = 1 + 2 + (*z).VoteID.Msgsize() + 2 + (*z).SelectionID.Msgsize() + 2 + (*z).VoteFirstValid.Msgsize() + 2 + (*z).VoteLastValid.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).StateProofID.Msgsize() - return -} - -// MsgIsZero returns whether this is a zero value -func (z *baseVotingData) MsgIsZero() bool { - return ((*z).VoteID.MsgIsZero()) && ((*z).SelectionID.MsgIsZero()) && ((*z).VoteFirstValid.MsgIsZero()) && ((*z).VoteLastValid.MsgIsZero()) && ((*z).VoteKeyDilution == 0) && ((*z).StateProofID.MsgIsZero()) -} - -// MarshalMsg implements msgp.Marshaler -func (z *catchpointFileBalancesChunkV5) MarshalMsg(b []byte) (o []byte) { - 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 - } - // 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))) - } - 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 = (*z).Balances[zb0001].AccountData.MarshalMsg(o) - } - if (zb0003Mask & 0x4) == 0 { // if not empty - // string "pk" - o = append(o, 0xa2, 0x70, 0x6b) - o = (*z).Balances[zb0001].Address.MarshalMsg(o) - } - } - } - } - return -} - -func (_ *catchpointFileBalancesChunkV5) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFileBalancesChunkV5) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *catchpointFileBalancesChunkV5) 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-- - var zb0004 int - var zb0005 bool - zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances") - return - } - if zb0004 > BalancesPerCatchpointFileChunk { - err = msgp.ErrOverflow(uint64(zb0004), uint64(BalancesPerCatchpointFileChunk)) - err = msgp.WrapError(err, "struct-from-array", "Balances") - return - } - if zb0005 { - (*z).Balances = nil - } else if (*z).Balances != nil && cap((*z).Balances) >= zb0004 { - (*z).Balances = ((*z).Balances)[:zb0004] - } else { - (*z).Balances = make([]encodedBalanceRecordV5, zb0004) - } - 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 - } - } + if zb0018 { + (*z).KVs = nil + } else if (*z).KVs != nil && cap((*z).KVs) >= zb0017 { + (*z).KVs = ((*z).KVs)[:zb0017] } else { - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) - return - } - if zb0007 { - (*z).Balances[zb0001] = encodedBalanceRecordV5{} - } - for zb0006 > 0 { - zb0006-- - field, bts, err = msgp.ReadMapKeyZC(bts) + (*z).KVs = make([]encodedKVRecordV6, zb0017) + } + for zb0002 := range (*z).KVs { + var zb0019 int + var zb0020 bool + zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) + err = msgp.WrapError(err, "KVs", zb0002) return } - switch string(field) { - case "pk": - bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) + if zb0019 > 0 { + zb0019-- + var zb0021 int + zb0021, err = msgp.ReadBytesBytesHeader(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "Address") + err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array", "Key") return } - case "ad": - bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "AccountData") + if zb0021 > encodedKVRecordV6MaxKeyLength { + err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedKVRecordV6MaxKeyLength)) return } - default: - err = msgp.ErrNoField(string(field)) + (*z).KVs[zb0002].Key, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Key) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) + err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array", "Key") 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) = catchpointFileBalancesChunkV5{} - } - for zb0002 > 0 { - zb0002-- - 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) - if err != nil { - err = msgp.WrapError(err, "Balances") - return - } - if zb0008 > BalancesPerCatchpointFileChunk { - err = msgp.ErrOverflow(uint64(zb0008), uint64(BalancesPerCatchpointFileChunk)) - err = msgp.WrapError(err, "Balances") - return - } - if zb0009 { - (*z).Balances = nil - } else if (*z).Balances != nil && cap((*z).Balances) >= zb0008 { - (*z).Balances = ((*z).Balances)[:zb0008] - } else { - (*z).Balances = make([]encodedBalanceRecordV5, zb0008) - } - 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 zb0019 > 0 { + zb0019-- + var zb0022 int + zb0022, err = msgp.ReadBytesBytesHeader(bts) if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array", "Address") + err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array", "Value") return } - } - if zb0010 > 0 { - zb0010-- - bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) + if zb0022 > encodedKVRecordV6MaxValueLength { + err = msgp.ErrOverflow(uint64(zb0022), uint64(encodedKVRecordV6MaxValueLength)) + return + } + (*z).KVs[zb0002].Value, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Value) if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array", "AccountData") + err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array", "Value") return } } - if zb0010 > 0 { - err = msgp.ErrTooManyArrayFields(zb0010) + if zb0019 > 0 { + err = msgp.ErrTooManyArrayFields(zb0019) if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array") + err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array") return } } } else { if err != nil { - err = msgp.WrapError(err, "Balances", zb0001) + err = msgp.WrapError(err, "KVs", zb0002) return } - if zb0011 { - (*z).Balances[zb0001] = encodedBalanceRecordV5{} + if zb0020 { + (*z).KVs[zb0002] = encodedKVRecordV6{} } - for zb0010 > 0 { - zb0010-- + for zb0019 > 0 { + zb0019-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { - err = msgp.WrapError(err, "Balances", zb0001) + err = msgp.WrapError(err, "KVs", zb0002) return } switch string(field) { - case "pk": - bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) + case "k": + var zb0023 int + zb0023, err = msgp.ReadBytesBytesHeader(bts) if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "Address") + err = msgp.WrapError(err, "KVs", zb0002, "Key") return } - case "ad": - bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) + if zb0023 > encodedKVRecordV6MaxKeyLength { + err = msgp.ErrOverflow(uint64(zb0023), uint64(encodedKVRecordV6MaxKeyLength)) + return + } + (*z).KVs[zb0002].Key, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Key) if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "AccountData") + err = msgp.WrapError(err, "KVs", zb0002, "Key") + return + } + case "v": + var zb0024 int + zb0024, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "KVs", zb0002, "Value") + return + } + if zb0024 > encodedKVRecordV6MaxValueLength { + err = msgp.ErrOverflow(uint64(zb0024), uint64(encodedKVRecordV6MaxValueLength)) + return + } + (*z).KVs[zb0002].Value, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Value) + if err != nil { + err = msgp.WrapError(err, "KVs", zb0002, "Value") return } default: err = msgp.ErrNoField(string(field)) if err != nil { - err = msgp.WrapError(err, "Balances", zb0001) + err = msgp.WrapError(err, "KVs", zb0002) return } } @@ -1819,1909 +1141,106 @@ func (z *catchpointFileBalancesChunkV5) UnmarshalMsg(bts []byte) (o []byte, err default: err = msgp.ErrNoField(string(field)) if err != nil { - err = msgp.WrapError(err) - return - } - } - } - } - o = bts - return -} - -func (_ *catchpointFileBalancesChunkV5) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFileBalancesChunkV5) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *catchpointFileBalancesChunkV5) 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 *catchpointFileBalancesChunkV5) MsgIsZero() bool { - return (len((*z).Balances) == 0) -} - -// MarshalMsg implements msgp.Marshaler -func (z *catchpointFileChunkV6) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0003Len := uint32(2) - var zb0003Mask uint8 /* 4 bits */ - if len((*z).Balances) == 0 { - zb0003Len-- - zb0003Mask |= 0x2 - } - if len((*z).KVs) == 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 "bl" - o = append(o, 0xa2, 0x62, 0x6c) - if (*z).Balances == nil { - o = msgp.AppendNil(o) - } else { - o = msgp.AppendArrayHeader(o, uint32(len((*z).Balances))) - } - for zb0001 := range (*z).Balances { - o = (*z).Balances[zb0001].MarshalMsg(o) - } - } - if (zb0003Mask & 0x4) == 0 { // if not empty - // string "kv" - o = append(o, 0xa2, 0x6b, 0x76) - if (*z).KVs == nil { - o = msgp.AppendNil(o) - } else { - o = msgp.AppendArrayHeader(o, uint32(len((*z).KVs))) - } - for zb0002 := range (*z).KVs { - // omitempty: check for empty values - zb0004Len := uint32(2) - var zb0004Mask uint8 /* 3 bits */ - if len((*z).KVs[zb0002].Key) == 0 { - zb0004Len-- - zb0004Mask |= 0x2 - } - if len((*z).KVs[zb0002].Value) == 0 { - zb0004Len-- - zb0004Mask |= 0x4 - } - // variable map header, size zb0004Len - o = append(o, 0x80|uint8(zb0004Len)) - if (zb0004Mask & 0x2) == 0 { // if not empty - // string "k" - o = append(o, 0xa1, 0x6b) - o = msgp.AppendBytes(o, (*z).KVs[zb0002].Key) - } - if (zb0004Mask & 0x4) == 0 { // if not empty - // string "v" - o = append(o, 0xa1, 0x76) - o = msgp.AppendBytes(o, (*z).KVs[zb0002].Value) - } - } - } - } - return -} - -func (_ *catchpointFileChunkV6) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFileChunkV6) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *catchpointFileChunkV6) 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 - } - if zb0003 > 0 { - zb0003-- - var zb0005 int - var zb0006 bool - zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances") - return - } - if zb0005 > BalancesPerCatchpointFileChunk { - err = msgp.ErrOverflow(uint64(zb0005), uint64(BalancesPerCatchpointFileChunk)) - err = msgp.WrapError(err, "struct-from-array", "Balances") - return - } - if zb0006 { - (*z).Balances = nil - } else if (*z).Balances != nil && cap((*z).Balances) >= zb0005 { - (*z).Balances = ((*z).Balances)[:zb0005] - } else { - (*z).Balances = make([]encodedBalanceRecordV6, zb0005) - } - for zb0001 := range (*z).Balances { - bts, err = (*z).Balances[zb0001].UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) - return - } - } - } - if zb0003 > 0 { - zb0003-- - var zb0007 int - var zb0008 bool - zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs") - return - } - if zb0007 > BalancesPerCatchpointFileChunk { - err = msgp.ErrOverflow(uint64(zb0007), uint64(BalancesPerCatchpointFileChunk)) - err = msgp.WrapError(err, "struct-from-array", "KVs") - return - } - if zb0008 { - (*z).KVs = nil - } else if (*z).KVs != nil && cap((*z).KVs) >= zb0007 { - (*z).KVs = ((*z).KVs)[:zb0007] - } else { - (*z).KVs = make([]encodedKVRecordV6, zb0007) - } - for zb0002 := range (*z).KVs { - 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, "struct-from-array", "KVs", zb0002) - return - } - if zb0009 > 0 { - zb0009-- - var zb0011 int - zb0011, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array", "Key") - return - } - if zb0011 > encodedKVRecordV6MaxKeyLength { - err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedKVRecordV6MaxKeyLength)) - return - } - (*z).KVs[zb0002].Key, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Key) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array", "Key") - return - } - } - if zb0009 > 0 { - zb0009-- - var zb0012 int - zb0012, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array", "Value") - return - } - if zb0012 > encodedKVRecordV6MaxValueLength { - err = msgp.ErrOverflow(uint64(zb0012), uint64(encodedKVRecordV6MaxValueLength)) - return - } - (*z).KVs[zb0002].Value, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Value) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array", "Value") - return - } - } - if zb0009 > 0 { - err = msgp.ErrTooManyArrayFields(zb0009) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "struct-from-array") - return - } - } - } else { - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002) - return - } - if zb0010 { - (*z).KVs[zb0002] = encodedKVRecordV6{} - } - for zb0009 > 0 { - zb0009-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002) - return - } - switch string(field) { - case "k": - var zb0013 int - zb0013, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "Key") - return - } - if zb0013 > encodedKVRecordV6MaxKeyLength { - err = msgp.ErrOverflow(uint64(zb0013), uint64(encodedKVRecordV6MaxKeyLength)) - return - } - (*z).KVs[zb0002].Key, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Key) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "Key") - return - } - case "v": - var zb0014 int - zb0014, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "Value") - return - } - if zb0014 > encodedKVRecordV6MaxValueLength { - err = msgp.ErrOverflow(uint64(zb0014), uint64(encodedKVRecordV6MaxValueLength)) - return - } - (*z).KVs[zb0002].Value, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Value) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002, "Value") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KVs", zb0002) - return - } - } - } - } - } - } - if zb0003 > 0 { - err = msgp.ErrTooManyArrayFields(zb0003) - if err != nil { - err = msgp.WrapError(err, "struct-from-array") - return - } - } - } else { - if err != nil { - err = msgp.WrapError(err) - return - } - if zb0004 { - (*z) = catchpointFileChunkV6{} - } - for zb0003 > 0 { - zb0003-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch string(field) { - case "bl": - var zb0015 int - var zb0016 bool - zb0015, zb0016, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Balances") - return - } - if zb0015 > BalancesPerCatchpointFileChunk { - err = msgp.ErrOverflow(uint64(zb0015), uint64(BalancesPerCatchpointFileChunk)) - err = msgp.WrapError(err, "Balances") - return - } - if zb0016 { - (*z).Balances = nil - } else if (*z).Balances != nil && cap((*z).Balances) >= zb0015 { - (*z).Balances = ((*z).Balances)[:zb0015] - } else { - (*z).Balances = make([]encodedBalanceRecordV6, zb0015) - } - for zb0001 := range (*z).Balances { - bts, err = (*z).Balances[zb0001].UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001) - return - } - } - case "kv": - var zb0017 int - var zb0018 bool - zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "KVs") - return - } - if zb0017 > BalancesPerCatchpointFileChunk { - err = msgp.ErrOverflow(uint64(zb0017), uint64(BalancesPerCatchpointFileChunk)) - err = msgp.WrapError(err, "KVs") - return - } - if zb0018 { - (*z).KVs = nil - } else if (*z).KVs != nil && cap((*z).KVs) >= zb0017 { - (*z).KVs = ((*z).KVs)[:zb0017] - } else { - (*z).KVs = make([]encodedKVRecordV6, zb0017) - } - for zb0002 := range (*z).KVs { - var zb0019 int - var zb0020 bool - zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) - if _, ok := err.(msgp.TypeError); ok { - zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002) - return - } - if zb0019 > 0 { - zb0019-- - var zb0021 int - zb0021, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array", "Key") - return - } - if zb0021 > encodedKVRecordV6MaxKeyLength { - err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedKVRecordV6MaxKeyLength)) - return - } - (*z).KVs[zb0002].Key, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Key) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array", "Key") - return - } - } - if zb0019 > 0 { - zb0019-- - var zb0022 int - zb0022, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array", "Value") - return - } - if zb0022 > encodedKVRecordV6MaxValueLength { - err = msgp.ErrOverflow(uint64(zb0022), uint64(encodedKVRecordV6MaxValueLength)) - return - } - (*z).KVs[zb0002].Value, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Value) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array", "Value") - return - } - } - if zb0019 > 0 { - err = msgp.ErrTooManyArrayFields(zb0019) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "struct-from-array") - return - } - } - } else { - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002) - return - } - if zb0020 { - (*z).KVs[zb0002] = encodedKVRecordV6{} - } - for zb0019 > 0 { - zb0019-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002) - return - } - switch string(field) { - case "k": - var zb0023 int - zb0023, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "Key") - return - } - if zb0023 > encodedKVRecordV6MaxKeyLength { - err = msgp.ErrOverflow(uint64(zb0023), uint64(encodedKVRecordV6MaxKeyLength)) - return - } - (*z).KVs[zb0002].Key, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Key) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "Key") - return - } - case "v": - var zb0024 int - zb0024, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "Value") - return - } - if zb0024 > encodedKVRecordV6MaxValueLength { - err = msgp.ErrOverflow(uint64(zb0024), uint64(encodedKVRecordV6MaxValueLength)) - return - } - (*z).KVs[zb0002].Value, bts, err = msgp.ReadBytesBytes(bts, (*z).KVs[zb0002].Value) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002, "Value") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err, "KVs", zb0002) - return - } - } - } - } - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - } - o = bts - return -} - -func (_ *catchpointFileChunkV6) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFileChunkV6) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *catchpointFileChunkV6) Msgsize() (s int) { - s = 1 + 3 + msgp.ArrayHeaderSize - for zb0001 := range (*z).Balances { - s += (*z).Balances[zb0001].Msgsize() - } - s += 3 + msgp.ArrayHeaderSize - for zb0002 := range (*z).KVs { - s += 1 + 2 + msgp.BytesPrefixSize + len((*z).KVs[zb0002].Key) + 2 + msgp.BytesPrefixSize + len((*z).KVs[zb0002].Value) - } - return -} - -// MsgIsZero returns whether this is a zero value -func (z *catchpointFileChunkV6) MsgIsZero() bool { - return (len((*z).Balances) == 0) && (len((*z).KVs) == 0) -} - -// MarshalMsg implements msgp.Marshaler -func (z *catchpointFirstStageInfo) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0001Len := uint32(6) - var zb0001Mask uint8 /* 7 bits */ - if (*z).Totals.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x2 - } - if (*z).TotalAccounts == 0 { - zb0001Len-- - zb0001Mask |= 0x4 - } - if (*z).BiggestChunkLen == 0 { - zb0001Len-- - zb0001Mask |= 0x8 - } - if (*z).TotalChunks == 0 { - zb0001Len-- - zb0001Mask |= 0x10 - } - if (*z).TotalKVs == 0 { - zb0001Len-- - zb0001Mask |= 0x20 - } - if (*z).TrieBalancesHash.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x40 - } - // 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 = (*z).Totals.MarshalMsg(o) - } - 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 "biggestChunk" - o = append(o, 0xac, 0x62, 0x69, 0x67, 0x67, 0x65, 0x73, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b) - o = msgp.AppendUint64(o, (*z).BiggestChunkLen) - } - if (zb0001Mask & 0x10) == 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 & 0x20) == 0 { // if not empty - // string "kvsCount" - o = append(o, 0xa8, 0x6b, 0x76, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74) - o = msgp.AppendUint64(o, (*z).TotalKVs) - } - if (zb0001Mask & 0x40) == 0 { // if not empty - // string "trieBalancesHash" - o = append(o, 0xb0, 0x74, 0x72, 0x69, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x48, 0x61, 0x73, 0x68) - o = (*z).TrieBalancesHash.MarshalMsg(o) - } - } - return -} - -func (_ *catchpointFirstStageInfo) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFirstStageInfo) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *catchpointFirstStageInfo) 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-- - bts, err = (*z).Totals.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Totals") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).TrieBalancesHash.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TrieBalancesHash") - return - } - } - 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).TotalKVs, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalKVs") - 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).BiggestChunkLen, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "BiggestChunkLen") - 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) = catchpointFirstStageInfo{} - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch string(field) { - case "accountTotals": - bts, err = (*z).Totals.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Totals") - return - } - case "trieBalancesHash": - bts, err = (*z).TrieBalancesHash.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "TrieBalancesHash") - return - } - case "accountsCount": - (*z).TotalAccounts, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalAccounts") - return - } - case "kvsCount": - (*z).TotalKVs, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalKVs") - return - } - case "chunksCount": - (*z).TotalChunks, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalChunks") - return - } - case "biggestChunk": - (*z).BiggestChunkLen, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "BiggestChunkLen") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - } - o = bts - return -} - -func (_ *catchpointFirstStageInfo) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFirstStageInfo) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *catchpointFirstStageInfo) Msgsize() (s int) { - s = 1 + 14 + (*z).Totals.Msgsize() + 17 + (*z).TrieBalancesHash.Msgsize() + 14 + msgp.Uint64Size + 9 + msgp.Uint64Size + 12 + msgp.Uint64Size + 13 + msgp.Uint64Size - return -} - -// MsgIsZero returns whether this is a zero value -func (z *catchpointFirstStageInfo) MsgIsZero() bool { - return ((*z).Totals.MsgIsZero()) && ((*z).TrieBalancesHash.MsgIsZero()) && ((*z).TotalAccounts == 0) && ((*z).TotalKVs == 0) && ((*z).TotalChunks == 0) && ((*z).BiggestChunkLen == 0) -} - -// MarshalMsg implements msgp.Marshaler -func (z catchpointState) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - o = msgp.AppendString(o, string(z)) - return -} - -func (_ catchpointState) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(catchpointState) - if !ok { - _, ok = (z).(*catchpointState) - } - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *catchpointState) UnmarshalMsg(bts []byte) (o []byte, err error) { - { - var zb0001 string - zb0001, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - (*z) = catchpointState(zb0001) - } - o = bts - return -} - -func (_ *catchpointState) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointState) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z catchpointState) Msgsize() (s int) { - s = msgp.StringPrefixSize + len(string(z)) - return -} - -// MsgIsZero returns whether this is a zero value -func (z catchpointState) MsgIsZero() bool { - return z == "" -} - -// MarshalMsg implements msgp.Marshaler -func (z *encodedBalanceRecordV5) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0001Len := uint32(2) - var zb0001Mask uint8 /* 3 bits */ - if (*z).AccountData.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x2 - } - if (*z).Address.MsgIsZero() { - 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 "ad" - o = append(o, 0xa2, 0x61, 0x64) - o = (*z).AccountData.MarshalMsg(o) - } - if (zb0001Mask & 0x4) == 0 { // if not empty - // string "pk" - o = append(o, 0xa2, 0x70, 0x6b) - o = (*z).Address.MarshalMsg(o) - } - } - return -} - -func (_ *encodedBalanceRecordV5) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*encodedBalanceRecordV5) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *encodedBalanceRecordV5) 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-- - bts, err = (*z).Address.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Address") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "AccountData") - 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) = encodedBalanceRecordV5{} - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch string(field) { - case "pk": - bts, err = (*z).Address.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Address") - return - } - case "ad": - bts, err = (*z).AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "AccountData") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - } - o = bts - return -} - -func (_ *encodedBalanceRecordV5) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*encodedBalanceRecordV5) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *encodedBalanceRecordV5) Msgsize() (s int) { - s = 1 + 3 + (*z).Address.Msgsize() + 3 + (*z).AccountData.Msgsize() - return -} - -// MsgIsZero returns whether this is a zero value -func (z *encodedBalanceRecordV5) MsgIsZero() bool { - return ((*z).Address.MsgIsZero()) && ((*z).AccountData.MsgIsZero()) -} - -// MarshalMsg implements msgp.Marshaler -func (z *encodedBalanceRecordV6) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0003Len := uint32(4) - var zb0003Mask uint8 /* 5 bits */ - if (*z).Address.MsgIsZero() { - zb0003Len-- - zb0003Mask |= 0x2 - } - if (*z).AccountData.MsgIsZero() { - zb0003Len-- - zb0003Mask |= 0x4 - } - if len((*z).Resources) == 0 { - zb0003Len-- - zb0003Mask |= 0x8 - } - if (*z).ExpectingMoreEntries == false { - zb0003Len-- - zb0003Mask |= 0x10 - } - // variable map header, size zb0003Len - o = append(o, 0x80|uint8(zb0003Len)) - if zb0003Len != 0 { - if (zb0003Mask & 0x2) == 0 { // if not empty - // string "a" - o = append(o, 0xa1, 0x61) - o = (*z).Address.MarshalMsg(o) - } - if (zb0003Mask & 0x4) == 0 { // if not empty - // string "b" - o = append(o, 0xa1, 0x62) - o = (*z).AccountData.MarshalMsg(o) - } - if (zb0003Mask & 0x8) == 0 { // if not empty - // string "c" - o = append(o, 0xa1, 0x63) - if (*z).Resources == nil { - o = msgp.AppendNil(o) - } else { - o = msgp.AppendMapHeader(o, uint32(len((*z).Resources))) - } - zb0001_keys := make([]uint64, 0, len((*z).Resources)) - for zb0001 := range (*z).Resources { - zb0001_keys = append(zb0001_keys, zb0001) - } - sort.Sort(SortUint64(zb0001_keys)) - for _, zb0001 := range zb0001_keys { - zb0002 := (*z).Resources[zb0001] - _ = zb0002 - o = msgp.AppendUint64(o, zb0001) - o = zb0002.MarshalMsg(o) - } - } - if (zb0003Mask & 0x10) == 0 { // if not empty - // string "e" - o = append(o, 0xa1, 0x65) - o = msgp.AppendBool(o, (*z).ExpectingMoreEntries) - } - } - return -} - -func (_ *encodedBalanceRecordV6) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*encodedBalanceRecordV6) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *encodedBalanceRecordV6) 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 - } - if zb0003 > 0 { - zb0003-- - bts, err = (*z).Address.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Address") - return - } - } - if zb0003 > 0 { - zb0003-- - bts, err = (*z).AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "AccountData") - return - } - } - if zb0003 > 0 { - zb0003-- - var zb0005 int - var zb0006 bool - zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Resources") - return - } - if zb0005 > resourcesPerCatchpointFileChunkBackwardCompatible { - err = msgp.ErrOverflow(uint64(zb0005), uint64(resourcesPerCatchpointFileChunkBackwardCompatible)) - err = msgp.WrapError(err, "struct-from-array", "Resources") - return - } - if zb0006 { - (*z).Resources = nil - } else if (*z).Resources == nil { - (*z).Resources = make(map[uint64]msgp.Raw, zb0005) - } - for zb0005 > 0 { - var zb0001 uint64 - var zb0002 msgp.Raw - zb0005-- - zb0001, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Resources") - return - } - bts, err = zb0002.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Resources", zb0001) - return - } - (*z).Resources[zb0001] = zb0002 - } - } - if zb0003 > 0 { - zb0003-- - (*z).ExpectingMoreEntries, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "ExpectingMoreEntries") - return - } - } - if zb0003 > 0 { - err = msgp.ErrTooManyArrayFields(zb0003) - if err != nil { - err = msgp.WrapError(err, "struct-from-array") - return - } - } - } else { - if err != nil { - err = msgp.WrapError(err) - return - } - if zb0004 { - (*z) = encodedBalanceRecordV6{} - } - for zb0003 > 0 { - zb0003-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch string(field) { - case "a": - bts, err = (*z).Address.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Address") - return - } - case "b": - bts, err = (*z).AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "AccountData") - return - } - case "c": - var zb0007 int - var zb0008 bool - zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Resources") - return - } - if zb0007 > resourcesPerCatchpointFileChunkBackwardCompatible { - err = msgp.ErrOverflow(uint64(zb0007), uint64(resourcesPerCatchpointFileChunkBackwardCompatible)) - err = msgp.WrapError(err, "Resources") - return - } - if zb0008 { - (*z).Resources = nil - } else if (*z).Resources == nil { - (*z).Resources = make(map[uint64]msgp.Raw, zb0007) - } - for zb0007 > 0 { - var zb0001 uint64 - var zb0002 msgp.Raw - zb0007-- - zb0001, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Resources") - return - } - bts, err = zb0002.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Resources", zb0001) - return - } - (*z).Resources[zb0001] = zb0002 - } - case "e": - (*z).ExpectingMoreEntries, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "ExpectingMoreEntries") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - } - o = bts - return -} - -func (_ *encodedBalanceRecordV6) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*encodedBalanceRecordV6) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *encodedBalanceRecordV6) Msgsize() (s int) { - s = 1 + 2 + (*z).Address.Msgsize() + 2 + (*z).AccountData.Msgsize() + 2 + msgp.MapHeaderSize - if (*z).Resources != nil { - for zb0001, zb0002 := range (*z).Resources { - _ = zb0001 - _ = zb0002 - s += 0 + msgp.Uint64Size + zb0002.Msgsize() - } - } - s += 2 + msgp.BoolSize - return -} - -// MsgIsZero returns whether this is a zero value -func (z *encodedBalanceRecordV6) MsgIsZero() bool { - return ((*z).Address.MsgIsZero()) && ((*z).AccountData.MsgIsZero()) && (len((*z).Resources) == 0) && ((*z).ExpectingMoreEntries == false) -} - -// MarshalMsg implements msgp.Marshaler -func (z *encodedKVRecordV6) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0001Len := uint32(2) - var zb0001Mask uint8 /* 3 bits */ - if len((*z).Key) == 0 { - zb0001Len-- - zb0001Mask |= 0x2 - } - if len((*z).Value) == 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 "k" - o = append(o, 0xa1, 0x6b) - o = msgp.AppendBytes(o, (*z).Key) - } - if (zb0001Mask & 0x4) == 0 { // if not empty - // string "v" - o = append(o, 0xa1, 0x76) - o = msgp.AppendBytes(o, (*z).Value) - } - } - return -} - -func (_ *encodedKVRecordV6) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*encodedKVRecordV6) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *encodedKVRecordV6) 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 - zb0003, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Key") - return - } - if zb0003 > encodedKVRecordV6MaxKeyLength { - err = msgp.ErrOverflow(uint64(zb0003), uint64(encodedKVRecordV6MaxKeyLength)) - return - } - (*z).Key, bts, err = msgp.ReadBytesBytes(bts, (*z).Key) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Key") - return - } - } - if zb0001 > 0 { - zb0001-- - var zb0004 int - zb0004, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Value") - return - } - if zb0004 > encodedKVRecordV6MaxValueLength { - err = msgp.ErrOverflow(uint64(zb0004), uint64(encodedKVRecordV6MaxValueLength)) - return - } - (*z).Value, bts, err = msgp.ReadBytesBytes(bts, (*z).Value) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Value") - 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) = encodedKVRecordV6{} - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch string(field) { - case "k": - var zb0005 int - zb0005, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "Key") - return - } - if zb0005 > encodedKVRecordV6MaxKeyLength { - err = msgp.ErrOverflow(uint64(zb0005), uint64(encodedKVRecordV6MaxKeyLength)) - return - } - (*z).Key, bts, err = msgp.ReadBytesBytes(bts, (*z).Key) - if err != nil { - err = msgp.WrapError(err, "Key") - return - } - case "v": - var zb0006 int - zb0006, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "Value") - return - } - if zb0006 > encodedKVRecordV6MaxValueLength { - err = msgp.ErrOverflow(uint64(zb0006), uint64(encodedKVRecordV6MaxValueLength)) - return - } - (*z).Value, bts, err = msgp.ReadBytesBytes(bts, (*z).Value) - if err != nil { - err = msgp.WrapError(err, "Value") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - } - o = bts - return -} - -func (_ *encodedKVRecordV6) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*encodedKVRecordV6) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *encodedKVRecordV6) Msgsize() (s int) { - s = 1 + 2 + msgp.BytesPrefixSize + len((*z).Key) + 2 + msgp.BytesPrefixSize + len((*z).Value) - return -} - -// MsgIsZero returns whether this is a zero value -func (z *encodedKVRecordV6) MsgIsZero() bool { - return (len((*z).Key) == 0) && (len((*z).Value) == 0) -} - -// MarshalMsg implements msgp.Marshaler -func (z hashKind) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - o = msgp.AppendByte(o, byte(z)) - return -} - -func (_ hashKind) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(hashKind) - if !ok { - _, ok = (z).(*hashKind) - } - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *hashKind) 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) = hashKind(zb0001) - } - o = bts - return -} - -func (_ *hashKind) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*hashKind) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z hashKind) Msgsize() (s int) { - s = msgp.ByteSize - return -} - -// MsgIsZero returns whether this is a zero value -func (z hashKind) MsgIsZero() bool { - return z == 0 -} - -// MarshalMsg implements msgp.Marshaler -func (z resourceFlags) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - o = msgp.AppendUint8(o, uint8(z)) - return -} - -func (_ resourceFlags) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(resourceFlags) - if !ok { - _, ok = (z).(*resourceFlags) - } - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *resourceFlags) UnmarshalMsg(bts []byte) (o []byte, err error) { - { - var zb0001 uint8 - zb0001, bts, err = msgp.ReadUint8Bytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - (*z) = resourceFlags(zb0001) - } - o = bts - return -} - -func (_ *resourceFlags) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*resourceFlags) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z resourceFlags) Msgsize() (s int) { - s = msgp.Uint8Size - return -} - -// MsgIsZero returns whether this is a zero value -func (z resourceFlags) MsgIsZero() bool { - return z == 0 -} - -// MarshalMsg implements msgp.Marshaler -func (z *resourcesData) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0002Len := uint32(26) - var zb0002Mask uint32 /* 27 bits */ - if (*z).Total == 0 { - zb0002Len-- - zb0002Mask |= 0x2 - } - if (*z).Decimals == 0 { - zb0002Len-- - zb0002Mask |= 0x4 - } - if (*z).DefaultFrozen == false { - zb0002Len-- - zb0002Mask |= 0x8 - } - if (*z).UnitName == "" { - zb0002Len-- - zb0002Mask |= 0x10 - } - if (*z).AssetName == "" { - zb0002Len-- - zb0002Mask |= 0x20 - } - if (*z).URL == "" { - zb0002Len-- - zb0002Mask |= 0x40 - } - if (*z).MetadataHash == ([32]byte{}) { - zb0002Len-- - zb0002Mask |= 0x80 - } - if (*z).Manager.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x100 - } - if (*z).Reserve.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x200 - } - if (*z).Freeze.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x400 - } - if (*z).Clawback.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x800 - } - if (*z).Amount == 0 { - zb0002Len-- - zb0002Mask |= 0x1000 - } - if (*z).Frozen == false { - zb0002Len-- - zb0002Mask |= 0x2000 - } - if (*z).SchemaNumUint == 0 { - zb0002Len-- - zb0002Mask |= 0x4000 - } - if (*z).SchemaNumByteSlice == 0 { - zb0002Len-- - zb0002Mask |= 0x8000 - } - if (*z).KeyValue.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x10000 - } - if len((*z).ApprovalProgram) == 0 { - zb0002Len-- - zb0002Mask |= 0x20000 - } - if len((*z).ClearStateProgram) == 0 { - zb0002Len-- - zb0002Mask |= 0x40000 - } - if (*z).GlobalState.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x80000 - } - if (*z).LocalStateSchemaNumUint == 0 { - zb0002Len-- - zb0002Mask |= 0x100000 - } - if (*z).LocalStateSchemaNumByteSlice == 0 { - zb0002Len-- - zb0002Mask |= 0x200000 - } - if (*z).GlobalStateSchemaNumUint == 0 { - zb0002Len-- - zb0002Mask |= 0x400000 - } - if (*z).GlobalStateSchemaNumByteSlice == 0 { - zb0002Len-- - zb0002Mask |= 0x800000 - } - if (*z).ExtraProgramPages == 0 { - zb0002Len-- - zb0002Mask |= 0x1000000 - } - if (*z).ResourceFlags == 0 { - zb0002Len-- - zb0002Mask |= 0x2000000 - } - if (*z).UpdateRound == 0 { - zb0002Len-- - zb0002Mask |= 0x4000000 - } - // variable map header, size zb0002Len - o = msgp.AppendMapHeader(o, zb0002Len) - if zb0002Len != 0 { - if (zb0002Mask & 0x2) == 0 { // if not empty - // string "a" - o = append(o, 0xa1, 0x61) - o = msgp.AppendUint64(o, (*z).Total) - } - if (zb0002Mask & 0x4) == 0 { // if not empty - // string "b" - o = append(o, 0xa1, 0x62) - o = msgp.AppendUint32(o, (*z).Decimals) - } - if (zb0002Mask & 0x8) == 0 { // if not empty - // string "c" - o = append(o, 0xa1, 0x63) - o = msgp.AppendBool(o, (*z).DefaultFrozen) - } - if (zb0002Mask & 0x10) == 0 { // if not empty - // string "d" - o = append(o, 0xa1, 0x64) - o = msgp.AppendString(o, (*z).UnitName) - } - if (zb0002Mask & 0x20) == 0 { // if not empty - // string "e" - o = append(o, 0xa1, 0x65) - o = msgp.AppendString(o, (*z).AssetName) - } - if (zb0002Mask & 0x40) == 0 { // if not empty - // string "f" - o = append(o, 0xa1, 0x66) - o = msgp.AppendString(o, (*z).URL) - } - if (zb0002Mask & 0x80) == 0 { // if not empty - // string "g" - o = append(o, 0xa1, 0x67) - o = msgp.AppendBytes(o, ((*z).MetadataHash)[:]) - } - if (zb0002Mask & 0x100) == 0 { // if not empty - // string "h" - o = append(o, 0xa1, 0x68) - o = (*z).Manager.MarshalMsg(o) - } - if (zb0002Mask & 0x200) == 0 { // if not empty - // string "i" - o = append(o, 0xa1, 0x69) - o = (*z).Reserve.MarshalMsg(o) - } - if (zb0002Mask & 0x400) == 0 { // if not empty - // string "j" - o = append(o, 0xa1, 0x6a) - o = (*z).Freeze.MarshalMsg(o) - } - if (zb0002Mask & 0x800) == 0 { // if not empty - // string "k" - o = append(o, 0xa1, 0x6b) - o = (*z).Clawback.MarshalMsg(o) - } - if (zb0002Mask & 0x1000) == 0 { // if not empty - // string "l" - o = append(o, 0xa1, 0x6c) - o = msgp.AppendUint64(o, (*z).Amount) - } - if (zb0002Mask & 0x2000) == 0 { // if not empty - // string "m" - o = append(o, 0xa1, 0x6d) - o = msgp.AppendBool(o, (*z).Frozen) - } - if (zb0002Mask & 0x4000) == 0 { // if not empty - // string "n" - o = append(o, 0xa1, 0x6e) - o = msgp.AppendUint64(o, (*z).SchemaNumUint) - } - if (zb0002Mask & 0x8000) == 0 { // if not empty - // string "o" - o = append(o, 0xa1, 0x6f) - o = msgp.AppendUint64(o, (*z).SchemaNumByteSlice) - } - if (zb0002Mask & 0x10000) == 0 { // if not empty - // string "p" - o = append(o, 0xa1, 0x70) - o = (*z).KeyValue.MarshalMsg(o) - } - if (zb0002Mask & 0x20000) == 0 { // if not empty - // string "q" - o = append(o, 0xa1, 0x71) - o = msgp.AppendBytes(o, (*z).ApprovalProgram) - } - if (zb0002Mask & 0x40000) == 0 { // if not empty - // string "r" - o = append(o, 0xa1, 0x72) - o = msgp.AppendBytes(o, (*z).ClearStateProgram) - } - if (zb0002Mask & 0x80000) == 0 { // if not empty - // string "s" - o = append(o, 0xa1, 0x73) - o = (*z).GlobalState.MarshalMsg(o) - } - if (zb0002Mask & 0x100000) == 0 { // if not empty - // string "t" - o = append(o, 0xa1, 0x74) - o = msgp.AppendUint64(o, (*z).LocalStateSchemaNumUint) - } - if (zb0002Mask & 0x200000) == 0 { // if not empty - // string "u" - o = append(o, 0xa1, 0x75) - o = msgp.AppendUint64(o, (*z).LocalStateSchemaNumByteSlice) - } - if (zb0002Mask & 0x400000) == 0 { // if not empty - // string "v" - o = append(o, 0xa1, 0x76) - o = msgp.AppendUint64(o, (*z).GlobalStateSchemaNumUint) - } - if (zb0002Mask & 0x800000) == 0 { // if not empty - // string "w" - o = append(o, 0xa1, 0x77) - o = msgp.AppendUint64(o, (*z).GlobalStateSchemaNumByteSlice) - } - if (zb0002Mask & 0x1000000) == 0 { // if not empty - // string "x" - o = append(o, 0xa1, 0x78) - o = msgp.AppendUint32(o, (*z).ExtraProgramPages) - } - if (zb0002Mask & 0x2000000) == 0 { // if not empty - // string "y" - o = append(o, 0xa1, 0x79) - o = msgp.AppendUint8(o, uint8((*z).ResourceFlags)) - } - if (zb0002Mask & 0x4000000) == 0 { // if not empty - // string "z" - o = append(o, 0xa1, 0x7a) - o = msgp.AppendUint64(o, (*z).UpdateRound) - } - } - return -} - -func (_ *resourcesData) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*resourcesData) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *resourcesData) 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 { - zb0002-- - (*z).Amount, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Amount") - return - } - } - if zb0002 > 0 { - zb0002-- - (*z).Frozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Frozen") - return - } - } - if zb0002 > 0 { - zb0002-- - (*z).SchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "SchemaNumUint") - return - } - } - if zb0002 > 0 { - zb0002-- - (*z).SchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "SchemaNumByteSlice") - return - } - } - if zb0002 > 0 { - zb0002-- - bts, err = (*z).KeyValue.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "KeyValue") - return - } - } - if zb0002 > 0 { - zb0002-- - var zb0004 int - zb0004, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") - return - } - if zb0004 > config.MaxAvailableAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0004), uint64(config.MaxAvailableAppProgramLen)) - return - } - (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") - return - } - } - if zb0002 > 0 { - zb0002-- - var zb0005 int - zb0005, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") - return - } - if zb0005 > config.MaxAvailableAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0005), uint64(config.MaxAvailableAppProgramLen)) - return - } - (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") - return - } - } - if zb0002 > 0 { - zb0002-- - bts, err = (*z).GlobalState.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "GlobalState") - return - } - } - if zb0002 > 0 { - zb0002-- - (*z).LocalStateSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "LocalStateSchemaNumUint") - return - } - } - if zb0002 > 0 { - zb0002-- - (*z).LocalStateSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "LocalStateSchemaNumByteSlice") - return - } - } - if zb0002 > 0 { - zb0002-- - (*z).GlobalStateSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchemaNumUint") - return + err = msgp.WrapError(err) + return + } } } - if zb0002 > 0 { - zb0002-- - (*z).GlobalStateSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchemaNumByteSlice") - return - } + } + o = bts + return +} + +func (_ *catchpointFileChunkV6) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*catchpointFileChunkV6) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *catchpointFileChunkV6) Msgsize() (s int) { + s = 1 + 3 + msgp.ArrayHeaderSize + for zb0001 := range (*z).Balances { + s += (*z).Balances[zb0001].Msgsize() + } + s += 3 + msgp.ArrayHeaderSize + for zb0002 := range (*z).KVs { + s += 1 + 2 + msgp.BytesPrefixSize + len((*z).KVs[zb0002].Key) + 2 + msgp.BytesPrefixSize + len((*z).KVs[zb0002].Value) + } + return +} + +// MsgIsZero returns whether this is a zero value +func (z *catchpointFileChunkV6) MsgIsZero() bool { + return (len((*z).Balances) == 0) && (len((*z).KVs) == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z *encodedBalanceRecordV5) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(2) + var zb0001Mask uint8 /* 3 bits */ + if (*z).AccountData.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).Address.MsgIsZero() { + 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 "ad" + o = append(o, 0xa2, 0x61, 0x64) + o = (*z).AccountData.MarshalMsg(o) } - if zb0002 > 0 { - zb0002-- - (*z).ExtraProgramPages, bts, err = msgp.ReadUint32Bytes(bts) + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "pk" + o = append(o, 0xa2, 0x70, 0x6b) + o = (*z).Address.MarshalMsg(o) + } + } + return +} + +func (_ *encodedBalanceRecordV5) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*encodedBalanceRecordV5) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *encodedBalanceRecordV5) 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-- + bts, err = (*z).Address.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "ExtraProgramPages") + err = msgp.WrapError(err, "struct-from-array", "Address") return } } - if zb0002 > 0 { - zb0002-- - { - var zb0006 uint8 - zb0006, bts, err = msgp.ReadUint8Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "ResourceFlags") - return - } - (*z).ResourceFlags = resourceFlags(zb0006) - } - } - if zb0002 > 0 { - zb0002-- - (*z).UpdateRound, bts, err = msgp.ReadUint64Bytes(bts) + if zb0001 > 0 { + zb0001-- + bts, err = (*z).AccountData.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "UpdateRound") + err = msgp.WrapError(err, "struct-from-array", "AccountData") return } } - if zb0002 > 0 { - err = msgp.ErrTooManyArrayFields(zb0002) + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -3732,195 +1251,27 @@ func (z *resourcesData) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0003 { - (*z) = resourcesData{} + if zb0002 { + (*z) = encodedBalanceRecordV5{} } - 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 "a": - (*z).Total, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Total") - return - } - case "b": - (*z).Decimals, bts, err = msgp.ReadUint32Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Decimals") - return - } - case "c": - (*z).DefaultFrozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "DefaultFrozen") - return - } - case "d": - (*z).UnitName, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "UnitName") - return - } - case "e": - (*z).AssetName, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "AssetName") - return - } - case "f": - (*z).URL, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "URL") - return - } - case "g": - bts, err = msgp.ReadExactBytes(bts, ((*z).MetadataHash)[:]) - if err != nil { - err = msgp.WrapError(err, "MetadataHash") - return - } - case "h": - bts, err = (*z).Manager.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Manager") - return - } - case "i": - bts, err = (*z).Reserve.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Reserve") - return - } - case "j": - bts, err = (*z).Freeze.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Freeze") - return - } - case "k": - bts, err = (*z).Clawback.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Clawback") - return - } - case "l": - (*z).Amount, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Amount") - return - } - case "m": - (*z).Frozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Frozen") - return - } - case "n": - (*z).SchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "SchemaNumUint") - return - } - case "o": - (*z).SchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "SchemaNumByteSlice") - return - } - case "p": - bts, err = (*z).KeyValue.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "KeyValue") - return - } - case "q": - var zb0007 int - zb0007, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "ApprovalProgram") - return - } - if zb0007 > config.MaxAvailableAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0007), uint64(config.MaxAvailableAppProgramLen)) - return - } - (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) - if err != nil { - err = msgp.WrapError(err, "ApprovalProgram") - return - } - case "r": - var zb0008 int - zb0008, err = msgp.ReadBytesBytesHeader(bts) - if err != nil { - err = msgp.WrapError(err, "ClearStateProgram") - return - } - if zb0008 > config.MaxAvailableAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0008), uint64(config.MaxAvailableAppProgramLen)) - return - } - (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) - if err != nil { - err = msgp.WrapError(err, "ClearStateProgram") - return - } - case "s": - bts, err = (*z).GlobalState.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "GlobalState") - return - } - case "t": - (*z).LocalStateSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "LocalStateSchemaNumUint") - return - } - case "u": - (*z).LocalStateSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "LocalStateSchemaNumByteSlice") - return - } - case "v": - (*z).GlobalStateSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "GlobalStateSchemaNumUint") - return - } - case "w": - (*z).GlobalStateSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "GlobalStateSchemaNumByteSlice") - return - } - case "x": - (*z).ExtraProgramPages, bts, err = msgp.ReadUint32Bytes(bts) + case "pk": + bts, err = (*z).Address.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "ExtraProgramPages") + err = msgp.WrapError(err, "Address") return } - case "y": - { - var zb0009 uint8 - zb0009, bts, err = msgp.ReadUint8Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "ResourceFlags") - return - } - (*z).ResourceFlags = resourceFlags(zb0009) - } - case "z": - (*z).UpdateRound, bts, err = msgp.ReadUint64Bytes(bts) + case "ad": + bts, err = (*z).AccountData.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "UpdateRound") + err = msgp.WrapError(err, "AccountData") return } default: @@ -3936,192 +1287,166 @@ func (z *resourcesData) UnmarshalMsg(bts []byte) (o []byte, err error) { return } -func (_ *resourcesData) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*resourcesData) +func (_ *encodedBalanceRecordV5) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*encodedBalanceRecordV5) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *resourcesData) Msgsize() (s int) { - s = 3 + 2 + msgp.Uint64Size + 2 + msgp.Uint32Size + 2 + msgp.BoolSize + 2 + msgp.StringPrefixSize + len((*z).UnitName) + 2 + msgp.StringPrefixSize + len((*z).AssetName) + 2 + msgp.StringPrefixSize + len((*z).URL) + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 2 + (*z).Manager.Msgsize() + 2 + (*z).Reserve.Msgsize() + 2 + (*z).Freeze.Msgsize() + 2 + (*z).Clawback.Msgsize() + 2 + msgp.Uint64Size + 2 + msgp.BoolSize + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + (*z).KeyValue.Msgsize() + 2 + msgp.BytesPrefixSize + len((*z).ApprovalProgram) + 2 + msgp.BytesPrefixSize + len((*z).ClearStateProgram) + 2 + (*z).GlobalState.Msgsize() + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint32Size + 2 + msgp.Uint8Size + 2 + msgp.Uint64Size +func (z *encodedBalanceRecordV5) Msgsize() (s int) { + s = 1 + 3 + (*z).Address.Msgsize() + 3 + (*z).AccountData.Msgsize() return } // MsgIsZero returns whether this is a zero value -func (z *resourcesData) 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()) && ((*z).Amount == 0) && ((*z).Frozen == false) && ((*z).SchemaNumUint == 0) && ((*z).SchemaNumByteSlice == 0) && ((*z).KeyValue.MsgIsZero()) && (len((*z).ApprovalProgram) == 0) && (len((*z).ClearStateProgram) == 0) && ((*z).GlobalState.MsgIsZero()) && ((*z).LocalStateSchemaNumUint == 0) && ((*z).LocalStateSchemaNumByteSlice == 0) && ((*z).GlobalStateSchemaNumUint == 0) && ((*z).GlobalStateSchemaNumByteSlice == 0) && ((*z).ExtraProgramPages == 0) && ((*z).ResourceFlags == 0) && ((*z).UpdateRound == 0) +func (z *encodedBalanceRecordV5) MsgIsZero() bool { + return ((*z).Address.MsgIsZero()) && ((*z).AccountData.MsgIsZero()) } // MarshalMsg implements msgp.Marshaler -func (z *txTailRound) MarshalMsg(b []byte) (o []byte) { +func (z *encodedBalanceRecordV6) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0004Len := uint32(4) - var zb0004Mask uint8 /* 5 bits */ - if (*z).Hdr.MsgIsZero() { - zb0004Len-- - zb0004Mask |= 0x2 + zb0003Len := uint32(4) + var zb0003Mask uint8 /* 5 bits */ + if (*z).Address.MsgIsZero() { + zb0003Len-- + zb0003Mask |= 0x2 } - if len((*z).TxnIDs) == 0 { - zb0004Len-- - zb0004Mask |= 0x4 + if (*z).AccountData.MsgIsZero() { + zb0003Len-- + zb0003Mask |= 0x4 } - if len((*z).Leases) == 0 { - zb0004Len-- - zb0004Mask |= 0x8 + if len((*z).Resources) == 0 { + zb0003Len-- + zb0003Mask |= 0x8 } - if len((*z).LastValid) == 0 { - zb0004Len-- - zb0004Mask |= 0x10 + if (*z).ExpectingMoreEntries == false { + zb0003Len-- + zb0003Mask |= 0x10 } - // variable map header, size zb0004Len - o = append(o, 0x80|uint8(zb0004Len)) - if zb0004Len != 0 { - if (zb0004Mask & 0x2) == 0 { // if not empty - // string "h" - o = append(o, 0xa1, 0x68) - o = (*z).Hdr.MarshalMsg(o) - } - if (zb0004Mask & 0x4) == 0 { // if not empty - // string "i" - o = append(o, 0xa1, 0x69) - if (*z).TxnIDs == nil { - o = msgp.AppendNil(o) - } else { - o = msgp.AppendArrayHeader(o, uint32(len((*z).TxnIDs))) - } - for zb0001 := range (*z).TxnIDs { - o = (*z).TxnIDs[zb0001].MarshalMsg(o) - } + // variable map header, size zb0003Len + o = append(o, 0x80|uint8(zb0003Len)) + if zb0003Len != 0 { + if (zb0003Mask & 0x2) == 0 { // if not empty + // string "a" + o = append(o, 0xa1, 0x61) + o = (*z).Address.MarshalMsg(o) } - if (zb0004Mask & 0x8) == 0 { // if not empty - // string "l" - o = append(o, 0xa1, 0x6c) - if (*z).Leases == nil { - o = msgp.AppendNil(o) - } else { - o = msgp.AppendArrayHeader(o, uint32(len((*z).Leases))) - } - for zb0003 := range (*z).Leases { - o = (*z).Leases[zb0003].MarshalMsg(o) - } + if (zb0003Mask & 0x4) == 0 { // if not empty + // string "b" + o = append(o, 0xa1, 0x62) + o = (*z).AccountData.MarshalMsg(o) } - if (zb0004Mask & 0x10) == 0 { // if not empty - // string "v" - o = append(o, 0xa1, 0x76) - if (*z).LastValid == nil { + if (zb0003Mask & 0x8) == 0 { // if not empty + // string "c" + o = append(o, 0xa1, 0x63) + if (*z).Resources == nil { o = msgp.AppendNil(o) } else { - o = msgp.AppendArrayHeader(o, uint32(len((*z).LastValid))) + o = msgp.AppendMapHeader(o, uint32(len((*z).Resources))) + } + zb0001_keys := make([]uint64, 0, len((*z).Resources)) + for zb0001 := range (*z).Resources { + zb0001_keys = append(zb0001_keys, zb0001) } - for zb0002 := range (*z).LastValid { - o = (*z).LastValid[zb0002].MarshalMsg(o) + sort.Sort(SortUint64(zb0001_keys)) + for _, zb0001 := range zb0001_keys { + zb0002 := (*z).Resources[zb0001] + _ = zb0002 + o = msgp.AppendUint64(o, zb0001) + o = zb0002.MarshalMsg(o) } } + if (zb0003Mask & 0x10) == 0 { // if not empty + // string "e" + o = append(o, 0xa1, 0x65) + o = msgp.AppendBool(o, (*z).ExpectingMoreEntries) + } } return } -func (_ *txTailRound) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*txTailRound) +func (_ *encodedBalanceRecordV6) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*encodedBalanceRecordV6) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *txTailRound) UnmarshalMsg(bts []byte) (o []byte, err error) { +func (z *encodedBalanceRecordV6) 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 zb0003 int + var zb0004 bool + zb0003, zb0004, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0003, zb0004, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0004 > 0 { - zb0004-- - var zb0006 int - var zb0007 bool - zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0003 > 0 { + zb0003-- + bts, err = (*z).Address.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TxnIDs") + err = msgp.WrapError(err, "struct-from-array", "Address") return } - if zb0007 { - (*z).TxnIDs = nil - } else if (*z).TxnIDs != nil && cap((*z).TxnIDs) >= zb0006 { - (*z).TxnIDs = ((*z).TxnIDs)[:zb0006] - } else { - (*z).TxnIDs = make([]transactions.Txid, zb0006) - } - for zb0001 := range (*z).TxnIDs { - bts, err = (*z).TxnIDs[zb0001].UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TxnIDs", zb0001) - return - } - } } - if zb0004 > 0 { - zb0004-- - var zb0008 int - var zb0009 bool - zb0008, zb0009, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0003 > 0 { + zb0003-- + bts, err = (*z).AccountData.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "LastValid") + err = msgp.WrapError(err, "struct-from-array", "AccountData") return } - if zb0009 { - (*z).LastValid = nil - } else if (*z).LastValid != nil && cap((*z).LastValid) >= zb0008 { - (*z).LastValid = ((*z).LastValid)[:zb0008] - } else { - (*z).LastValid = make([]basics.Round, zb0008) - } - for zb0002 := range (*z).LastValid { - bts, err = (*z).LastValid[zb0002].UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "LastValid", zb0002) - return - } - } } - if zb0004 > 0 { - zb0004-- - var zb0010 int - var zb0011 bool - zb0010, zb0011, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0003 > 0 { + zb0003-- + var zb0005 int + var zb0006 bool + zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Leases") + err = msgp.WrapError(err, "struct-from-array", "Resources") return } - if zb0011 { - (*z).Leases = nil - } else if (*z).Leases != nil && cap((*z).Leases) >= zb0010 { - (*z).Leases = ((*z).Leases)[:zb0010] - } else { - (*z).Leases = make([]txTailRoundLease, zb0010) + if zb0005 > resourcesPerCatchpointFileChunkBackwardCompatible { + err = msgp.ErrOverflow(uint64(zb0005), uint64(resourcesPerCatchpointFileChunkBackwardCompatible)) + err = msgp.WrapError(err, "struct-from-array", "Resources") + return } - for zb0003 := range (*z).Leases { - bts, err = (*z).Leases[zb0003].UnmarshalMsg(bts) + if zb0006 { + (*z).Resources = nil + } else if (*z).Resources == nil { + (*z).Resources = make(map[uint64]msgp.Raw, zb0005) + } + for zb0005 > 0 { + var zb0001 uint64 + var zb0002 msgp.Raw + zb0005-- + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Resources") + return + } + bts, err = zb0002.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Leases", zb0003) + err = msgp.WrapError(err, "struct-from-array", "Resources", zb0001) return } + (*z).Resources[zb0001] = zb0002 } } - if zb0004 > 0 { - zb0004-- - bts, err = (*z).Hdr.UnmarshalMsg(bts) + if zb0003 > 0 { + zb0003-- + (*z).ExpectingMoreEntries, bts, err = msgp.ReadBoolBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Hdr") + err = msgp.WrapError(err, "struct-from-array", "ExpectingMoreEntries") return } } - if zb0004 > 0 { - err = msgp.ErrTooManyArrayFields(zb0004) + if zb0003 > 0 { + err = msgp.ErrTooManyArrayFields(zb0003) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -4132,87 +1457,67 @@ func (z *txTailRound) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0005 { - (*z) = txTailRound{} + if zb0004 { + (*z) = encodedBalanceRecordV6{} } - for zb0004 > 0 { - zb0004-- + for zb0003 > 0 { + zb0003-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) return } switch string(field) { - case "i": - var zb0012 int - var zb0013 bool - zb0012, zb0013, bts, err = msgp.ReadArrayHeaderBytes(bts) + case "a": + bts, err = (*z).Address.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "TxnIDs") + err = msgp.WrapError(err, "Address") return } - if zb0013 { - (*z).TxnIDs = nil - } else if (*z).TxnIDs != nil && cap((*z).TxnIDs) >= zb0012 { - (*z).TxnIDs = ((*z).TxnIDs)[:zb0012] - } else { - (*z).TxnIDs = make([]transactions.Txid, zb0012) - } - for zb0001 := range (*z).TxnIDs { - bts, err = (*z).TxnIDs[zb0001].UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "TxnIDs", zb0001) - return - } + case "b": + bts, err = (*z).AccountData.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AccountData") + return } - case "v": - var zb0014 int - var zb0015 bool - zb0014, zb0015, bts, err = msgp.ReadArrayHeaderBytes(bts) + case "c": + var zb0007 int + var zb0008 bool + zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "LastValid") + err = msgp.WrapError(err, "Resources") return } - if zb0015 { - (*z).LastValid = nil - } else if (*z).LastValid != nil && cap((*z).LastValid) >= zb0014 { - (*z).LastValid = ((*z).LastValid)[:zb0014] - } else { - (*z).LastValid = make([]basics.Round, zb0014) + if zb0007 > resourcesPerCatchpointFileChunkBackwardCompatible { + err = msgp.ErrOverflow(uint64(zb0007), uint64(resourcesPerCatchpointFileChunkBackwardCompatible)) + err = msgp.WrapError(err, "Resources") + return + } + if zb0008 { + (*z).Resources = nil + } else if (*z).Resources == nil { + (*z).Resources = make(map[uint64]msgp.Raw, zb0007) } - for zb0002 := range (*z).LastValid { - bts, err = (*z).LastValid[zb0002].UnmarshalMsg(bts) + for zb0007 > 0 { + var zb0001 uint64 + var zb0002 msgp.Raw + zb0007-- + zb0001, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { - err = msgp.WrapError(err, "LastValid", zb0002) + err = msgp.WrapError(err, "Resources") return } - } - case "l": - var zb0016 int - var zb0017 bool - zb0016, zb0017, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Leases") - return - } - if zb0017 { - (*z).Leases = nil - } else if (*z).Leases != nil && cap((*z).Leases) >= zb0016 { - (*z).Leases = ((*z).Leases)[:zb0016] - } else { - (*z).Leases = make([]txTailRoundLease, zb0016) - } - for zb0003 := range (*z).Leases { - bts, err = (*z).Leases[zb0003].UnmarshalMsg(bts) + bts, err = zb0002.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "Leases", zb0003) + err = msgp.WrapError(err, "Resources", zb0001) return } + (*z).Resources[zb0001] = zb0002 } - case "h": - bts, err = (*z).Hdr.UnmarshalMsg(bts) + case "e": + (*z).ExpectingMoreEntries, bts, err = msgp.ReadBoolBytes(bts) if err != nil { - err = msgp.WrapError(err, "Hdr") + err = msgp.WrapError(err, "ExpectingMoreEntries") return } default: @@ -4228,118 +1533,117 @@ func (z *txTailRound) UnmarshalMsg(bts []byte) (o []byte, err error) { return } -func (_ *txTailRound) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*txTailRound) +func (_ *encodedBalanceRecordV6) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*encodedBalanceRecordV6) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *txTailRound) Msgsize() (s int) { - s = 1 + 2 + msgp.ArrayHeaderSize - for zb0001 := range (*z).TxnIDs { - s += (*z).TxnIDs[zb0001].Msgsize() - } - s += 2 + msgp.ArrayHeaderSize - for zb0002 := range (*z).LastValid { - s += (*z).LastValid[zb0002].Msgsize() - } - s += 2 + msgp.ArrayHeaderSize - for zb0003 := range (*z).Leases { - s += (*z).Leases[zb0003].Msgsize() +func (z *encodedBalanceRecordV6) Msgsize() (s int) { + s = 1 + 2 + (*z).Address.Msgsize() + 2 + (*z).AccountData.Msgsize() + 2 + msgp.MapHeaderSize + if (*z).Resources != nil { + for zb0001, zb0002 := range (*z).Resources { + _ = zb0001 + _ = zb0002 + s += 0 + msgp.Uint64Size + zb0002.Msgsize() + } } - s += 2 + (*z).Hdr.Msgsize() + s += 2 + msgp.BoolSize return } // MsgIsZero returns whether this is a zero value -func (z *txTailRound) MsgIsZero() bool { - return (len((*z).TxnIDs) == 0) && (len((*z).LastValid) == 0) && (len((*z).Leases) == 0) && ((*z).Hdr.MsgIsZero()) +func (z *encodedBalanceRecordV6) MsgIsZero() bool { + return ((*z).Address.MsgIsZero()) && ((*z).AccountData.MsgIsZero()) && (len((*z).Resources) == 0) && ((*z).ExpectingMoreEntries == false) } // MarshalMsg implements msgp.Marshaler -func (z *txTailRoundLease) MarshalMsg(b []byte) (o []byte) { +func (z *encodedKVRecordV6) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0002Len := uint32(3) - var zb0002Mask uint8 /* 4 bits */ - if (*z).TxnIdx == 0 { - zb0002Len-- - zb0002Mask |= 0x1 - } - if (*z).Lease == ([32]byte{}) { - zb0002Len-- - zb0002Mask |= 0x4 + zb0001Len := uint32(2) + var zb0001Mask uint8 /* 3 bits */ + if len((*z).Key) == 0 { + zb0001Len-- + zb0001Mask |= 0x2 } - if (*z).Sender.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x8 + if len((*z).Value) == 0 { + zb0001Len-- + zb0001Mask |= 0x4 } - // variable map header, size zb0002Len - o = append(o, 0x80|uint8(zb0002Len)) - if zb0002Len != 0 { - if (zb0002Mask & 0x1) == 0 { // if not empty - // string "TxnIdx" - o = append(o, 0xa6, 0x54, 0x78, 0x6e, 0x49, 0x64, 0x78) - o = msgp.AppendUint64(o, (*z).TxnIdx) - } - if (zb0002Mask & 0x4) == 0 { // if not empty - // string "l" - o = append(o, 0xa1, 0x6c) - o = msgp.AppendBytes(o, ((*z).Lease)[:]) + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "k" + o = append(o, 0xa1, 0x6b) + o = msgp.AppendBytes(o, (*z).Key) } - if (zb0002Mask & 0x8) == 0 { // if not empty - // string "s" - o = append(o, 0xa1, 0x73) - o = (*z).Sender.MarshalMsg(o) + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "v" + o = append(o, 0xa1, 0x76) + o = msgp.AppendBytes(o, (*z).Value) } } return } -func (_ *txTailRoundLease) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*txTailRoundLease) +func (_ *encodedKVRecordV6) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*encodedKVRecordV6) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *txTailRoundLease) UnmarshalMsg(bts []byte) (o []byte, err error) { +func (z *encodedKVRecordV6) 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-- - bts, err = (*z).Sender.UnmarshalMsg(bts) + if zb0001 > 0 { + zb0001-- + var zb0003 int + zb0003, err = msgp.ReadBytesBytesHeader(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Sender") + err = msgp.WrapError(err, "struct-from-array", "Key") return } - } - if zb0002 > 0 { - zb0002-- - bts, err = msgp.ReadExactBytes(bts, ((*z).Lease)[:]) + if zb0003 > encodedKVRecordV6MaxKeyLength { + err = msgp.ErrOverflow(uint64(zb0003), uint64(encodedKVRecordV6MaxKeyLength)) + return + } + (*z).Key, bts, err = msgp.ReadBytesBytes(bts, (*z).Key) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Lease") + err = msgp.WrapError(err, "struct-from-array", "Key") return } } - if zb0002 > 0 { - zb0002-- - (*z).TxnIdx, bts, err = msgp.ReadUint64Bytes(bts) + if zb0001 > 0 { + zb0001-- + var zb0004 int + zb0004, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Value") + return + } + if zb0004 > encodedKVRecordV6MaxValueLength { + err = msgp.ErrOverflow(uint64(zb0004), uint64(encodedKVRecordV6MaxValueLength)) + return + } + (*z).Value, bts, err = msgp.ReadBytesBytes(bts, (*z).Value) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TxnIdx") + err = msgp.WrapError(err, "struct-from-array", "Value") return } } - if zb0002 > 0 { - err = msgp.ErrTooManyArrayFields(zb0002) + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -4350,33 +1654,47 @@ func (z *txTailRoundLease) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0003 { - (*z) = txTailRoundLease{} + if zb0002 { + (*z) = encodedKVRecordV6{} } - 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 "s": - bts, err = (*z).Sender.UnmarshalMsg(bts) + case "k": + var zb0005 int + zb0005, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "Key") + return + } + if zb0005 > encodedKVRecordV6MaxKeyLength { + err = msgp.ErrOverflow(uint64(zb0005), uint64(encodedKVRecordV6MaxKeyLength)) + return + } + (*z).Key, bts, err = msgp.ReadBytesBytes(bts, (*z).Key) if err != nil { - err = msgp.WrapError(err, "Sender") + err = msgp.WrapError(err, "Key") return } - case "l": - bts, err = msgp.ReadExactBytes(bts, ((*z).Lease)[:]) + case "v": + var zb0006 int + zb0006, err = msgp.ReadBytesBytesHeader(bts) if err != nil { - err = msgp.WrapError(err, "Lease") + err = msgp.WrapError(err, "Value") + return + } + if zb0006 > encodedKVRecordV6MaxValueLength { + err = msgp.ErrOverflow(uint64(zb0006), uint64(encodedKVRecordV6MaxValueLength)) return } - case "TxnIdx": - (*z).TxnIdx, bts, err = msgp.ReadUint64Bytes(bts) + (*z).Value, bts, err = msgp.ReadBytesBytes(bts, (*z).Value) if err != nil { - err = msgp.WrapError(err, "TxnIdx") + err = msgp.WrapError(err, "Value") return } default: @@ -4392,18 +1710,18 @@ func (z *txTailRoundLease) UnmarshalMsg(bts []byte) (o []byte, err error) { return } -func (_ *txTailRoundLease) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*txTailRoundLease) +func (_ *encodedKVRecordV6) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*encodedKVRecordV6) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *txTailRoundLease) Msgsize() (s int) { - s = 1 + 2 + (*z).Sender.Msgsize() + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 7 + msgp.Uint64Size +func (z *encodedKVRecordV6) Msgsize() (s int) { + s = 1 + 2 + msgp.BytesPrefixSize + len((*z).Key) + 2 + msgp.BytesPrefixSize + len((*z).Value) return } // MsgIsZero returns whether this is a zero value -func (z *txTailRoundLease) MsgIsZero() bool { - return ((*z).Sender.MsgIsZero()) && ((*z).Lease == ([32]byte{})) && ((*z).TxnIdx == 0) +func (z *encodedKVRecordV6) MsgIsZero() bool { + return (len((*z).Key) == 0) && (len((*z).Value) == 0) } diff --git a/ledger/msgp_gen_test.go b/ledger/msgp_gen_test.go index 2481023983..e7739ad972 100644 --- a/ledger/msgp_gen_test.go +++ b/ledger/msgp_gen_test.go @@ -74,186 +74,6 @@ func BenchmarkUnmarshalCatchpointFileHeader(b *testing.B) { } } -func TestMarshalUnmarshalbaseAccountData(t *testing.T) { - partitiontest.PartitionTest(t) - v := baseAccountData{} - bts := v.MarshalMsg(nil) - 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 TestRandomizedEncodingbaseAccountData(t *testing.T) { - protocol.RunEncodingTest(t, &baseAccountData{}) -} - -func BenchmarkMarshalMsgbaseAccountData(b *testing.B) { - v := baseAccountData{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - v.MarshalMsg(nil) - } -} - -func BenchmarkAppendMsgbaseAccountData(b *testing.B) { - v := baseAccountData{} - 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 BenchmarkUnmarshalbaseAccountData(b *testing.B) { - v := baseAccountData{} - 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 TestMarshalUnmarshalbaseOnlineAccountData(t *testing.T) { - partitiontest.PartitionTest(t) - v := baseOnlineAccountData{} - bts := v.MarshalMsg(nil) - 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 TestRandomizedEncodingbaseOnlineAccountData(t *testing.T) { - protocol.RunEncodingTest(t, &baseOnlineAccountData{}) -} - -func BenchmarkMarshalMsgbaseOnlineAccountData(b *testing.B) { - v := baseOnlineAccountData{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - v.MarshalMsg(nil) - } -} - -func BenchmarkAppendMsgbaseOnlineAccountData(b *testing.B) { - v := baseOnlineAccountData{} - 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 BenchmarkUnmarshalbaseOnlineAccountData(b *testing.B) { - v := baseOnlineAccountData{} - 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 TestMarshalUnmarshalbaseVotingData(t *testing.T) { - partitiontest.PartitionTest(t) - v := baseVotingData{} - bts := v.MarshalMsg(nil) - 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 TestRandomizedEncodingbaseVotingData(t *testing.T) { - protocol.RunEncodingTest(t, &baseVotingData{}) -} - -func BenchmarkMarshalMsgbaseVotingData(b *testing.B) { - v := baseVotingData{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - v.MarshalMsg(nil) - } -} - -func BenchmarkAppendMsgbaseVotingData(b *testing.B) { - v := baseVotingData{} - 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 BenchmarkUnmarshalbaseVotingData(b *testing.B) { - v := baseVotingData{} - 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 TestMarshalUnmarshalcatchpointFileBalancesChunkV5(t *testing.T) { partitiontest.PartitionTest(t) v := catchpointFileBalancesChunkV5{} @@ -374,66 +194,6 @@ func BenchmarkUnmarshalcatchpointFileChunkV6(b *testing.B) { } } -func TestMarshalUnmarshalcatchpointFirstStageInfo(t *testing.T) { - partitiontest.PartitionTest(t) - v := catchpointFirstStageInfo{} - bts := v.MarshalMsg(nil) - 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 TestRandomizedEncodingcatchpointFirstStageInfo(t *testing.T) { - protocol.RunEncodingTest(t, &catchpointFirstStageInfo{}) -} - -func BenchmarkMarshalMsgcatchpointFirstStageInfo(b *testing.B) { - v := catchpointFirstStageInfo{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - v.MarshalMsg(nil) - } -} - -func BenchmarkAppendMsgcatchpointFirstStageInfo(b *testing.B) { - v := catchpointFirstStageInfo{} - 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 BenchmarkUnmarshalcatchpointFirstStageInfo(b *testing.B) { - v := catchpointFirstStageInfo{} - 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 TestMarshalUnmarshalencodedBalanceRecordV5(t *testing.T) { partitiontest.PartitionTest(t) v := encodedBalanceRecordV5{} @@ -613,183 +373,3 @@ func BenchmarkUnmarshalencodedKVRecordV6(b *testing.B) { } } } - -func TestMarshalUnmarshalresourcesData(t *testing.T) { - partitiontest.PartitionTest(t) - v := resourcesData{} - bts := v.MarshalMsg(nil) - 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 TestRandomizedEncodingresourcesData(t *testing.T) { - protocol.RunEncodingTest(t, &resourcesData{}) -} - -func BenchmarkMarshalMsgresourcesData(b *testing.B) { - v := resourcesData{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - v.MarshalMsg(nil) - } -} - -func BenchmarkAppendMsgresourcesData(b *testing.B) { - v := resourcesData{} - 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 BenchmarkUnmarshalresourcesData(b *testing.B) { - v := resourcesData{} - 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 TestMarshalUnmarshaltxTailRound(t *testing.T) { - partitiontest.PartitionTest(t) - v := txTailRound{} - bts := v.MarshalMsg(nil) - 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 TestRandomizedEncodingtxTailRound(t *testing.T) { - protocol.RunEncodingTest(t, &txTailRound{}) -} - -func BenchmarkMarshalMsgtxTailRound(b *testing.B) { - v := txTailRound{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - v.MarshalMsg(nil) - } -} - -func BenchmarkAppendMsgtxTailRound(b *testing.B) { - v := txTailRound{} - 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 BenchmarkUnmarshaltxTailRound(b *testing.B) { - v := txTailRound{} - 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 TestMarshalUnmarshaltxTailRoundLease(t *testing.T) { - partitiontest.PartitionTest(t) - v := txTailRoundLease{} - bts := v.MarshalMsg(nil) - 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 TestRandomizedEncodingtxTailRoundLease(t *testing.T) { - protocol.RunEncodingTest(t, &txTailRoundLease{}) -} - -func BenchmarkMarshalMsgtxTailRoundLease(b *testing.B) { - v := txTailRoundLease{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - v.MarshalMsg(nil) - } -} - -func BenchmarkAppendMsgtxTailRoundLease(b *testing.B) { - v := txTailRoundLease{} - 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 BenchmarkUnmarshaltxTailRoundLease(b *testing.B) { - v := txTailRoundLease{} - 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/ledger/onlineaccountscache.go b/ledger/onlineaccountscache.go index 2d43994094..4d6c976027 100644 --- a/ledger/onlineaccountscache.go +++ b/ledger/onlineaccountscache.go @@ -20,6 +20,7 @@ import ( "container/list" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" ) // Worst case memory usage = 2500 * 320 * 150B = 120MB @@ -34,17 +35,17 @@ type onlineAccountsCache struct { // init initializes the onlineAccountsCache for use. // thread locking semantics : write lock -func (o *onlineAccountsCache) init(accts []persistedOnlineAccountData, maxCacheSize int) { +func (o *onlineAccountsCache) init(accts []store.PersistedOnlineAccountData, maxCacheSize int) { o.accounts = make(map[basics.Address]*list.List) o.maxCacheSize = maxCacheSize for _, acct := range accts { // if cache full, stop writing cachedAcct := cachedOnlineAccount{ - baseOnlineAccountData: acct.accountData, - updRound: acct.updRound, + BaseOnlineAccountData: acct.AccountData, + updRound: acct.UpdRound, } - if !o.writeFront(acct.addr, cachedAcct) { + if !o.writeFront(acct.Addr, cachedAcct) { break } } diff --git a/ledger/onlineaccountscache_test.go b/ledger/onlineaccountscache_test.go index 42a94bb42c..23010b35d8 100644 --- a/ledger/onlineaccountscache_test.go +++ b/ledger/onlineaccountscache_test.go @@ -24,6 +24,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/ledger/store" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" @@ -44,7 +45,7 @@ func TestOnlineAccountsCacheBasic(t *testing.T) { for i := 0; i < roundsNum; i++ { acct := cachedOnlineAccount{ updRound: basics.Round(i), - baseOnlineAccountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}, baseVotingData: baseVotingData{VoteLastValid: 1000}}, + BaseOnlineAccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}, BaseVotingData: store.BaseVotingData{VoteLastValid: 1000}}, } written := oac.writeFront(addr, acct) require.True(t, written) @@ -61,7 +62,7 @@ func TestOnlineAccountsCacheBasic(t *testing.T) { for i := proto.MaxBalLookback; i < uint64(roundsNum)+proto.MaxBalLookback; i++ { acct := cachedOnlineAccount{ updRound: basics.Round(i), - baseOnlineAccountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: i}, baseVotingData: baseVotingData{VoteLastValid: 1000}}, + BaseOnlineAccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: i}, BaseVotingData: store.BaseVotingData{VoteLastValid: 1000}}, } written := oac.writeFront(addr, acct) require.True(t, written) @@ -88,7 +89,7 @@ func TestOnlineAccountsCacheBasic(t *testing.T) { // attempt to insert a value with the updRound less than latest, expect it to have ignored acct = cachedOnlineAccount{ updRound: basics.Round(uint64(roundsNum) + proto.MaxBalLookback - 1), - baseOnlineAccountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: 100}, baseVotingData: baseVotingData{VoteLastValid: 1000}}, + BaseOnlineAccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: 100}, BaseVotingData: store.BaseVotingData{VoteLastValid: 1000}}, } written := oac.writeFront(addr, acct) require.False(t, written) @@ -109,13 +110,13 @@ func TestOnlineAccountsCachePruneOffline(t *testing.T) { for i := 0; i < roundsNum; i++ { acct := cachedOnlineAccount{ updRound: basics.Round(i), - baseOnlineAccountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}, baseVotingData: baseVotingData{VoteLastValid: 1000}}, + BaseOnlineAccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}, BaseVotingData: store.BaseVotingData{VoteLastValid: 1000}}, } oac.writeFront(addr, acct) } acct := cachedOnlineAccount{ updRound: basics.Round(roundsNum), - baseOnlineAccountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(roundsNum)}}, + BaseOnlineAccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(roundsNum)}}, } oac.writeFront(addr, acct) @@ -139,7 +140,7 @@ func TestOnlineAccountsCacheMaxEntries(t *testing.T) { lastAddr = ledgertesting.RandomAddress() acct := cachedOnlineAccount{ updRound: basics.Round(i), - baseOnlineAccountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}, baseVotingData: baseVotingData{VoteLastValid: 1000}}, + BaseOnlineAccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(i)}, BaseVotingData: store.BaseVotingData{VoteLastValid: 1000}}, } written := oac.writeFront(lastAddr, acct) require.True(t, written) @@ -147,7 +148,7 @@ func TestOnlineAccountsCacheMaxEntries(t *testing.T) { acct := cachedOnlineAccount{ updRound: basics.Round(100), - baseOnlineAccountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(100)}, baseVotingData: baseVotingData{VoteLastValid: 1000}}, + BaseOnlineAccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(100)}, BaseVotingData: store.BaseVotingData{VoteLastValid: 1000}}, } written := oac.writeFront(ledgertesting.RandomAddress(), acct) require.False(t, written) @@ -158,7 +159,7 @@ func TestOnlineAccountsCacheMaxEntries(t *testing.T) { // set one to be expired acct = cachedOnlineAccount{ updRound: basics.Round(maxCacheSize), - baseOnlineAccountData: baseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(100)}, baseVotingData: baseVotingData{}}, + BaseOnlineAccountData: store.BaseOnlineAccountData{MicroAlgos: basics.MicroAlgos{Raw: uint64(100)}, BaseVotingData: store.BaseVotingData{}}, } written = oac.writeFront(lastAddr, acct) require.True(t, written) diff --git a/ledger/persistedaccts_list.go b/ledger/persistedaccts_list.go index de8c8380fa..c7c884a860 100644 --- a/ledger/persistedaccts_list.go +++ b/ledger/persistedaccts_list.go @@ -16,6 +16,8 @@ package ledger +import "github.com/algorand/go-algorand/ledger/store" + // persistedAccountDataList represents a doubly linked list. // must initiate with newPersistedAccountList. type persistedAccountDataList struct { @@ -31,7 +33,7 @@ type persistedAccountDataListNode struct { // element (l.Front()). next, prev *persistedAccountDataListNode - Value *persistedAccountData + Value *store.PersistedAccountData } func newPersistedAccountList() *persistedAccountDataList { @@ -99,7 +101,7 @@ func (l *persistedAccountDataList) remove(e *persistedAccountDataListNode) { } // pushFront inserts a new element e with value v at the front of list l and returns e. -func (l *persistedAccountDataList) pushFront(v *persistedAccountData) *persistedAccountDataListNode { +func (l *persistedAccountDataList) pushFront(v *store.PersistedAccountData) *persistedAccountDataListNode { newNode := l.getNewNode() newNode.Value = v return l.insertValue(newNode, &l.root) diff --git a/ledger/persistedaccts_list_test.go b/ledger/persistedaccts_list_test.go index 8e8b9531b6..9c7b0d69ce 100644 --- a/ledger/persistedaccts_list_test.go +++ b/ledger/persistedaccts_list_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -77,9 +78,9 @@ func checkListLen(t *testing.T, l dataList, len int) bool { func TestRemoveFromListAD(t *testing.T) { partitiontest.PartitionTest(t) l := newPersistedAccountList() - e1 := l.pushFront(&persistedAccountData{addr: basics.Address{1}}) - e2 := l.pushFront(&persistedAccountData{addr: basics.Address{2}}) - e3 := l.pushFront(&persistedAccountData{addr: basics.Address{3}}) + e1 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{1}}) + e2 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{2}}) + e3 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{3}}) checkListPointersAD(t, l, []*persistedAccountDataListNode{e3, e2, e1}) l.remove(e2) @@ -97,7 +98,7 @@ func TestAddingNewNodeWithAllocatedFreeListAD(t *testing.T) { return } // test elements - e1 := l.pushFront(&persistedAccountData{addr: basics.Address{1}}) + e1 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{1}}) checkListPointersAD(t, l, []*persistedAccountDataListNode{e1}) if countListSize(l.freeList) != 9 { @@ -167,11 +168,11 @@ func TestMultielementListPositioningAD(t *testing.T) { l := newPersistedAccountList() checkListPointersAD(t, l, []*persistedAccountDataListNode{}) // test elements - e2 := l.pushFront(&persistedAccountData{addr: basics.Address{2}}) - e1 := l.pushFront(&persistedAccountData{addr: basics.Address{1}}) - e3 := l.pushFront(&persistedAccountData{addr: basics.Address{3}}) - e4 := l.pushFront(&persistedAccountData{addr: basics.Address{4}}) - e5 := l.pushFront(&persistedAccountData{addr: basics.Address{5}}) + e2 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{2}}) + e1 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{1}}) + e3 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{3}}) + e4 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{4}}) + e5 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{5}}) checkListPointersAD(t, l, []*persistedAccountDataListNode{e5, e4, e3, e1, e2}) @@ -199,7 +200,7 @@ func TestMultielementListPositioningAD(t *testing.T) { l.moveToFront(e1) // no movement checkListPointersAD(t, l, []*persistedAccountDataListNode{e1, e3, e4}) - e2 = l.pushFront(&persistedAccountData{addr: basics.Address{2}}) + e2 = l.pushFront(&store.PersistedAccountData{Addr: basics.Address{2}}) checkListPointersAD(t, l, []*persistedAccountDataListNode{e2, e1, e3, e4}) l.remove(e3) // removing from middle @@ -225,7 +226,7 @@ func TestSingleElementListPositioningAD(t *testing.T) { partitiontest.PartitionTest(t) l := newPersistedAccountList() checkListPointersAD(t, l, []*persistedAccountDataListNode{}) - e := l.pushFront(&persistedAccountData{addr: basics.Address{1}}) + e := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{1}}) checkListPointersAD(t, l, []*persistedAccountDataListNode{e}) l.moveToFront(e) checkListPointersAD(t, l, []*persistedAccountDataListNode{e}) @@ -236,8 +237,8 @@ func TestSingleElementListPositioningAD(t *testing.T) { func TestRemovedNodeShouldBeMovedToFreeListAD(t *testing.T) { partitiontest.PartitionTest(t) l := newPersistedAccountList() - e1 := l.pushFront(&persistedAccountData{addr: basics.Address{1}}) - e2 := l.pushFront(&persistedAccountData{addr: basics.Address{2}}) + e1 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{1}}) + e2 := l.pushFront(&store.PersistedAccountData{Addr: basics.Address{2}}) checkListPointersAD(t, l, []*persistedAccountDataListNode{e2, e1}) diff --git a/ledger/persistedonlineaccts_list.go b/ledger/persistedonlineaccts_list.go index a5c37ca8fb..e5775498d1 100644 --- a/ledger/persistedonlineaccts_list.go +++ b/ledger/persistedonlineaccts_list.go @@ -16,6 +16,8 @@ package ledger +import "github.com/algorand/go-algorand/ledger/store" + // persistedOnlineAccountDataList represents a doubly linked list. // must initiate with newPersistedAccountList. type persistedOnlineAccountDataList struct { @@ -31,7 +33,7 @@ type persistedOnlineAccountDataListNode struct { // element (l.Front()). next, prev *persistedOnlineAccountDataListNode - Value *persistedOnlineAccountData + Value *store.PersistedOnlineAccountData } func newPersistedOnlineAccountList() *persistedOnlineAccountDataList { @@ -99,7 +101,7 @@ func (l *persistedOnlineAccountDataList) remove(e *persistedOnlineAccountDataLis } // pushFront inserts a new element e with value v at the front of list l and returns e. -func (l *persistedOnlineAccountDataList) pushFront(v *persistedOnlineAccountData) *persistedOnlineAccountDataListNode { +func (l *persistedOnlineAccountDataList) pushFront(v *store.PersistedOnlineAccountData) *persistedOnlineAccountDataListNode { newNode := l.getNewNode() newNode.Value = v return l.insertValue(newNode, &l.root) diff --git a/ledger/persistedonlineaccts_list_test.go b/ledger/persistedonlineaccts_list_test.go index 7bc0ad3733..7e4a10721b 100644 --- a/ledger/persistedonlineaccts_list_test.go +++ b/ledger/persistedonlineaccts_list_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -45,9 +46,9 @@ func (l *persistedOnlineAccountDataListNode) getPrev() dataListNode { func TestRemoveFromListOAD(t *testing.T) { partitiontest.PartitionTest(t) l := newPersistedOnlineAccountList() - e1 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{1}}) - e2 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{2}}) - e3 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{3}}) + e1 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{1}}) + e2 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{2}}) + e3 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{3}}) checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{e3, e2, e1}) l.remove(e2) @@ -65,7 +66,7 @@ func TestAddingNewNodeWithAllocatedFreeListOAD(t *testing.T) { return } // test elements - e1 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{1}}) + e1 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{1}}) checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{e1}) if countListSize(l.freeList) != 9 { @@ -89,11 +90,11 @@ func TestMultielementListPositioningOAD(t *testing.T) { l := newPersistedOnlineAccountList() checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{}) // test elements - e2 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{2}}) - e1 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{1}}) - e3 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{3}}) - e4 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{4}}) - e5 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{5}}) + e2 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{2}}) + e1 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{1}}) + e3 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{3}}) + e4 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{4}}) + e5 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{5}}) checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{e5, e4, e3, e1, e2}) @@ -121,7 +122,7 @@ func TestMultielementListPositioningOAD(t *testing.T) { l.moveToFront(e1) // no movement checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{e1, e3, e4}) - e2 = l.pushFront(&persistedOnlineAccountData{addr: basics.Address{2}}) + e2 = l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{2}}) checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{e2, e1, e3, e4}) l.remove(e3) // removing from middle @@ -147,7 +148,7 @@ func TestSingleElementListPositioningOD(t *testing.T) { partitiontest.PartitionTest(t) l := newPersistedOnlineAccountList() checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{}) - e := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{1}}) + e := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{1}}) checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{e}) l.moveToFront(e) checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{e}) @@ -158,8 +159,8 @@ func TestSingleElementListPositioningOD(t *testing.T) { func TestRemovedNodeShouldBeMovedToFreeListOAD(t *testing.T) { partitiontest.PartitionTest(t) l := newPersistedOnlineAccountList() - e1 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{1}}) - e2 := l.pushFront(&persistedOnlineAccountData{addr: basics.Address{2}}) + e1 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{1}}) + e2 := l.pushFront(&store.PersistedOnlineAccountData{Addr: basics.Address{2}}) checkListPointersOAD(t, l, []*persistedOnlineAccountDataListNode{e2, e1}) diff --git a/ledger/store/accountsV2.go b/ledger/store/accountsV2.go new file mode 100644 index 0000000000..649e4111bb --- /dev/null +++ b/ledger/store/accountsV2.go @@ -0,0 +1,738 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "bytes" + "context" + "database/sql" + "fmt" + "strings" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" +) + +type accountsV2Reader struct { + q db.Queryable + preparedStatements map[string]*sql.Stmt +} + +type accountsV2Writer struct { + e db.Executable +} + +type accountsV2ReaderWriter struct { + accountsV2Reader + accountsV2Writer +} + +// NewAccountsSQLReaderWriter creates a Catchpoint SQL reader+writer +func NewAccountsSQLReaderWriter(e db.Executable) *accountsV2ReaderWriter { + return &accountsV2ReaderWriter{ + accountsV2Reader{q: e, preparedStatements: make(map[string]*sql.Stmt)}, + accountsV2Writer{e: e}, + } +} + +func (r *accountsV2Reader) getOrPrepare(queryString string) (stmt *sql.Stmt, err error) { + // fetch statement (use the query as the key) + if stmt, ok := r.preparedStatements[queryString]; ok { + return stmt, nil + } + // we do not have it, prepare it + stmt, err = r.q.Prepare(queryString) + if err != nil { + return + } + // cache the statement + r.preparedStatements[queryString] = stmt + + return stmt, nil +} + +// AccountsTotals returns account totals +func (r *accountsV2Reader) AccountsTotals(ctx context.Context, catchpointStaging bool) (totals ledgercore.AccountTotals, err error) { + id := "" + if catchpointStaging { + id = "catchpointStaging" + } + row := r.q.QueryRowContext(ctx, "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) + + return +} + +// AccountsRound returns the tracker balances round number +func (r *accountsV2Reader) AccountsRound() (rnd basics.Round, err error) { + err = r.q.QueryRow("SELECT rnd FROM acctrounds WHERE id='acctbase'").Scan(&rnd) + if err != nil { + return + } + return +} + +// AccountsHashRound returns the round of the hash tree +// if the hash of the tree doesn't exists, it returns zero. +func (r *accountsV2Reader) AccountsHashRound(ctx context.Context) (hashrnd basics.Round, err error) { + err = r.q.QueryRowContext(ctx, "SELECT rnd FROM acctrounds WHERE id='hashbase'").Scan(&hashrnd) + if err == sql.ErrNoRows { + hashrnd = basics.Round(0) + err = nil + } + return +} + +// AccountsOnlineTop returns the top n online accounts starting at position offset +// (that is, the top offset'th account through the top offset+n-1'th account). +// +// The accounts are sorted by their normalized balance and address. The normalized +// balance has to do with the reward parts of online account balances. See the +// normalization procedure in AccountData.NormalizedOnlineBalance(). +// +// Note that this does not check if the accounts have a vote key valid for any +// particular round (past, present, or future). +func (r *accountsV2Reader) AccountsOnlineTop(rnd basics.Round, offset uint64, n uint64, proto config.ConsensusParams) (map[basics.Address]*ledgercore.OnlineAccount, error) { + // onlineaccounts has historical data ordered by updround for both online and offline accounts. + // This means some account A might have norm balance != 0 at round N and norm balance == 0 at some round K > N. + // For online top query one needs to find entries not fresher than X with norm balance != 0. + // To do that the query groups row by address and takes the latest updround, and then filters out rows with zero nor balance. + rows, err := r.q.Query(`SELECT address, normalizedonlinebalance, data, max(updround) FROM onlineaccounts +WHERE updround <= ? +GROUP BY address HAVING normalizedonlinebalance > 0 +ORDER BY normalizedonlinebalance DESC, address DESC LIMIT ? OFFSET ?`, rnd, n, offset) + + if err != nil { + return nil, err + } + defer rows.Close() + + res := make(map[basics.Address]*ledgercore.OnlineAccount, n) + for rows.Next() { + var addrbuf []byte + var buf []byte + var normBal sql.NullInt64 + var updround sql.NullInt64 + err = rows.Scan(&addrbuf, &normBal, &buf, &updround) + if err != nil { + return nil, err + } + + var data BaseOnlineAccountData + err = protocol.Decode(buf, &data) + if err != nil { + return nil, err + } + + var addr basics.Address + if len(addrbuf) != len(addr) { + err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) + return nil, err + } + + if !normBal.Valid { + return nil, fmt.Errorf("non valid norm balance for online account %s", addr.String()) + } + + copy(addr[:], addrbuf) + // TODO: figure out protocol to use for rewards + // The original implementation uses current proto to recalculate norm balance + // In the same time, in accountsNewRound genesis protocol is used to fill norm balance value + // In order to be consistent with the original implementation recalculate the balance with current proto + normBalance := basics.NormalizedOnlineAccountBalance(basics.Online, data.RewardsBase, data.MicroAlgos, proto) + oa := data.GetOnlineAccount(addr, normBalance) + res[addr] = &oa + } + + return res, rows.Err() +} + +// OnlineAccountsAll returns all online accounts +func (r *accountsV2Reader) OnlineAccountsAll(maxAccounts uint64) ([]PersistedOnlineAccountData, error) { + rows, err := r.q.Query("SELECT rowid, address, updround, data FROM onlineaccounts ORDER BY address, updround ASC") + if err != nil { + return nil, err + } + defer rows.Close() + + result := make([]PersistedOnlineAccountData, 0, maxAccounts) + var numAccounts uint64 + seenAddr := make([]byte, len(basics.Address{})) + for rows.Next() { + var addrbuf []byte + var buf []byte + data := PersistedOnlineAccountData{} + err := rows.Scan(&data.Rowid, &addrbuf, &data.UpdRound, &buf) + if err != nil { + return nil, err + } + if len(addrbuf) != len(data.Addr) { + err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(data.Addr)) + return nil, err + } + if maxAccounts > 0 { + if !bytes.Equal(seenAddr, addrbuf) { + numAccounts++ + if numAccounts > maxAccounts { + break + } + copy(seenAddr, addrbuf) + } + } + copy(data.Addr[:], addrbuf) + err = protocol.Decode(buf, &data.AccountData) + if err != nil { + return nil, err + } + result = append(result, data) + } + return result, nil +} + +// TotalAccounts returns the total number of accounts +func (r *accountsV2Reader) TotalAccounts(ctx context.Context) (total uint64, err error) { + err = r.q.QueryRowContext(ctx, "SELECT count(1) FROM accountbase").Scan(&total) + if err == sql.ErrNoRows { + total = 0 + err = nil + return + } + return +} + +// TotalKVs returns the total number of kv items +func (r *accountsV2Reader) TotalKVs(ctx context.Context) (total uint64, err error) { + err = r.q.QueryRowContext(ctx, "SELECT count(1) FROM kvstore").Scan(&total) + if err == sql.ErrNoRows { + total = 0 + err = nil + return + } + return +} + +// LoadTxTail returns the tx tails +func (r *accountsV2Reader) LoadTxTail(ctx context.Context, dbRound basics.Round) (roundData []*TxTailRound, roundHash []crypto.Digest, baseRound basics.Round, err error) { + rows, err := r.q.QueryContext(ctx, "SELECT rnd, data FROM txtail ORDER BY rnd DESC") + if err != nil { + return nil, nil, 0, err + } + defer rows.Close() + + expectedRound := dbRound + for rows.Next() { + var round basics.Round + var data []byte + err = rows.Scan(&round, &data) + if err != nil { + return nil, nil, 0, err + } + if round != expectedRound { + return nil, nil, 0, fmt.Errorf("txtail table contain unexpected round %d; round %d was expected", round, expectedRound) + } + tail := &TxTailRound{} + err = protocol.Decode(data, tail) + if err != nil { + return nil, nil, 0, err + } + roundData = append(roundData, tail) + roundHash = append(roundHash, crypto.Hash(data)) + expectedRound-- + } + // reverse the array ordering in-place so that it would be incremental order. + for i := 0; i < len(roundData)/2; i++ { + roundData[i], roundData[len(roundData)-i-1] = roundData[len(roundData)-i-1], roundData[i] + roundHash[i], roundHash[len(roundHash)-i-1] = roundHash[len(roundHash)-i-1], roundHash[i] + } + return roundData, roundHash, expectedRound + 1, nil +} + +// LookupAccountAddressFromAddressID looks up an account based on a rowid +func (r *accountsV2Reader) LookupAccountAddressFromAddressID(ctx context.Context, addrid int64) (address basics.Address, err error) { + var addrbuf []byte + err = r.q.QueryRowContext(ctx, "SELECT address FROM accountbase WHERE rowid = ?", addrid).Scan(&addrbuf) + if err != nil { + if err == sql.ErrNoRows { + err = fmt.Errorf("no matching address could be found for rowid %d: %w", addrid, err) + } + return + } + if len(addrbuf) != len(address) { + err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(address)) + return + } + copy(address[:], addrbuf) + return +} + +func (r *accountsV2Reader) LookupAccountDataByAddress(addr basics.Address) (rowid int64, data []byte, err error) { + // optimize this query for repeated usage + selectStmt, err := r.getOrPrepare("SELECT rowid, data FROM accountbase WHERE address=?") + if err != nil { + return + } + + err = selectStmt.QueryRow(addr[:]).Scan(&rowid, &data) + if err != nil { + return + } + return rowid, data, err +} + +// LookupOnlineAccountDataByAddress looks up online account data by address. +func (r *accountsV2Reader) LookupOnlineAccountDataByAddress(addr basics.Address) (rowid int64, data []byte, err error) { + // optimize this query for repeated usage + selectStmt, err := r.getOrPrepare("SELECT rowid, data FROM onlineaccounts WHERE address=? ORDER BY updround DESC LIMIT 1") + if err != nil { + return + } + + err = selectStmt.QueryRow(addr[:]).Scan(&rowid, &data) + if err != nil { + return + } + return rowid, data, err +} + +// LookupAccountRowID looks up the rowid of an account based on its address. +func (r *accountsV2Reader) LookupAccountRowID(addr basics.Address) (rowid int64, err error) { + // optimize this query for repeated usage + addrRowidStmt, err := r.getOrPrepare("SELECT rowid FROM accountbase WHERE address=?") + if err != nil { + return + } + + err = addrRowidStmt.QueryRow(addr[:]).Scan(&rowid) + if err != nil { + return + } + return rowid, err +} + +// LookupResourceDataByAddrID looks up the resource data by account rowid + resource aidx. +func (r *accountsV2Reader) LookupResourceDataByAddrID(addrid int64, aidx basics.CreatableIndex) (data []byte, err error) { + // optimize this query for repeated usage + selectStmt, err := r.getOrPrepare("SELECT data FROM resources WHERE addrid = ? AND aidx = ?") + if err != nil { + return + } + + err = selectStmt.QueryRow(addrid, aidx).Scan(&data) + if err != nil { + return + } + return data, err +} + +// LoadAllFullAccounts loads all accounts from balancesTable and resourcesTable. +// On every account full load it invokes acctCb callback to report progress and data. +func (r *accountsV2Reader) LoadAllFullAccounts( + ctx context.Context, + balancesTable string, resourcesTable string, + acctCb func(basics.Address, basics.AccountData), +) (count int, err error) { + baseRows, err := r.q.QueryContext(ctx, fmt.Sprintf("SELECT rowid, address, data FROM %s ORDER BY address", balancesTable)) + if err != nil { + return + } + defer baseRows.Close() + + for baseRows.Next() { + var addrbuf []byte + var buf []byte + var rowid sql.NullInt64 + err = baseRows.Scan(&rowid, &addrbuf, &buf) + if err != nil { + return + } + if !rowid.Valid { + err = fmt.Errorf("invalid rowid in %s", balancesTable) + return + } + + var data BaseAccountData + 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) + + var ad basics.AccountData + ad, err = r.LoadFullAccount(ctx, resourcesTable, addr, rowid.Int64, data) + if err != nil { + return + } + + acctCb(addr, ad) + + count++ + } + return +} + +// LoadFullAccount converts BaseAccountData into basics.AccountData and loads all resources as needed +func (r *accountsV2Reader) LoadFullAccount(ctx context.Context, resourcesTable string, addr basics.Address, addrid int64, data BaseAccountData) (ad basics.AccountData, err error) { + ad = data.GetAccountData() + + hasResources := false + if data.TotalAppParams > 0 { + ad.AppParams = make(map[basics.AppIndex]basics.AppParams, data.TotalAppParams) + hasResources = true + } + if data.TotalAppLocalStates > 0 { + ad.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState, data.TotalAppLocalStates) + hasResources = true + } + if data.TotalAssetParams > 0 { + ad.AssetParams = make(map[basics.AssetIndex]basics.AssetParams, data.TotalAssetParams) + hasResources = true + } + if data.TotalAssets > 0 { + ad.Assets = make(map[basics.AssetIndex]basics.AssetHolding, data.TotalAssets) + hasResources = true + } + + if !hasResources { + return + } + + var resRows *sql.Rows + query := fmt.Sprintf("SELECT aidx, data FROM %s where addrid = ?", resourcesTable) + resRows, err = r.q.QueryContext(ctx, query, addrid) + if err != nil { + return + } + defer resRows.Close() + + for resRows.Next() { + var buf []byte + var aidx int64 + err = resRows.Scan(&aidx, &buf) + if err != nil { + return + } + var resData ResourcesData + err = protocol.Decode(buf, &resData) + if err != nil { + return + } + if resData.ResourceFlags == ResourceFlagsNotHolding { + err = fmt.Errorf("addr %s (%d) aidx = %d resourceFlagsNotHolding should not be persisted", addr.String(), addrid, aidx) + return + } + if resData.IsApp() { + if resData.IsOwning() { + ad.AppParams[basics.AppIndex(aidx)] = resData.GetAppParams() + } + if resData.IsHolding() { + ad.AppLocalStates[basics.AppIndex(aidx)] = resData.GetAppLocalState() + } + } else if resData.IsAsset() { + if resData.IsOwning() { + ad.AssetParams[basics.AssetIndex(aidx)] = resData.GetAssetParams() + } + if resData.IsHolding() { + ad.Assets[basics.AssetIndex(aidx)] = resData.GetAssetHolding() + } + } else { + err = fmt.Errorf("unknown resource data: %v", resData) + return + } + } + + if uint64(len(ad.AssetParams)) != data.TotalAssetParams { + err = fmt.Errorf("%s assets params mismatch: %d != %d", addr.String(), len(ad.AssetParams), data.TotalAssetParams) + } + if err == nil && uint64(len(ad.Assets)) != data.TotalAssets { + err = fmt.Errorf("%s assets mismatch: %d != %d", addr.String(), len(ad.Assets), data.TotalAssets) + } + if err == nil && uint64(len(ad.AppParams)) != data.TotalAppParams { + err = fmt.Errorf("%s app params mismatch: %d != %d", addr.String(), len(ad.AppParams), data.TotalAppParams) + } + if err == nil && uint64(len(ad.AppLocalStates)) != data.TotalAppLocalStates { + err = fmt.Errorf("%s app local states mismatch: %d != %d", addr.String(), len(ad.AppLocalStates), data.TotalAppLocalStates) + } + + return ad, err +} + +func (r *accountsV2Reader) AccountsOnlineRoundParams() (onlineRoundParamsData []ledgercore.OnlineRoundParamsData, endRound basics.Round, err error) { + rows, err := r.q.Query("SELECT rnd, data FROM onlineroundparamstail ORDER BY rnd ASC") + if err != nil { + return nil, 0, err + } + defer rows.Close() + + for rows.Next() { + var buf []byte + err = rows.Scan(&endRound, &buf) + if err != nil { + return nil, 0, err + } + + var data ledgercore.OnlineRoundParamsData + err = protocol.Decode(buf, &data) + if err != nil { + return nil, 0, err + } + + onlineRoundParamsData = append(onlineRoundParamsData, data) + } + return +} + +// AccountsPutTotals updates account totals +func (w *accountsV2Writer) AccountsPutTotals(totals ledgercore.AccountTotals, catchpointStaging bool) error { + id := "" + if catchpointStaging { + id = "catchpointStaging" + } + _, err := w.e.Exec("REPLACE INTO accounttotals (id, online, onlinerewardunits, offline, offlinerewardunits, notparticipating, notparticipatingrewardunits, rewardslevel) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + id, + totals.Online.Money.Raw, totals.Online.RewardUnits, + totals.Offline.Money.Raw, totals.Offline.RewardUnits, + totals.NotParticipating.Money.Raw, totals.NotParticipating.RewardUnits, + totals.RewardsLevel) + return err +} + +func (w *accountsV2Writer) TxtailNewRound(ctx context.Context, baseRound basics.Round, roundData [][]byte, forgetBeforeRound basics.Round) error { + insertStmt, err := w.e.PrepareContext(ctx, "INSERT INTO txtail(rnd, data) VALUES(?, ?)") + if err != nil { + return err + } + defer insertStmt.Close() + + for i, data := range roundData { + _, err = insertStmt.ExecContext(ctx, int(baseRound)+i, data[:]) + if err != nil { + return err + } + } + + _, err = w.e.ExecContext(ctx, "DELETE FROM txtail WHERE rnd < ?", forgetBeforeRound) + return err +} + +// OnlineAccountsDelete deleted entries with updRound <= expRound +func (w *accountsV2Writer) OnlineAccountsDelete(forgetBefore basics.Round) (err error) { + rows, err := w.e.Query("SELECT rowid, address, updRound, data FROM onlineaccounts WHERE updRound < ? ORDER BY address, updRound DESC", forgetBefore) + if err != nil { + return err + } + defer rows.Close() + + var rowids []int64 + var rowid sql.NullInt64 + var updRound sql.NullInt64 + var buf []byte + var addrbuf []byte + + var prevAddr []byte + + for rows.Next() { + err = rows.Scan(&rowid, &addrbuf, &updRound, &buf) + if err != nil { + return err + } + if !rowid.Valid || !updRound.Valid { + return fmt.Errorf("onlineAccountsDelete: invalid rowid or updRound") + } + if len(addrbuf) != len(basics.Address{}) { + err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(basics.Address{})) + return + } + + if !bytes.Equal(addrbuf, prevAddr) { + // new address + // if the first (latest) entry is + // - offline then delete all + // - online then safe to delete all previous except this first (latest) + + // reset the state + prevAddr = addrbuf + + var oad BaseOnlineAccountData + err = protocol.Decode(buf, &oad) + if err != nil { + return + } + if oad.IsVotingEmpty() { + // delete this and all subsequent + rowids = append(rowids, rowid.Int64) + } + + // restart the loop + // if there are some subsequent entries, they will deleted on the next iteration + // if no subsequent entries, the loop will reset the state and the latest entry does not get deleted + continue + } + // delete all subsequent entries + rowids = append(rowids, rowid.Int64) + } + + return onlineAccountsDeleteByRowIDs(w.e, rowids) +} + +func onlineAccountsDeleteByRowIDs(e db.Executable, rowids []int64) (err error) { + if len(rowids) == 0 { + return + } + + // sqlite3 < 3.32.0 allows SQLITE_MAX_VARIABLE_NUMBER = 999 bindings + // see https://www.sqlite.org/limits.html + // rowids might be larger => split to chunks are remove + chunks := rowidsToChunkedArgs(rowids) + for _, chunk := range chunks { + _, err = e.Exec("DELETE FROM onlineaccounts WHERE rowid IN (?"+strings.Repeat(",?", len(chunk)-1)+")", chunk...) + if err != nil { + return + } + } + return +} + +func rowidsToChunkedArgs(rowids []int64) [][]interface{} { + const sqliteMaxVariableNumber = 999 + + numChunks := len(rowids)/sqliteMaxVariableNumber + 1 + if len(rowids)%sqliteMaxVariableNumber == 0 { + numChunks-- + } + chunks := make([][]interface{}, numChunks) + if numChunks == 1 { + // optimize memory consumption for the most common case + chunks[0] = make([]interface{}, len(rowids)) + for i, rowid := range rowids { + chunks[0][i] = interface{}(rowid) + } + } else { + for i := 0; i < numChunks; i++ { + chunkSize := sqliteMaxVariableNumber + if i == numChunks-1 { + chunkSize = len(rowids) - (numChunks-1)*sqliteMaxVariableNumber + } + chunks[i] = make([]interface{}, chunkSize) + } + for i, rowid := range rowids { + chunkIndex := i / sqliteMaxVariableNumber + chunks[chunkIndex][i%sqliteMaxVariableNumber] = interface{}(rowid) + } + } + return chunks +} + +// UpdateAccountsRound updates the round number associated with the current account data. +func (w *accountsV2Writer) UpdateAccountsRound(rnd basics.Round) (err error) { + res, err := w.e.Exec("UPDATE acctrounds SET rnd=? WHERE id='acctbase' AND rnd rnd { + err = fmt.Errorf("newRound %d is not after base %d", rnd, base) + return + } else if base != rnd { + err = fmt.Errorf("updateAccountsRound(acctbase, %d): expected to update 1 row but got %d", rnd, aff) + return + } + } + return +} + +// UpdateAccountsHashRound updates the round number associated with the hash of current account data. +func (w *accountsV2Writer) UpdateAccountsHashRound(ctx context.Context, hashRound basics.Round) (err error) { + res, err := w.e.ExecContext(ctx, "INSERT OR REPLACE INTO acctrounds(id,rnd) VALUES('hashbase',?)", hashRound) + if err != nil { + return + } + + aff, err := res.RowsAffected() + if err != nil { + return + } + + if aff != 1 { + err = fmt.Errorf("updateAccountsHashRound(hashbase,%d): expected to update 1 row but got %d", hashRound, aff) + return + } + return +} + +// ResetAccountHashes resets the account hashes generated by the merkle commiter. +func (w *accountsV2Writer) ResetAccountHashes(ctx context.Context) (err error) { + _, err = w.e.ExecContext(ctx, `DELETE FROM accounthashes`) + return +} + +func (w *accountsV2Writer) AccountsPutOnlineRoundParams(onlineRoundParamsData []ledgercore.OnlineRoundParamsData, startRound basics.Round) error { + insertStmt, err := w.e.Prepare("INSERT INTO onlineroundparamstail (rnd, data) VALUES (?, ?)") + if err != nil { + return err + } + + for i, onlineRoundParams := range onlineRoundParamsData { + _, err = insertStmt.Exec(startRound+basics.Round(i), protocol.Encode(&onlineRoundParams)) + if err != nil { + return err + } + } + return nil +} + +func (w *accountsV2Writer) AccountsPruneOnlineRoundParams(deleteBeforeRound basics.Round) error { + _, err := w.e.Exec("DELETE FROM onlineroundparamstail WHERE rnd. + +package store + +import ( + "testing" + + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" +) + +func TestRowidsToChunkedArgs(t *testing.T) { + partitiontest.PartitionTest(t) + + res := rowidsToChunkedArgs([]int64{1}) + require.Equal(t, 1, cap(res)) + require.Equal(t, 1, len(res)) + require.Equal(t, 1, cap(res[0])) + require.Equal(t, 1, len(res[0])) + require.Equal(t, []interface{}{int64(1)}, res[0]) + + input := make([]int64, 999) + for i := 0; i < len(input); i++ { + input[i] = int64(i) + } + res = rowidsToChunkedArgs(input) + require.Equal(t, 1, cap(res)) + require.Equal(t, 1, len(res)) + require.Equal(t, 999, cap(res[0])) + require.Equal(t, 999, len(res[0])) + for i := 0; i < len(input); i++ { + require.Equal(t, interface{}(int64(i)), res[0][i]) + } + + input = make([]int64, 1001) + for i := 0; i < len(input); i++ { + input[i] = int64(i) + } + res = rowidsToChunkedArgs(input) + require.Equal(t, 2, cap(res)) + require.Equal(t, 2, len(res)) + require.Equal(t, 999, cap(res[0])) + require.Equal(t, 999, len(res[0])) + require.Equal(t, 2, cap(res[1])) + require.Equal(t, 2, len(res[1])) + for i := 0; i < 999; i++ { + require.Equal(t, interface{}(int64(i)), res[0][i]) + } + j := 0 + for i := 999; i < len(input); i++ { + require.Equal(t, interface{}(int64(i)), res[1][j]) + j++ + } + + input = make([]int64, 2*999) + for i := 0; i < len(input); i++ { + input[i] = int64(i) + } + res = rowidsToChunkedArgs(input) + require.Equal(t, 2, cap(res)) + require.Equal(t, 2, len(res)) + require.Equal(t, 999, cap(res[0])) + require.Equal(t, 999, len(res[0])) + require.Equal(t, 999, cap(res[1])) + require.Equal(t, 999, len(res[1])) + for i := 0; i < 999; i++ { + require.Equal(t, interface{}(int64(i)), res[0][i]) + } + j = 0 + for i := 999; i < len(input); i++ { + require.Equal(t, interface{}(int64(i)), res[1][j]) + j++ + } +} diff --git a/ledger/blockdb.go b/ledger/store/blockdb/blockdb.go similarity index 76% rename from ledger/blockdb.go rename to ledger/store/blockdb/blockdb.go index 1919fdb80b..6417d84529 100644 --- a/ledger/blockdb.go +++ b/ledger/store/blockdb/blockdb.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 ledger +package blockdb import ( "database/sql" @@ -44,7 +44,8 @@ var blockResetExprs = []string{ `DROP TABLE IF EXISTS blocks`, } -func blockInit(tx *sql.Tx, initBlocks []bookkeeping.Block) error { +// BlockInit initializes blockdb +func BlockInit(tx *sql.Tx, initBlocks []bookkeeping.Block) error { for _, tableCreate := range blockSchema { _, err := tx.Exec(tableCreate) if err != nil { @@ -52,14 +53,14 @@ func blockInit(tx *sql.Tx, initBlocks []bookkeeping.Block) error { } } - next, err := blockNext(tx) + next, err := BlockNext(tx) if err != nil { return err } if next == 0 { for _, blk := range initBlocks { - err = blockPut(tx, blk, agreement.Certificate{}) + err = BlockPut(tx, blk, agreement.Certificate{}) if err != nil { serr, ok := err.(sqlite3.Error) if ok && serr.Code == sqlite3.ErrConstraint { @@ -73,7 +74,8 @@ func blockInit(tx *sql.Tx, initBlocks []bookkeeping.Block) error { return nil } -func blockResetDB(tx *sql.Tx) error { +// BlockResetDB resets blockdb +func BlockResetDB(tx *sql.Tx) error { for _, stmt := range blockResetExprs { _, err := tx.Exec(stmt) if err != nil { @@ -83,7 +85,8 @@ func blockResetDB(tx *sql.Tx) error { return nil } -func blockGet(tx *sql.Tx, rnd basics.Round) (blk bookkeeping.Block, err error) { +// BlockGet retrieves a block by a round number +func BlockGet(tx *sql.Tx, rnd basics.Round) (blk bookkeeping.Block, err error) { var buf []byte err = tx.QueryRow("SELECT blkdata FROM blocks WHERE rnd=?", rnd).Scan(&buf) if err != nil { @@ -98,7 +101,8 @@ func blockGet(tx *sql.Tx, rnd basics.Round) (blk bookkeeping.Block, err error) { return } -func blockGetHdr(tx *sql.Tx, rnd basics.Round) (hdr bookkeeping.BlockHeader, err error) { +// BlockGetHdr retrieves a block header by a round number +func BlockGetHdr(tx *sql.Tx, rnd basics.Round) (hdr bookkeeping.BlockHeader, err error) { var buf []byte err = tx.QueryRow("SELECT hdrdata FROM blocks WHERE rnd=?", rnd).Scan(&buf) if err != nil { @@ -113,7 +117,8 @@ func blockGetHdr(tx *sql.Tx, rnd basics.Round) (hdr bookkeeping.BlockHeader, err return } -func blockGetEncodedCert(tx *sql.Tx, rnd basics.Round) (blk []byte, cert []byte, err error) { +// BlockGetEncodedCert retrieves raw block and cert by a round number +func BlockGetEncodedCert(tx *sql.Tx, rnd basics.Round) (blk []byte, cert []byte, err error) { err = tx.QueryRow("SELECT blkdata, certdata FROM blocks WHERE rnd=?", rnd).Scan(&blk, &cert) if err != nil { if err == sql.ErrNoRows { @@ -125,8 +130,9 @@ func blockGetEncodedCert(tx *sql.Tx, rnd basics.Round) (blk []byte, cert []byte, return } -func blockGetCert(tx *sql.Tx, rnd basics.Round) (blk bookkeeping.Block, cert agreement.Certificate, err error) { - blkbuf, certbuf, err := blockGetEncodedCert(tx, rnd) +// BlockGetCert retrieves block and cert by a round number +func BlockGetCert(tx *sql.Tx, rnd basics.Round) (blk bookkeeping.Block, cert agreement.Certificate, err error) { + blkbuf, certbuf, err := BlockGetEncodedCert(tx, rnd) if err != nil { return } @@ -145,7 +151,8 @@ func blockGetCert(tx *sql.Tx, rnd basics.Round) (blk bookkeeping.Block, cert agr return } -func blockPut(tx *sql.Tx, blk bookkeeping.Block, cert agreement.Certificate) error { +// BlockPut stores block and certificate +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) if err != nil { @@ -174,7 +181,8 @@ func blockPut(tx *sql.Tx, blk bookkeeping.Block, cert agreement.Certificate) err return err } -func blockNext(tx *sql.Tx) (basics.Round, error) { +// BlockNext returns the next expected round number +func BlockNext(tx *sql.Tx) (basics.Round, error) { var max sql.NullInt64 err := tx.QueryRow("SELECT MAX(rnd) FROM blocks").Scan(&max) if err != nil { @@ -188,7 +196,8 @@ func blockNext(tx *sql.Tx) (basics.Round, error) { return 0, nil } -func blockLatest(tx *sql.Tx) (basics.Round, error) { +// BlockLatest returns the latest persisted round number +func BlockLatest(tx *sql.Tx) (basics.Round, error) { var max sql.NullInt64 err := tx.QueryRow("SELECT MAX(rnd) FROM blocks").Scan(&max) if err != nil { @@ -202,7 +211,8 @@ func blockLatest(tx *sql.Tx) (basics.Round, error) { return 0, fmt.Errorf("no blocks present") } -func blockEarliest(tx *sql.Tx) (basics.Round, error) { +// BlockEarliest returns the lowest persisted round number +func BlockEarliest(tx *sql.Tx) (basics.Round, error) { var min sql.NullInt64 err := tx.QueryRow("SELECT MIN(rnd) FROM blocks").Scan(&min) if err != nil { @@ -216,8 +226,9 @@ func blockEarliest(tx *sql.Tx) (basics.Round, error) { return 0, fmt.Errorf("no blocks present") } -func blockForgetBefore(tx *sql.Tx, rnd basics.Round) error { - next, err := blockNext(tx) +// BlockForgetBefore removes block entries with round numbers less than the specified round +func BlockForgetBefore(tx *sql.Tx, rnd basics.Round) error { + next, err := BlockNext(tx) if err != nil { return err } @@ -230,7 +241,8 @@ func blockForgetBefore(tx *sql.Tx, rnd basics.Round) error { return err } -func blockStartCatchupStaging(tx *sql.Tx, blk bookkeeping.Block) error { +// BlockStartCatchupStaging initializes catchup for catchpoint +func BlockStartCatchupStaging(tx *sql.Tx, blk bookkeeping.Block) error { // delete the old catchpointblocks table, if there is such. for _, stmt := range blockResetExprs { stmt = strings.Replace(stmt, "blocks", "catchpointblocks", 1) @@ -262,7 +274,8 @@ func blockStartCatchupStaging(tx *sql.Tx, blk bookkeeping.Block) error { return nil } -func blockCompleteCatchup(tx *sql.Tx) (err error) { +// BlockCompleteCatchup applies catchpoint caught up blocks +func BlockCompleteCatchup(tx *sql.Tx) (err error) { _, err = tx.Exec("ALTER TABLE blocks RENAME TO blocks_old") if err != nil { return err @@ -278,8 +291,8 @@ func blockCompleteCatchup(tx *sql.Tx) (err error) { return nil } -// TODO: unused, either actually implement cleanup on catchpoint failure, or delete this -func blockAbortCatchup(tx *sql.Tx) error { +// BlockAbortCatchup TODO: unused, either actually implement cleanup on catchpoint failure, or delete this +func BlockAbortCatchup(tx *sql.Tx) error { // delete the old catchpointblocks table, if there is such. for _, stmt := range blockResetExprs { stmt = strings.Replace(stmt, "blocks", "catchpointblocks", 1) @@ -291,7 +304,8 @@ func blockAbortCatchup(tx *sql.Tx) error { return nil } -func blockPutStaging(tx *sql.Tx, blk bookkeeping.Block) (err error) { +// BlockPutStaging store a block into catchpoint staging table +func BlockPutStaging(tx *sql.Tx, blk bookkeeping.Block) (err error) { // insert the new entry _, err = tx.Exec("INSERT INTO catchpointblocks (rnd, proto, hdrdata, blkdata) VALUES (?, ?, ?, ?)", blk.Round(), @@ -305,7 +319,8 @@ func blockPutStaging(tx *sql.Tx, blk bookkeeping.Block) (err error) { return nil } -func blockEnsureSingleBlock(tx *sql.Tx) (blk bookkeeping.Block, err error) { +// BlockEnsureSingleBlock retains only one (highest) block in catchpoint staging table +func BlockEnsureSingleBlock(tx *sql.Tx) (blk bookkeeping.Block, err error) { // delete all the blocks that aren't the latest one. var max sql.NullInt64 err = tx.QueryRow("SELECT MAX(rnd) FROM catchpointblocks").Scan(&max) diff --git a/ledger/blockdb_test.go b/ledger/store/blockdb/blockdb_test.go similarity index 68% rename from ledger/blockdb_test.go rename to ledger/store/blockdb/blockdb_test.go index da1d8303c9..f659888c2b 100644 --- a/ledger/blockdb_test.go +++ b/ledger/store/blockdb/blockdb_test.go @@ -14,12 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package ledger +package blockdb import ( "database/sql" - "fmt" - "strings" "testing" "github.com/stretchr/testify/require" @@ -28,20 +26,20 @@ import ( "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" + storetesting "github.com/algorand/go-algorand/ledger/store/testing" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/algorand/go-algorand/util/db" ) -func dbOpenTest(t testing.TB, inMemory bool) (db.Pair, string) { - fn := fmt.Sprintf("%s.%d", strings.ReplaceAll(t.Name(), "/", "."), crypto.RandUint64()) - dbs, err := db.OpenPair(fn, inMemory) - require.NoErrorf(t, err, "Filename : %s\nInMemory: %v", fn, inMemory) - return dbs, fn +var testPoolAddr = 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 testSinkAddr = basics.Address{0x2c, 0x2a, 0x6c, 0xe9, 0xa9, 0xa7, 0xc2, 0x8c, 0x22, 0x95, 0xfd, 0x32, 0x4f, 0x77, 0xa5, 0x4, 0x8b, 0x42, 0xc2, 0xb7, 0xa8, 0x54, 0x84, 0xb6, 0x80, 0xb1, 0xe1, 0x3d, 0x59, 0x9b, 0xeb, 0x36} + +type testBlockEntry struct { + block bookkeeping.Block + cert agreement.Certificate } -func randomBlock(r basics.Round) blockEntry { +func randomBlock(r basics.Round) testBlockEntry { b := bookkeeping.Block{} c := agreement.Certificate{} @@ -51,14 +49,14 @@ func randomBlock(r basics.Round) blockEntry { b.FeeSink = testSinkAddr c.Round = r - return blockEntry{ + return testBlockEntry{ block: b, cert: c, } } -func randomInitChain(proto protocol.ConsensusVersion, nblock int) []blockEntry { - res := make([]blockEntry, 0) +func randomInitChain(proto protocol.ConsensusVersion, nblock int) []testBlockEntry { + res := make([]testBlockEntry, 0) for i := 0; i < nblock; i++ { blkent := randomBlock(basics.Round(i)) blkent.cert = agreement.Certificate{} @@ -68,20 +66,12 @@ func randomInitChain(proto protocol.ConsensusVersion, nblock int) []blockEntry { return res } -func blockChainBlocks(be []blockEntry) []bookkeeping.Block { - res := make([]bookkeeping.Block, 0) - for _, e := range be { - res = append(res, e.block) - } - return res -} - -func checkBlockDB(t *testing.T, tx *sql.Tx, blocks []blockEntry) { - next, err := blockNext(tx) +func checkBlockDB(t *testing.T, tx *sql.Tx, blocks []testBlockEntry) { + next, err := BlockNext(tx) require.NoError(t, err) require.Equal(t, next, basics.Round(len(blocks))) - latest, err := blockLatest(tx) + latest, err := BlockLatest(tx) if len(blocks) == 0 { require.Error(t, err) } else { @@ -89,7 +79,7 @@ func checkBlockDB(t *testing.T, tx *sql.Tx, blocks []blockEntry) { require.Equal(t, latest, basics.Round(len(blocks))-1) } - earliest, err := blockEarliest(tx) + earliest, err := BlockEarliest(tx) if len(blocks) == 0 { require.Error(t, err) } else { @@ -98,38 +88,40 @@ func checkBlockDB(t *testing.T, tx *sql.Tx, blocks []blockEntry) { } for rnd := basics.Round(0); rnd < basics.Round(len(blocks)); rnd++ { - blk, err := blockGet(tx, rnd) + blk, err := BlockGet(tx, rnd) require.NoError(t, err) require.Equal(t, blk, blocks[rnd].block) - blk, cert, err := blockGetCert(tx, rnd) + blk, cert, err := BlockGetCert(tx, rnd) require.NoError(t, err) require.Equal(t, blk, blocks[rnd].block) require.Equal(t, cert, blocks[rnd].cert) } - _, err = blockGet(tx, basics.Round(len(blocks))) + _, err = BlockGet(tx, basics.Round(len(blocks))) require.Error(t, err) } -func setDbLogging(t testing.TB, dbs db.Pair) { - dblogger := logging.TestingLog(t) - dbs.Rdb.SetLogger(dblogger) - dbs.Wdb.SetLogger(dblogger) +func blockChainBlocks(be []testBlockEntry) []bookkeeping.Block { + res := make([]bookkeeping.Block, 0) + for _, e := range be { + res = append(res, e.block) + } + return res } func TestBlockDBEmpty(t *testing.T) { partitiontest.PartitionTest(t) - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() require.NoError(t, err) defer tx.Rollback() - err = blockInit(tx, nil) + err = BlockInit(tx, nil) require.NoError(t, err) checkBlockDB(t, tx, nil) } @@ -137,8 +129,8 @@ func TestBlockDBEmpty(t *testing.T) { func TestBlockDBInit(t *testing.T) { partitiontest.PartitionTest(t) - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() @@ -147,11 +139,11 @@ func TestBlockDBInit(t *testing.T) { blocks := randomInitChain(protocol.ConsensusCurrentVersion, 10) - err = blockInit(tx, blockChainBlocks(blocks)) + err = BlockInit(tx, blockChainBlocks(blocks)) require.NoError(t, err) checkBlockDB(t, tx, blocks) - err = blockInit(tx, blockChainBlocks(blocks)) + err = BlockInit(tx, blockChainBlocks(blocks)) require.NoError(t, err) checkBlockDB(t, tx, blocks) } @@ -159,8 +151,8 @@ func TestBlockDBInit(t *testing.T) { func TestBlockDBAppend(t *testing.T) { partitiontest.PartitionTest(t) - dbs, _ := dbOpenTest(t, true) - setDbLogging(t, dbs) + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) defer dbs.Close() tx, err := dbs.Wdb.Handle.Begin() @@ -169,13 +161,13 @@ func TestBlockDBAppend(t *testing.T) { blocks := randomInitChain(protocol.ConsensusCurrentVersion, 10) - err = blockInit(tx, blockChainBlocks(blocks)) + err = BlockInit(tx, blockChainBlocks(blocks)) require.NoError(t, err) checkBlockDB(t, tx, blocks) for i := 0; i < 10; i++ { blkent := randomBlock(basics.Round(len(blocks))) - err = blockPut(tx, blkent.block, blkent.cert) + err = BlockPut(tx, blkent.block, blkent.cert) require.NoError(t, err) blocks = append(blocks, blkent) diff --git a/ledger/store/catchpoint.go b/ledger/store/catchpoint.go new file mode 100644 index 0000000000..0b4f2da66d --- /dev/null +++ b/ledger/store/catchpoint.go @@ -0,0 +1,732 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "context" + "database/sql" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/crypto/merkletrie" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" + "github.com/mattn/go-sqlite3" +) + +// TrieMemoryConfig is the memory configuration setup used for the merkle trie. +var TrieMemoryConfig = merkletrie.MemoryConfig{ + NodesCountPerPage: MerkleCommitterNodesPerPage, + CachedNodesCount: TrieCachedNodesCount, + PageFillFactor: 0.95, + MaxChildrenPagesThreshold: 64, +} + +// MerkleCommitterNodesPerPage controls how many nodes will be stored in a single page +// value was calibrated using BenchmarkCalibrateNodesPerPage +var MerkleCommitterNodesPerPage = int64(116) + +// TrieCachedNodesCount defines how many balances trie nodes we would like to keep around in memory. +// value was calibrated using BenchmarkCalibrateCacheNodeSize +var TrieCachedNodesCount = 9000 + +// CatchpointDirName represents the directory name in which all the catchpoints files are stored +var CatchpointDirName = "catchpoints" + +// CatchpointState is used to store catchpoint related variables into the catchpointstate table. +// +//msgp:ignore CatchpointState +type CatchpointState string + +const ( + // CatchpointStateLastCatchpoint is written by a node once a catchpoint label is created for a round + CatchpointStateLastCatchpoint = CatchpointState("lastCatchpoint") + // CatchpointStateWritingFirstStageInfo state variable is set to 1 if catchpoint's first stage is unfinished, + // and is 0 otherwise. Used to clear / restart the first stage after a crash. + // This key is set in the same db transaction as the account updates, so the + // unfinished first stage corresponds to the current db round. + CatchpointStateWritingFirstStageInfo = CatchpointState("writingFirstStageInfo") + // catchpointStateWritingCatchpoint if there is an unfinished catchpoint, this state variable is set to + // the catchpoint's round. Otherwise, it is set to 0. + // DEPRECATED. + catchpointStateWritingCatchpoint = CatchpointState("writingCatchpoint") + // CatchpointStateCatchupState is the state of the catchup process. The variable is stored only during the catchpoint catchup process, and removed afterward. + CatchpointStateCatchupState = CatchpointState("catchpointCatchupState") + // CatchpointStateCatchupLabel is the label to which the currently catchpoint catchup process is trying to catchup to. + CatchpointStateCatchupLabel = CatchpointState("catchpointCatchupLabel") + // CatchpointStateCatchupBlockRound is the block round that is associated with the current running catchpoint catchup. + CatchpointStateCatchupBlockRound = CatchpointState("catchpointCatchupBlockRound") + // CatchpointStateCatchupBalancesRound is the balance round that is associated with the current running catchpoint catchup. Typically it would be + // equal to CatchpointStateCatchupBlockRound - 320. + CatchpointStateCatchupBalancesRound = CatchpointState("catchpointCatchupBalancesRound") + // CatchpointStateCatchupHashRound is the round that is associated with the hash of the merkle trie. Normally, it's identical to CatchpointStateCatchupBalancesRound, + // however, it could differ when we catchup from a catchpoint that was created using a different version : in this case, + // we set it to zero in order to reset the merkle trie. This would force the merkle trie to be re-build on startup ( if needed ). + CatchpointStateCatchupHashRound = CatchpointState("catchpointCatchupHashRound") + // CatchpointStateCatchpointLookback is the number of rounds we keep catchpoints for + CatchpointStateCatchpointLookback = CatchpointState("catchpointLookback") +) + +// UnfinishedCatchpointRecord represents a stored record of an unfinished catchpoint. +type UnfinishedCatchpointRecord struct { + Round basics.Round + BlockHash crypto.Digest +} + +// NormalizedAccountBalance is a staging area for a catchpoint file account information before it's being added to the catchpoint staging tables. +type NormalizedAccountBalance struct { + // The public key address to which the account belongs. + Address basics.Address + // accountData contains the baseAccountData for that account. + AccountData BaseAccountData + // resources is a map, where the key is the creatable index, and the value is the resource data. + Resources map[basics.CreatableIndex]ResourcesData + // encodedAccountData contains the baseAccountData encoded bytes that are going to be written to the accountbase table. + EncodedAccountData []byte + // accountHashes contains a list of all the hashes that would need to be added to the merkle trie for that account. + // on V6, we could have multiple hashes, since we have separate account/resource hashes. + AccountHashes [][]byte + // normalizedBalance contains the normalized balance for the account. + NormalizedBalance uint64 + // encodedResources provides the encoded form of the resources + EncodedResources map[basics.CreatableIndex][]byte + // partial balance indicates that the original account balance was split into multiple parts in catchpoint creation time + PartialBalance bool +} + +type catchpointReader struct { + q db.Queryable +} + +type catchpointWriter struct { + e db.Executable +} + +type catchpointReaderWriter struct { + catchpointReader + catchpointWriter +} + +// CatchpointFirstStageInfo For the `catchpointfirststageinfo` table. +type CatchpointFirstStageInfo struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Totals ledgercore.AccountTotals `codec:"accountTotals"` + TrieBalancesHash crypto.Digest `codec:"trieBalancesHash"` + // Total number of accounts in the catchpoint data file. Only set when catchpoint + // data files are generated. + TotalAccounts uint64 `codec:"accountsCount"` + + // Total number of accounts in the catchpoint data file. Only set when catchpoint + // data files are generated. + TotalKVs uint64 `codec:"kvsCount"` + + // Total number of chunks in the catchpoint data file. Only set when catchpoint + // data files are generated. + TotalChunks uint64 `codec:"chunksCount"` + // BiggestChunkLen is the size in the bytes of the largest chunk, used when re-packing. + BiggestChunkLen uint64 `codec:"biggestChunk"` +} + +// NewCatchpointSQLReaderWriter creates a Catchpoint SQL reader+writer +func NewCatchpointSQLReaderWriter(e db.Executable) *catchpointReaderWriter { + return &catchpointReaderWriter{ + catchpointReader{q: e}, + catchpointWriter{e: e}, + } +} + +func (cr *catchpointReader) GetCatchpoint(ctx context.Context, round basics.Round) (fileName string, catchpoint string, fileSize int64, err error) { + err = cr.q.QueryRowContext(ctx, "SELECT filename, catchpoint, filesize FROM storedcatchpoints WHERE round=?", int64(round)).Scan(&fileName, &catchpoint, &fileSize) + return +} + +func (cr *catchpointReader) GetOldestCatchpointFiles(ctx context.Context, fileCount int, filesToKeep int) (fileNames map[basics.Round]string, err error) { + err = db.Retry(func() (err error) { + query := "SELECT round, filename FROM storedcatchpoints WHERE pinned = 0 and round <= COALESCE((SELECT round FROM storedcatchpoints WHERE pinned = 0 ORDER BY round DESC LIMIT ?, 1),0) ORDER BY round ASC LIMIT ?" + rows, err := cr.q.QueryContext(ctx, query, filesToKeep, fileCount) + if err != nil { + return err + } + defer rows.Close() + + fileNames = make(map[basics.Round]string) + for rows.Next() { + var fileName string + var round basics.Round + err = rows.Scan(&round, &fileName) + if err != nil { + return err + } + fileNames[round] = fileName + } + + return rows.Err() + }) + if err != nil { + fileNames = nil + } + return +} + +func (cr *catchpointReader) ReadCatchpointStateUint64(ctx context.Context, stateName CatchpointState) (val uint64, err error) { + err = db.Retry(func() (err error) { + query := "SELECT intval FROM catchpointstate WHERE id=?" + var v sql.NullInt64 + err = cr.q.QueryRowContext(ctx, query, stateName).Scan(&v) + if err == sql.ErrNoRows { + return nil + } + if err != nil { + return err + } + if v.Valid { + val = uint64(v.Int64) + } + return nil + }) + return val, err +} + +func (cr *catchpointReader) ReadCatchpointStateString(ctx context.Context, stateName CatchpointState) (val string, err error) { + err = db.Retry(func() (err error) { + query := "SELECT strval FROM catchpointstate WHERE id=?" + var v sql.NullString + err = cr.q.QueryRowContext(ctx, query, stateName).Scan(&v) + if err == sql.ErrNoRows { + return nil + } + if err != nil { + return err + } + + if v.Valid { + val = v.String + } + return nil + }) + return val, err +} + +func (cr *catchpointReader) SelectUnfinishedCatchpoints(ctx context.Context) ([]UnfinishedCatchpointRecord, error) { + var res []UnfinishedCatchpointRecord + + f := func() error { + query := "SELECT round, blockhash FROM unfinishedcatchpoints ORDER BY round" + rows, err := cr.q.QueryContext(ctx, query) + if err != nil { + return err + } + + // Clear `res` in case this function is repeated. + res = res[:0] + for rows.Next() { + var record UnfinishedCatchpointRecord + var blockHash []byte + err = rows.Scan(&record.Round, &blockHash) + if err != nil { + return err + } + copy(record.BlockHash[:], blockHash) + res = append(res, record) + } + + return nil + } + err := db.Retry(f) + if err != nil { + return nil, err + } + + return res, nil +} + +func (cr *catchpointReader) SelectCatchpointFirstStageInfo(ctx context.Context, round basics.Round) (CatchpointFirstStageInfo, bool /*exists*/, error) { + var data []byte + f := func() error { + query := "SELECT info FROM catchpointfirststageinfo WHERE round=?" + err := cr.q.QueryRowContext(ctx, query, round).Scan(&data) + if err == sql.ErrNoRows { + data = nil + return nil + } + return err + } + err := db.Retry(f) + if err != nil { + return CatchpointFirstStageInfo{}, false, err + } + + if data == nil { + return CatchpointFirstStageInfo{}, false, nil + } + + var res CatchpointFirstStageInfo + err = protocol.Decode(data, &res) + if err != nil { + return CatchpointFirstStageInfo{}, false, err + } + + return res, true, nil +} + +func (cr *catchpointReader) SelectOldCatchpointFirstStageInfoRounds(ctx context.Context, maxRound basics.Round) ([]basics.Round, error) { + var res []basics.Round + + f := func() error { + query := "SELECT round FROM catchpointfirststageinfo WHERE round <= ?" + rows, err := cr.q.QueryContext(ctx, query, maxRound) + if err != nil { + return err + } + + // Clear `res` in case this function is repeated. + res = res[:0] + for rows.Next() { + var r basics.Round + err = rows.Scan(&r) + if err != nil { + return err + } + res = append(res, r) + } + + return nil + } + err := db.Retry(f) + if err != nil { + return nil, err + } + + return res, nil +} + +func (cw *catchpointWriter) StoreCatchpoint(ctx context.Context, round basics.Round, fileName string, catchpoint string, fileSize int64) (err error) { + err = db.Retry(func() (err error) { + query := "DELETE FROM storedcatchpoints WHERE round=?" + _, err = cw.e.ExecContext(ctx, query, round) + if err != nil || (fileName == "" && catchpoint == "" && fileSize == 0) { + return err + } + + query = "INSERT INTO storedcatchpoints(round, filename, catchpoint, filesize, pinned) VALUES(?, ?, ?, ?, 0)" + _, err = cw.e.ExecContext(ctx, query, round, fileName, catchpoint, fileSize) + return err + }) + return +} + +func (cw *catchpointWriter) WriteCatchpointStateUint64(ctx context.Context, stateName CatchpointState, setValue uint64) (err error) { + err = db.Retry(func() (err error) { + if setValue == 0 { + return deleteCatchpointStateImpl(ctx, cw.e, stateName) + } + + // we don't know if there is an entry in the table for this state, so we'll insert/replace it just in case. + query := "INSERT OR REPLACE INTO catchpointstate(id, intval) VALUES(?, ?)" + _, err = cw.e.ExecContext(ctx, query, stateName, setValue) + return err + }) + return err +} + +func (cw *catchpointWriter) WriteCatchpointStateString(ctx context.Context, stateName CatchpointState, setValue string) (err error) { + err = db.Retry(func() (err error) { + if setValue == "" { + return deleteCatchpointStateImpl(ctx, cw.e, stateName) + } + + // we don't know if there is an entry in the table for this state, so we'll insert/replace it just in case. + query := "INSERT OR REPLACE INTO catchpointstate(id, strval) VALUES(?, ?)" + _, err = cw.e.ExecContext(ctx, query, stateName, setValue) + return err + }) + return err +} + +func (cw *catchpointWriter) InsertUnfinishedCatchpoint(ctx context.Context, round basics.Round, blockHash crypto.Digest) error { + f := func() error { + query := "INSERT INTO unfinishedcatchpoints(round, blockhash) VALUES(?, ?)" + _, err := cw.e.ExecContext(ctx, query, round, blockHash[:]) + return err + } + return db.Retry(f) +} + +func (cw *catchpointWriter) DeleteUnfinishedCatchpoint(ctx context.Context, round basics.Round) error { + f := func() error { + query := "DELETE FROM unfinishedcatchpoints WHERE round = ?" + _, err := cw.e.ExecContext(ctx, query, round) + return err + } + return db.Retry(f) +} + +func deleteCatchpointStateImpl(ctx context.Context, e db.Executable, stateName CatchpointState) error { + query := "DELETE FROM catchpointstate WHERE id=?" + _, err := e.ExecContext(ctx, query, stateName) + return err +} + +func (cw *catchpointWriter) InsertOrReplaceCatchpointFirstStageInfo(ctx context.Context, round basics.Round, info *CatchpointFirstStageInfo) error { + infoSerialized := protocol.Encode(info) + f := func() error { + query := "INSERT OR REPLACE INTO catchpointfirststageinfo(round, info) VALUES(?, ?)" + _, err := cw.e.ExecContext(ctx, query, round, infoSerialized) + return err + } + return db.Retry(f) +} + +func (cw *catchpointWriter) DeleteOldCatchpointFirstStageInfo(ctx context.Context, maxRoundToDelete basics.Round) error { + f := func() error { + query := "DELETE FROM catchpointfirststageinfo WHERE round <= ?" + _, err := cw.e.ExecContext(ctx, query, maxRoundToDelete) + return err + } + return db.Retry(f) +} + +// WriteCatchpointStagingBalances inserts all the account balances in the provided array into the catchpoint balance staging table catchpointbalances. +func (cw *catchpointWriter) WriteCatchpointStagingBalances(ctx context.Context, bals []NormalizedAccountBalance) error { + selectAcctStmt, err := cw.e.PrepareContext(ctx, "SELECT rowid FROM catchpointbalances WHERE address = ?") + if err != nil { + return err + } + + insertAcctStmt, err := cw.e.PrepareContext(ctx, "INSERT INTO catchpointbalances(address, normalizedonlinebalance, data) VALUES(?, ?, ?)") + if err != nil { + return err + } + + insertRscStmt, err := cw.e.PrepareContext(ctx, "INSERT INTO catchpointresources(addrid, aidx, data) VALUES(?, ?, ?)") + if err != nil { + return err + } + + var result sql.Result + var rowID int64 + for _, balance := range bals { + result, err = insertAcctStmt.ExecContext(ctx, balance.Address[:], balance.NormalizedBalance, balance.EncodedAccountData) + if err == nil { + var aff int64 + aff, err = result.RowsAffected() + if err != nil { + return err + } + if aff != 1 { + return fmt.Errorf("number of affected record in insert was expected to be one, but was %d", aff) + } + rowID, err = result.LastInsertId() + if err != nil { + return err + } + } else { + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) && sqliteErr.Code == sqlite3.ErrConstraint && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { + // address exists: overflowed account record: find addrid + err = selectAcctStmt.QueryRowContext(ctx, balance.Address[:]).Scan(&rowID) + if err != nil { + return err + } + } else { + return err + } + } + + // write resources + for aidx := range balance.Resources { + var result sql.Result + result, err = insertRscStmt.ExecContext(ctx, rowID, aidx, balance.EncodedResources[aidx]) + if err != nil { + return err + } + var aff int64 + aff, err = result.RowsAffected() + if err != nil { + return err + } + if aff != 1 { + return fmt.Errorf("number of affected record in insert was expected to be one, but was %d", aff) + } + } + } + return nil +} + +// WriteCatchpointStagingHashes inserts all the account hashes in the provided array into the catchpoint pending hashes table catchpointpendinghashes. +func (cw *catchpointWriter) WriteCatchpointStagingHashes(ctx context.Context, bals []NormalizedAccountBalance) error { + insertStmt, err := cw.e.PrepareContext(ctx, "INSERT INTO catchpointpendinghashes(data) VALUES(?)") + if err != nil { + return err + } + + for _, balance := range bals { + for _, hash := range balance.AccountHashes { + result, err := insertStmt.ExecContext(ctx, hash[:]) + if err != nil { + return err + } + + aff, err := result.RowsAffected() + if err != nil { + return err + } + if aff != 1 { + return fmt.Errorf("number of affected record in insert was expected to be one, but was %d", aff) + } + } + } + return nil +} + +// WriteCatchpointStagingCreatable inserts all the creatables in the provided array into the catchpoint asset creator staging table catchpointassetcreators. +// note that we cannot insert the resources here : in order to insert the resources, we need the rowid of the accountbase entry. This is being inserted by +// writeCatchpointStagingBalances via a separate go-routine. +func (cw *catchpointWriter) WriteCatchpointStagingCreatable(ctx context.Context, bals []NormalizedAccountBalance) error { + var insertCreatorsStmt *sql.Stmt + var err error + insertCreatorsStmt, err = cw.e.PrepareContext(ctx, "INSERT INTO catchpointassetcreators(asset, creator, ctype) VALUES(?, ?, ?)") + if err != nil { + return err + } + defer insertCreatorsStmt.Close() + + for _, balance := range bals { + for aidx, resData := range balance.Resources { + if resData.IsOwning() { + // determine if it's an asset + if resData.IsAsset() { + _, err := insertCreatorsStmt.ExecContext(ctx, aidx, balance.Address[:], basics.AssetCreatable) + if err != nil { + return err + } + } + // determine if it's an application + if resData.IsApp() { + _, err := insertCreatorsStmt.ExecContext(ctx, aidx, balance.Address[:], basics.AppCreatable) + if err != nil { + return err + } + } + } + } + } + return nil +} + +// WriteCatchpointStagingKVs inserts all the KVs in the provided array into the +// catchpoint kvstore staging table catchpointkvstore, and their hashes to the pending +func (cw *catchpointWriter) WriteCatchpointStagingKVs(ctx context.Context, keys [][]byte, values [][]byte, hashes [][]byte) error { + insertKV, err := cw.e.PrepareContext(ctx, "INSERT INTO catchpointkvstore(key, value) VALUES(?, ?)") + if err != nil { + return err + } + defer insertKV.Close() + + insertHash, err := cw.e.PrepareContext(ctx, "INSERT INTO catchpointpendinghashes(data) VALUES(?)") + if err != nil { + return err + } + defer insertHash.Close() + + for i := 0; i < len(keys); i++ { + _, err := insertKV.ExecContext(ctx, keys[i], values[i]) + if err != nil { + return err + } + + _, err = insertHash.ExecContext(ctx, hashes[i]) + if err != nil { + return err + } + } + return nil +} + +func (cw *catchpointWriter) ResetCatchpointStagingBalances(ctx context.Context, newCatchup bool) (err error) { + s := []string{ + "DROP TABLE IF EXISTS catchpointbalances", + "DROP TABLE IF EXISTS catchpointassetcreators", + "DROP TABLE IF EXISTS catchpointaccounthashes", + "DROP TABLE IF EXISTS catchpointpendinghashes", + "DROP TABLE IF EXISTS catchpointresources", + "DROP TABLE IF EXISTS catchpointkvstore", + "DELETE FROM accounttotals where id='catchpointStaging'", + } + + if newCatchup { + // SQLite has no way to rename an existing index. So, we need + // to cook up a fresh name for the index, which will be kept + // around after we rename the table from "catchpointbalances" + // to "accountbase". To construct a unique index name, we + // use the current time. + // Apply the same logic to + now := time.Now().UnixNano() + idxnameBalances := fmt.Sprintf("onlineaccountbals_idx_%d", now) + idxnameAddress := fmt.Sprintf("accountbase_address_idx_%d", now) + + s = append(s, + "CREATE TABLE IF NOT EXISTS catchpointassetcreators (asset integer primary key, creator blob, ctype integer)", + "CREATE TABLE IF NOT EXISTS catchpointbalances (addrid INTEGER PRIMARY KEY NOT NULL, address blob NOT NULL, data blob, normalizedonlinebalance INTEGER)", + "CREATE TABLE IF NOT EXISTS catchpointpendinghashes (data blob)", + "CREATE TABLE IF NOT EXISTS catchpointaccounthashes (id integer primary key, data blob)", + "CREATE TABLE IF NOT EXISTS catchpointresources (addrid INTEGER NOT NULL, aidx INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY (addrid, aidx) ) WITHOUT ROWID", + "CREATE TABLE IF NOT EXISTS catchpointkvstore (key blob primary key, value blob)", + + createNormalizedOnlineBalanceIndex(idxnameBalances, "catchpointbalances"), // should this be removed ? + createUniqueAddressBalanceIndex(idxnameAddress, "catchpointbalances"), + ) + } + + for _, stmt := range s { + _, err = cw.e.Exec(stmt) + if err != nil { + return err + } + } + + return nil +} + +// ApplyCatchpointStagingBalances switches the staged catchpoint catchup tables onto the actual +// tables and update the correct balance round. This is the final step in switching onto the new catchpoint round. +func (cw *catchpointWriter) ApplyCatchpointStagingBalances(ctx context.Context, balancesRound basics.Round, merkleRootRound basics.Round) (err error) { + stmts := []string{ + "DROP TABLE IF EXISTS accountbase", + "DROP TABLE IF EXISTS assetcreators", + "DROP TABLE IF EXISTS accounthashes", + "DROP TABLE IF EXISTS resources", + "DROP TABLE IF EXISTS kvstore", + + "ALTER TABLE catchpointbalances RENAME TO accountbase", + "ALTER TABLE catchpointassetcreators RENAME TO assetcreators", + "ALTER TABLE catchpointaccounthashes RENAME TO accounthashes", + "ALTER TABLE catchpointresources RENAME TO resources", + "ALTER TABLE catchpointkvstore RENAME TO kvstore", + } + + for _, stmt := range stmts { + _, err = cw.e.Exec(stmt) + if err != nil { + return err + } + } + + _, err = cw.e.Exec("INSERT OR REPLACE INTO acctrounds(id, rnd) VALUES('acctbase', ?)", balancesRound) + if err != nil { + return err + } + + _, err = cw.e.Exec("INSERT OR REPLACE INTO acctrounds(id, rnd) VALUES('hashbase', ?)", merkleRootRound) + if err != nil { + return err + } + + return +} + +// CreateCatchpointStagingHashesIndex creates an index on catchpointpendinghashes to allow faster scanning according to the hash order +func (cw *catchpointWriter) CreateCatchpointStagingHashesIndex(ctx context.Context) (err error) { + _, err = cw.e.ExecContext(ctx, "CREATE INDEX IF NOT EXISTS catchpointpendinghashesidx ON catchpointpendinghashes(data)") + if err != nil { + return + } + return +} + +// 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 (crw *catchpointReaderWriter) DeleteStoredCatchpoints(ctx context.Context, dbDirectory string) (err error) { + catchpointsFilesChunkSize := 50 + for { + fileNames, err := crw.GetOldestCatchpointFiles(ctx, catchpointsFilesChunkSize, 0) + if err != nil { + return err + } + if len(fileNames) == 0 { + break + } + + for round, fileName := range fileNames { + err = RemoveSingleCatchpointFileFromDisk(dbDirectory, fileName) + if err != nil { + return err + } + // clear the entry from the database + err = crw.StoreCatchpoint(ctx, round, "", "", 0) + if err != nil { + return err + } + } + } + return nil +} + +// RemoveSingleCatchpointFileFromDisk removes a single catchpoint file from the disk. this function does not leave empty directories +func RemoveSingleCatchpointFileFromDisk(dbDirectory, fileToDelete string) (err error) { + absCatchpointFileName := filepath.Join(dbDirectory, fileToDelete) + err = os.Remove(absCatchpointFileName) + if err == nil || os.IsNotExist(err) { + // it's ok if the file doesn't exist. + err = nil + } else { + // we can't delete the file, abort - + return fmt.Errorf("unable to delete old catchpoint file '%s' : %v", absCatchpointFileName, err) + } + splitedDirName := strings.Split(fileToDelete, string(os.PathSeparator)) + + var subDirectoriesToScan []string + //build a list of all the subdirs + currentSubDir := "" + for _, element := range splitedDirName { + currentSubDir = filepath.Join(currentSubDir, element) + subDirectoriesToScan = append(subDirectoriesToScan, currentSubDir) + } + + // iterating over the list of directories. starting from the sub dirs and moving up. + // skipping the file itself. + for i := len(subDirectoriesToScan) - 2; i >= 0; i-- { + absSubdir := filepath.Join(dbDirectory, subDirectoriesToScan[i]) + if _, err := os.Stat(absSubdir); os.IsNotExist(err) { + continue + } + + isEmpty, err := isDirEmpty(absSubdir) + if err != nil { + return fmt.Errorf("unable to read old catchpoint directory '%s' : %v", subDirectoriesToScan[i], err) + } + if isEmpty { + err = os.Remove(absSubdir) + if err != nil { + if os.IsNotExist(err) { + continue + } + return fmt.Errorf("unable to delete old catchpoint directory '%s' : %v", subDirectoriesToScan[i], err) + } + } + } + + return nil +} diff --git a/ledger/store/catchpoint_test.go b/ledger/store/catchpoint_test.go new file mode 100644 index 0000000000..d4da07ae16 --- /dev/null +++ b/ledger/store/catchpoint_test.go @@ -0,0 +1,128 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "context" + "crypto/rand" + "testing" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + storetesting "github.com/algorand/go-algorand/ledger/store/testing" + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" +) + +// Test functions operating on catchpointfirststageinfo table. +func TestCatchpointFirstStageInfoTable(t *testing.T) { + partitiontest.PartitionTest(t) + + dbs, _ := storetesting.DbOpenTest(t, true) + defer dbs.Close() + + ctx := context.Background() + + err := accountsCreateCatchpointFirstStageInfoTable(ctx, dbs.Wdb.Handle) + require.NoError(t, err) + + crw := NewCatchpointSQLReaderWriter(dbs.Wdb.Handle) + + for _, round := range []basics.Round{4, 6, 8} { + info := CatchpointFirstStageInfo{ + TotalAccounts: uint64(round) * 10, + } + err = crw.InsertOrReplaceCatchpointFirstStageInfo(ctx, round, &info) + require.NoError(t, err) + } + + for _, round := range []basics.Round{4, 6, 8} { + info, exists, err := crw.SelectCatchpointFirstStageInfo(ctx, round) + require.NoError(t, err) + require.True(t, exists) + + infoExpected := CatchpointFirstStageInfo{ + TotalAccounts: uint64(round) * 10, + } + require.Equal(t, infoExpected, info) + } + + _, exists, err := crw.SelectCatchpointFirstStageInfo(ctx, 7) + require.NoError(t, err) + require.False(t, exists) + + rounds, err := crw.SelectOldCatchpointFirstStageInfoRounds(ctx, 6) + require.NoError(t, err) + require.Equal(t, []basics.Round{4, 6}, rounds) + + err = crw.DeleteOldCatchpointFirstStageInfo(ctx, 6) + require.NoError(t, err) + + rounds, err = crw.SelectOldCatchpointFirstStageInfoRounds(ctx, 9) + require.NoError(t, err) + require.Equal(t, []basics.Round{8}, rounds) +} + +func TestUnfinishedCatchpointsTable(t *testing.T) { + partitiontest.PartitionTest(t) + + dbs, _ := storetesting.DbOpenTest(t, true) + defer dbs.Close() + + cts := NewCatchpointSQLReaderWriter(dbs.Wdb.Handle) + + err := accountsCreateUnfinishedCatchpointsTable( + context.Background(), dbs.Wdb.Handle) + require.NoError(t, err) + + var d3 crypto.Digest + rand.Read(d3[:]) + err = cts.InsertUnfinishedCatchpoint(context.Background(), 3, d3) + require.NoError(t, err) + + var d5 crypto.Digest + rand.Read(d5[:]) + err = cts.InsertUnfinishedCatchpoint(context.Background(), 5, d5) + require.NoError(t, err) + + ret, err := cts.SelectUnfinishedCatchpoints(context.Background()) + require.NoError(t, err) + expected := []UnfinishedCatchpointRecord{ + { + Round: 3, + BlockHash: d3, + }, + { + Round: 5, + BlockHash: d5, + }, + } + require.Equal(t, expected, ret) + + err = cts.DeleteUnfinishedCatchpoint(context.Background(), 3) + require.NoError(t, err) + + ret, err = cts.SelectUnfinishedCatchpoints(context.Background()) + require.NoError(t, err) + expected = []UnfinishedCatchpointRecord{ + { + Round: 5, + BlockHash: d5, + }, + } + require.Equal(t, expected, ret) +} diff --git a/ledger/store/data.go b/ledger/store/data.go new file mode 100644 index 0000000000..d09771bd34 --- /dev/null +++ b/ledger/store/data.go @@ -0,0 +1,879 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "context" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/crypto/merklesignature" + "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/ledgercore" + "github.com/algorand/go-algorand/protocol" +) + +// BaseAccountData is the base struct used to store account data +type BaseAccountData struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Status basics.Status `codec:"a"` + MicroAlgos basics.MicroAlgos `codec:"b"` + RewardsBase uint64 `codec:"c"` + RewardedMicroAlgos basics.MicroAlgos `codec:"d"` + AuthAddr basics.Address `codec:"e"` + TotalAppSchemaNumUint uint64 `codec:"f"` + TotalAppSchemaNumByteSlice uint64 `codec:"g"` + TotalExtraAppPages uint32 `codec:"h"` + TotalAssetParams uint64 `codec:"i"` + TotalAssets uint64 `codec:"j"` + TotalAppParams uint64 `codec:"k"` + TotalAppLocalStates uint64 `codec:"l"` + TotalBoxes uint64 `codec:"m"` + TotalBoxBytes uint64 `codec:"n"` + + BaseVotingData + + // UpdateRound is the round that modified this account data last. Since we want all the nodes to have the exact same + // value for this field, we'll be setting the value of this field to zero *before* the EnableAccountDataResourceSeparation + // consensus parameter is being set. Once the above consensus takes place, this field would be populated with the + // correct round number. + UpdateRound uint64 `codec:"z"` +} + +// ResourceFlags are bitmask used to indicate which portions ofa resources are used. +type ResourceFlags uint8 + +const ( + // ResourceFlagsHolding indicates "Holding" + ResourceFlagsHolding ResourceFlags = 0 + // ResourceFlagsNotHolding indicates "Not Holding" + ResourceFlagsNotHolding ResourceFlags = 1 + // ResourceFlagsOwnership indicates "Ownerhip" + ResourceFlagsOwnership ResourceFlags = 2 + // ResourceFlagsEmptyAsset indicates "Empty Asset" + ResourceFlagsEmptyAsset ResourceFlags = 4 + // ResourceFlagsEmptyApp indicates "Empty App" + ResourceFlagsEmptyApp ResourceFlags = 8 +) + +// +// Resource flags interpretation: +// +// ResourceFlagsHolding - the resource contains the holding of asset/app. +// ResourceFlagsNotHolding - the resource is completely empty. This state should not be persisted. +// ResourceFlagsOwnership - the resource contains the asset parameter or application parameters. +// ResourceFlagsEmptyAsset - this is an asset resource, and it is empty. +// ResourceFlagsEmptyApp - this is an app resource, and it is empty. + +// ResourcesData holds the resource data that will be stored. +type ResourcesData struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + // asset parameters ( basics.AssetParams ) + Total uint64 `codec:"a"` + Decimals uint32 `codec:"b"` + DefaultFrozen bool `codec:"c"` + UnitName string `codec:"d"` + AssetName string `codec:"e"` + URL string `codec:"f"` + MetadataHash [32]byte `codec:"g"` + Manager basics.Address `codec:"h"` + Reserve basics.Address `codec:"i"` + Freeze basics.Address `codec:"j"` + Clawback basics.Address `codec:"k"` + + // asset holding ( basics.AssetHolding ) + Amount uint64 `codec:"l"` + Frozen bool `codec:"m"` + + // application local state ( basics.AppLocalState ) + SchemaNumUint uint64 `codec:"n"` + SchemaNumByteSlice uint64 `codec:"o"` + KeyValue basics.TealKeyValue `codec:"p"` + + // application global params ( basics.AppParams ) + ApprovalProgram []byte `codec:"q,allocbound=config.MaxAvailableAppProgramLen"` + ClearStateProgram []byte `codec:"r,allocbound=config.MaxAvailableAppProgramLen"` + GlobalState basics.TealKeyValue `codec:"s"` + LocalStateSchemaNumUint uint64 `codec:"t"` + LocalStateSchemaNumByteSlice uint64 `codec:"u"` + GlobalStateSchemaNumUint uint64 `codec:"v"` + GlobalStateSchemaNumByteSlice uint64 `codec:"w"` + ExtraProgramPages uint32 `codec:"x"` + + // ResourceFlags helps to identify which portions of this structure should be used; in particular, it + // helps to provide a marker - i.e. whether the account was, for instance, opted-in for the asset compared + // to just being the owner of the asset. A comparison against the empty structure doesn't work here - + // since both the holdings and the parameters are allowed to be all at their default values. + ResourceFlags ResourceFlags `codec:"y"` + + // UpdateRound is the round that modified this resource last. Since we want all the nodes to have the exact same + // value for this field, we'll be setting the value of this field to zero *before* the EnableAccountDataResourceSeparation + // consensus parameter is being set. Once the above consensus takes place, this field would be populated with the + // correct round number. + UpdateRound uint64 `codec:"z"` +} + +// BaseVotingData is the base struct used to store voting data +type BaseVotingData struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + VoteID crypto.OneTimeSignatureVerifier `codec:"A"` + SelectionID crypto.VRFVerifier `codec:"B"` + VoteFirstValid basics.Round `codec:"C"` + VoteLastValid basics.Round `codec:"D"` + VoteKeyDilution uint64 `codec:"E"` + StateProofID merklesignature.Commitment `codec:"F"` +} + +// BaseOnlineAccountData is the base struct used to store online account data +type BaseOnlineAccountData struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + BaseVotingData + + MicroAlgos basics.MicroAlgos `codec:"Y"` + RewardsBase uint64 `codec:"Z"` +} + +// PersistedKVData represents the stored entry behind a application boxed key/value. +type PersistedKVData struct { + // kv value + Value []byte + // the round number that is associated with the kv value. This field is the corresponding one to the round field + // in persistedAccountData, and serves the same purpose. + Round basics.Round +} + +// PersistedAccountData is used for representing a single account stored on the disk. In addition to the +// basics.AccountData, it also stores complete referencing information used to maintain the base accounts +// list. +type PersistedAccountData struct { + // The address of the account. In contrasts to maps, having this value explicitly here allows us to use this + // data structure in queues directly, without "attaching" the address as the address as the map key. + Addr basics.Address + // The underlaying account data + AccountData BaseAccountData + // The rowid, when available. If the entry was loaded from the disk, then we have the rowid for it. Entries + // that doesn't have rowid ( hence, rowid == 0 ) represent either deleted accounts or non-existing accounts. + Rowid int64 + // the round number that is associated with the accountData. This field is needed so that we can maintain a correct + // lruAccounts cache. We use it to ensure that the entries on the lruAccounts.accountsList are the latest ones. + // this becomes an issue since while we attempt to write an update to disk, we might be reading an entry and placing + // it on the lruAccounts.pendingAccounts; The commitRound doesn't attempt to flush the pending accounts, but rather + // just write the latest ( which is correct ) to the lruAccounts.accountsList. later on, during on newBlockImpl, we + // want to ensure that the "real" written value isn't being overridden by the value from the pending accounts. + Round basics.Round +} + +// PersistedResourcesData is exported view of persistedResourcesData +type PersistedResourcesData struct { + // addrid is the rowid of the account address that holds this resource. + // it is used in update/delete operations so must be filled for existing records. + // resolution is a multi stage process: + // - baseResources cache might have valid entries + // - baseAccount cache might have an entry for the address with rowid set + // - when loading non-cached resources in resourcesLoadOld + // - when creating new accounts in accountsNewRound + Addrid int64 + // creatable index + Aidx basics.CreatableIndex + // actual resource data + Data ResourcesData + // the round number that is associated with the resourcesData. This field is the corresponding one to the round field + // in persistedAccountData, and serves the same purpose. + Round basics.Round +} + +// PersistedOnlineAccountData is exported view of persistedOnlineAccountData +type PersistedOnlineAccountData struct { + Addr basics.Address + AccountData BaseOnlineAccountData + Rowid int64 + // the round number that is associated with the baseOnlineAccountData. This field is the corresponding one to the round field + // in persistedAccountData, and serves the same purpose. This value comes from account rounds table and correspond to + // the last trackers db commit round. + Round basics.Round + // the round number that the online account is for, i.e. account state change round. + UpdRound basics.Round +} + +// TxTailRound contains the information about a single round of transactions. +// The TxnIDs and LastValid would both be of the same length, and are stored +// in that way for efficient message=pack encoding. The Leases would point to the +// respective transaction index. Note that this isn’t optimized for storing +// leases, as leases are extremely rare. +type TxTailRound struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + TxnIDs []transactions.Txid `codec:"i,allocbound=-"` + LastValid []basics.Round `codec:"v,allocbound=-"` + Leases []TxTailRoundLease `codec:"l,allocbound=-"` + Hdr bookkeeping.BlockHeader `codec:"h,allocbound=-"` +} + +// TxTailRoundLease is used as part of txTailRound for storing +// a single lease. +type TxTailRoundLease struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Sender basics.Address `codec:"s"` + Lease [32]byte `codec:"l,allocbound=-"` + TxnIdx uint64 `code:"i"` //!-- index of the entry in TxnIDs/LastValid +} + +// AccountResource returns the corresponding account resource data based on the type of resource. +func (prd *PersistedResourcesData) AccountResource() ledgercore.AccountResource { + var ret ledgercore.AccountResource + if prd.Data.IsAsset() { + if prd.Data.IsHolding() { + holding := prd.Data.GetAssetHolding() + ret.AssetHolding = &holding + } + if prd.Data.IsOwning() { + assetParams := prd.Data.GetAssetParams() + ret.AssetParams = &assetParams + } + } + if prd.Data.IsApp() { + if prd.Data.IsHolding() { + localState := prd.Data.GetAppLocalState() + ret.AppLocalState = &localState + } + if prd.Data.IsOwning() { + appParams := prd.Data.GetAppParams() + ret.AppParams = &appParams + } + } + return ret +} + +// NormalizedOnlineBalance getter for normalized online balance. +func (ba *BaseAccountData) NormalizedOnlineBalance(proto config.ConsensusParams) uint64 { + return basics.NormalizedOnlineAccountBalance(ba.Status, ba.RewardsBase, ba.MicroAlgos, proto) +} + +// SetCoreAccountData setter for core account data. +func (ba *BaseAccountData) SetCoreAccountData(ad *ledgercore.AccountData) { + ba.Status = ad.Status + ba.MicroAlgos = ad.MicroAlgos + ba.RewardsBase = ad.RewardsBase + ba.RewardedMicroAlgos = ad.RewardedMicroAlgos + ba.AuthAddr = ad.AuthAddr + ba.TotalAppSchemaNumUint = ad.TotalAppSchema.NumUint + ba.TotalAppSchemaNumByteSlice = ad.TotalAppSchema.NumByteSlice + ba.TotalExtraAppPages = ad.TotalExtraAppPages + ba.TotalAssetParams = ad.TotalAssetParams + ba.TotalAssets = ad.TotalAssets + ba.TotalAppParams = ad.TotalAppParams + ba.TotalAppLocalStates = ad.TotalAppLocalStates + ba.TotalBoxes = ad.TotalBoxes + ba.TotalBoxBytes = ad.TotalBoxBytes + + ba.BaseVotingData.SetCoreAccountData(ad) +} + +// SetAccountData setter for account data. +func (ba *BaseAccountData) SetAccountData(ad *basics.AccountData) { + ba.Status = ad.Status + ba.MicroAlgos = ad.MicroAlgos + ba.RewardsBase = ad.RewardsBase + ba.RewardedMicroAlgos = ad.RewardedMicroAlgos + ba.AuthAddr = ad.AuthAddr + ba.TotalAppSchemaNumUint = ad.TotalAppSchema.NumUint + ba.TotalAppSchemaNumByteSlice = ad.TotalAppSchema.NumByteSlice + ba.TotalExtraAppPages = ad.TotalExtraAppPages + ba.TotalAssetParams = uint64(len(ad.AssetParams)) + ba.TotalAssets = uint64(len(ad.Assets)) + ba.TotalAppParams = uint64(len(ad.AppParams)) + ba.TotalAppLocalStates = uint64(len(ad.AppLocalStates)) + ba.TotalBoxes = ad.TotalBoxes + ba.TotalBoxBytes = ad.TotalBoxBytes + + ba.BaseVotingData.VoteID = ad.VoteID + ba.BaseVotingData.SelectionID = ad.SelectionID + ba.BaseVotingData.StateProofID = ad.StateProofID + ba.BaseVotingData.VoteFirstValid = ad.VoteFirstValid + ba.BaseVotingData.VoteLastValid = ad.VoteLastValid + ba.BaseVotingData.VoteKeyDilution = ad.VoteKeyDilution +} + +// GetLedgerCoreAccountData getter for account data. +func (ba *BaseAccountData) GetLedgerCoreAccountData() ledgercore.AccountData { + return ledgercore.AccountData{ + AccountBaseData: ba.GetLedgerCoreAccountBaseData(), + VotingData: ba.GetLedgerCoreVotingData(), + } +} + +// GetLedgerCoreAccountBaseData getter for account base data. +func (ba *BaseAccountData) GetLedgerCoreAccountBaseData() ledgercore.AccountBaseData { + return ledgercore.AccountBaseData{ + Status: ba.Status, + MicroAlgos: ba.MicroAlgos, + RewardsBase: ba.RewardsBase, + RewardedMicroAlgos: ba.RewardedMicroAlgos, + AuthAddr: ba.AuthAddr, + TotalAppSchema: basics.StateSchema{ + NumUint: ba.TotalAppSchemaNumUint, + NumByteSlice: ba.TotalAppSchemaNumByteSlice, + }, + TotalExtraAppPages: ba.TotalExtraAppPages, + TotalAppParams: ba.TotalAppParams, + TotalAppLocalStates: ba.TotalAppLocalStates, + TotalAssetParams: ba.TotalAssetParams, + TotalAssets: ba.TotalAssets, + TotalBoxes: ba.TotalBoxes, + TotalBoxBytes: ba.TotalBoxBytes, + } +} + +// GetLedgerCoreVotingData getter for voting data. +func (ba *BaseAccountData) GetLedgerCoreVotingData() ledgercore.VotingData { + return ledgercore.VotingData{ + VoteID: ba.VoteID, + SelectionID: ba.SelectionID, + StateProofID: ba.StateProofID, + VoteFirstValid: ba.VoteFirstValid, + VoteLastValid: ba.VoteLastValid, + VoteKeyDilution: ba.VoteKeyDilution, + } +} + +// GetAccountData getter for account data. +func (ba *BaseAccountData) GetAccountData() basics.AccountData { + return basics.AccountData{ + Status: ba.Status, + MicroAlgos: ba.MicroAlgos, + RewardsBase: ba.RewardsBase, + RewardedMicroAlgos: ba.RewardedMicroAlgos, + AuthAddr: ba.AuthAddr, + TotalAppSchema: basics.StateSchema{ + NumUint: ba.TotalAppSchemaNumUint, + NumByteSlice: ba.TotalAppSchemaNumByteSlice, + }, + TotalExtraAppPages: ba.TotalExtraAppPages, + TotalBoxes: ba.TotalBoxes, + TotalBoxBytes: ba.TotalBoxBytes, + + VoteID: ba.VoteID, + SelectionID: ba.SelectionID, + StateProofID: ba.StateProofID, + VoteFirstValid: ba.VoteFirstValid, + VoteLastValid: ba.VoteLastValid, + VoteKeyDilution: ba.VoteKeyDilution, + } +} + +// IsEmpty return true if any of the fields other then the UpdateRound are non-zero. +func (ba *BaseAccountData) IsEmpty() bool { + return ba.Status == 0 && + ba.MicroAlgos.Raw == 0 && + ba.RewardsBase == 0 && + ba.RewardedMicroAlgos.Raw == 0 && + ba.AuthAddr.IsZero() && + ba.TotalAppSchemaNumUint == 0 && + ba.TotalAppSchemaNumByteSlice == 0 && + ba.TotalExtraAppPages == 0 && + ba.TotalAssetParams == 0 && + ba.TotalAssets == 0 && + ba.TotalAppParams == 0 && + ba.TotalAppLocalStates == 0 && + ba.TotalBoxes == 0 && + ba.TotalBoxBytes == 0 && + ba.BaseVotingData.IsEmpty() +} + +// IsEmpty returns true if all of the fields are zero. +func (bv BaseVotingData) IsEmpty() bool { + return bv == BaseVotingData{} +} + +// SetCoreAccountData initializes baseVotingData from ledgercore.AccountData +func (bv *BaseVotingData) SetCoreAccountData(ad *ledgercore.AccountData) { + bv.VoteID = ad.VoteID + bv.SelectionID = ad.SelectionID + bv.StateProofID = ad.StateProofID + bv.VoteFirstValid = ad.VoteFirstValid + bv.VoteLastValid = ad.VoteLastValid + bv.VoteKeyDilution = ad.VoteKeyDilution +} + +// IsVotingEmpty checks if voting data fields are empty +func (bo *BaseOnlineAccountData) IsVotingEmpty() bool { + return bo.BaseVotingData.IsEmpty() +} + +// IsEmpty return true if any of the fields are non-zero. +func (bo *BaseOnlineAccountData) IsEmpty() bool { + return bo.IsVotingEmpty() && + bo.MicroAlgos.Raw == 0 && + bo.RewardsBase == 0 +} + +// GetOnlineAccount returns ledgercore.OnlineAccount for top online accounts / voters +// TODO: unify +func (bo *BaseOnlineAccountData) GetOnlineAccount(addr basics.Address, normBalance uint64) ledgercore.OnlineAccount { + return ledgercore.OnlineAccount{ + Address: addr, + MicroAlgos: bo.MicroAlgos, + RewardsBase: bo.RewardsBase, + NormalizedOnlineBalance: normBalance, + VoteFirstValid: bo.VoteFirstValid, + VoteLastValid: bo.VoteLastValid, + StateProofID: bo.StateProofID, + } +} + +// GetOnlineAccountData returns basics.OnlineAccountData for lookup agreement +// TODO: unify with GetOnlineAccount/ledgercore.OnlineAccount +func (bo *BaseOnlineAccountData) GetOnlineAccountData(proto config.ConsensusParams, rewardsLevel uint64) ledgercore.OnlineAccountData { + microAlgos, _, _ := basics.WithUpdatedRewards( + proto, basics.Online, bo.MicroAlgos, basics.MicroAlgos{}, bo.RewardsBase, rewardsLevel, + ) + + return ledgercore.OnlineAccountData{ + MicroAlgosWithRewards: microAlgos, + VotingData: ledgercore.VotingData{ + VoteID: bo.VoteID, + SelectionID: bo.SelectionID, + StateProofID: bo.StateProofID, + VoteFirstValid: bo.VoteFirstValid, + VoteLastValid: bo.VoteLastValid, + VoteKeyDilution: bo.VoteKeyDilution, + }, + } +} + +// NormalizedOnlineBalance getter for normalized online balance. +func (bo *BaseOnlineAccountData) NormalizedOnlineBalance(proto config.ConsensusParams) uint64 { + return basics.NormalizedOnlineAccountBalance(basics.Online, bo.RewardsBase, bo.MicroAlgos, proto) +} + +// SetCoreAccountData setter for core account data. +func (bo *BaseOnlineAccountData) SetCoreAccountData(ad *ledgercore.AccountData) { + bo.BaseVotingData.SetCoreAccountData(ad) + + // MicroAlgos/RewardsBase are updated by the evaluator when accounts are touched + bo.MicroAlgos = ad.MicroAlgos + bo.RewardsBase = ad.RewardsBase +} + +// MakeResourcesData returns a new empty instance of resourcesData. +// Using this constructor method is necessary because of the ResourceFlags field. +// An optional rnd args sets UpdateRound +func MakeResourcesData(rnd uint64) ResourcesData { + return ResourcesData{ResourceFlags: ResourceFlagsNotHolding, UpdateRound: rnd} +} + +// IsHolding returns true if the resource flag is ResourceFlagsHolding +func (rd *ResourcesData) IsHolding() bool { + return (rd.ResourceFlags & ResourceFlagsNotHolding) == ResourceFlagsHolding +} + +// IsOwning returns true if the resource flag is ResourceFlagsOwnership +func (rd *ResourcesData) IsOwning() bool { + return (rd.ResourceFlags & ResourceFlagsOwnership) == ResourceFlagsOwnership +} + +// IsEmpty returns true if the resource flag is not an app or asset. +func (rd *ResourcesData) IsEmpty() bool { + return !rd.IsApp() && !rd.IsAsset() +} + +// IsEmptyAppFields returns true if the app fields are empty. +func (rd *ResourcesData) IsEmptyAppFields() bool { + return rd.SchemaNumUint == 0 && + rd.SchemaNumByteSlice == 0 && + len(rd.KeyValue) == 0 && + len(rd.ApprovalProgram) == 0 && + len(rd.ClearStateProgram) == 0 && + len(rd.GlobalState) == 0 && + rd.LocalStateSchemaNumUint == 0 && + rd.LocalStateSchemaNumByteSlice == 0 && + rd.GlobalStateSchemaNumUint == 0 && + rd.GlobalStateSchemaNumByteSlice == 0 && + rd.ExtraProgramPages == 0 +} + +// IsApp returns true if the flag is ResourceFlagsEmptyApp and the fields are not empty. +func (rd *ResourcesData) IsApp() bool { + if (rd.ResourceFlags & ResourceFlagsEmptyApp) == ResourceFlagsEmptyApp { + return true + } + return !rd.IsEmptyAppFields() +} + +// IsEmptyAssetFields returns true if the asset fields are empty. +func (rd *ResourcesData) IsEmptyAssetFields() bool { + return rd.Amount == 0 && + !rd.Frozen && + rd.Total == 0 && + rd.Decimals == 0 && + !rd.DefaultFrozen && + rd.UnitName == "" && + rd.AssetName == "" && + rd.URL == "" && + rd.MetadataHash == [32]byte{} && + rd.Manager.IsZero() && + rd.Reserve.IsZero() && + rd.Freeze.IsZero() && + rd.Clawback.IsZero() +} + +// IsAsset returns true if the flag is ResourceFlagsEmptyAsset and the fields are not empty. +func (rd *ResourcesData) IsAsset() bool { + if (rd.ResourceFlags & ResourceFlagsEmptyAsset) == ResourceFlagsEmptyAsset { + return true + } + return !rd.IsEmptyAssetFields() +} + +// ClearAssetParams clears the asset params. +func (rd *ResourcesData) ClearAssetParams() { + rd.Total = 0 + rd.Decimals = 0 + rd.DefaultFrozen = false + rd.UnitName = "" + rd.AssetName = "" + rd.URL = "" + rd.MetadataHash = basics.Address{} + rd.Manager = basics.Address{} + rd.Reserve = basics.Address{} + rd.Freeze = basics.Address{} + rd.Clawback = basics.Address{} + hadHolding := (rd.ResourceFlags & ResourceFlagsNotHolding) == ResourceFlagsHolding + rd.ResourceFlags -= rd.ResourceFlags & ResourceFlagsOwnership + rd.ResourceFlags &= ^ResourceFlagsEmptyAsset + if rd.IsEmptyAssetFields() && hadHolding { + rd.ResourceFlags |= ResourceFlagsEmptyAsset + } +} + +// SetAssetParams setter for asset params. +func (rd *ResourcesData) SetAssetParams(ap basics.AssetParams, haveHoldings bool) { + rd.Total = ap.Total + rd.Decimals = ap.Decimals + rd.DefaultFrozen = ap.DefaultFrozen + rd.UnitName = ap.UnitName + rd.AssetName = ap.AssetName + rd.URL = ap.URL + rd.MetadataHash = ap.MetadataHash + rd.Manager = ap.Manager + rd.Reserve = ap.Reserve + rd.Freeze = ap.Freeze + rd.Clawback = ap.Clawback + rd.ResourceFlags |= ResourceFlagsOwnership + if !haveHoldings { + rd.ResourceFlags |= ResourceFlagsNotHolding + } + rd.ResourceFlags &= ^ResourceFlagsEmptyAsset + if rd.IsEmptyAssetFields() { + rd.ResourceFlags |= ResourceFlagsEmptyAsset + } +} + +// GetAssetParams getter for asset params. +func (rd *ResourcesData) GetAssetParams() basics.AssetParams { + ap := basics.AssetParams{ + Total: rd.Total, + Decimals: rd.Decimals, + DefaultFrozen: rd.DefaultFrozen, + UnitName: rd.UnitName, + AssetName: rd.AssetName, + URL: rd.URL, + MetadataHash: rd.MetadataHash, + Manager: rd.Manager, + Reserve: rd.Reserve, + Freeze: rd.Freeze, + Clawback: rd.Clawback, + } + return ap +} + +// ClearAssetHolding clears asset holding. +func (rd *ResourcesData) ClearAssetHolding() { + rd.Amount = 0 + rd.Frozen = false + + rd.ResourceFlags |= ResourceFlagsNotHolding + hadParams := (rd.ResourceFlags & ResourceFlagsOwnership) == ResourceFlagsOwnership + if hadParams && rd.IsEmptyAssetFields() { + rd.ResourceFlags |= ResourceFlagsEmptyAsset + } else { + rd.ResourceFlags &= ^ResourceFlagsEmptyAsset + } +} + +// SetAssetHolding setter for asset holding. +func (rd *ResourcesData) SetAssetHolding(ah basics.AssetHolding) { + rd.Amount = ah.Amount + rd.Frozen = ah.Frozen + rd.ResourceFlags &= ^(ResourceFlagsNotHolding + ResourceFlagsEmptyAsset) + // ResourceFlagsHolding is set implicitly since it is zero + if rd.IsEmptyAssetFields() { + rd.ResourceFlags |= ResourceFlagsEmptyAsset + } +} + +// GetAssetHolding getter for asset holding. +func (rd *ResourcesData) GetAssetHolding() basics.AssetHolding { + return basics.AssetHolding{ + Amount: rd.Amount, + Frozen: rd.Frozen, + } +} + +// ClearAppLocalState clears app local state. +func (rd *ResourcesData) ClearAppLocalState() { + rd.SchemaNumUint = 0 + rd.SchemaNumByteSlice = 0 + rd.KeyValue = nil + + rd.ResourceFlags |= ResourceFlagsNotHolding + hadParams := (rd.ResourceFlags & ResourceFlagsOwnership) == ResourceFlagsOwnership + if hadParams && rd.IsEmptyAppFields() { + rd.ResourceFlags |= ResourceFlagsEmptyApp + } else { + rd.ResourceFlags &= ^ResourceFlagsEmptyApp + } +} + +// SetAppLocalState setter for app local state. +func (rd *ResourcesData) SetAppLocalState(als basics.AppLocalState) { + rd.SchemaNumUint = als.Schema.NumUint + rd.SchemaNumByteSlice = als.Schema.NumByteSlice + rd.KeyValue = als.KeyValue + rd.ResourceFlags &= ^(ResourceFlagsEmptyApp + ResourceFlagsNotHolding) + if rd.IsEmptyAppFields() { + rd.ResourceFlags |= ResourceFlagsEmptyApp + } +} + +// GetAppLocalState getter for app local state. +func (rd *ResourcesData) GetAppLocalState() basics.AppLocalState { + return basics.AppLocalState{ + Schema: basics.StateSchema{ + NumUint: rd.SchemaNumUint, + NumByteSlice: rd.SchemaNumByteSlice, + }, + KeyValue: rd.KeyValue, + } +} + +// ClearAppParams clears the app params. +func (rd *ResourcesData) ClearAppParams() { + rd.ApprovalProgram = nil + rd.ClearStateProgram = nil + rd.GlobalState = nil + rd.LocalStateSchemaNumUint = 0 + rd.LocalStateSchemaNumByteSlice = 0 + rd.GlobalStateSchemaNumUint = 0 + rd.GlobalStateSchemaNumByteSlice = 0 + rd.ExtraProgramPages = 0 + hadHolding := (rd.ResourceFlags & ResourceFlagsNotHolding) == ResourceFlagsHolding + rd.ResourceFlags -= rd.ResourceFlags & ResourceFlagsOwnership + rd.ResourceFlags &= ^ResourceFlagsEmptyApp + if rd.IsEmptyAppFields() && hadHolding { + rd.ResourceFlags |= ResourceFlagsEmptyApp + } +} + +// SetAppParams setter for app params. +func (rd *ResourcesData) SetAppParams(ap basics.AppParams, haveHoldings bool) { + rd.ApprovalProgram = ap.ApprovalProgram + rd.ClearStateProgram = ap.ClearStateProgram + rd.GlobalState = ap.GlobalState + rd.LocalStateSchemaNumUint = ap.LocalStateSchema.NumUint + rd.LocalStateSchemaNumByteSlice = ap.LocalStateSchema.NumByteSlice + rd.GlobalStateSchemaNumUint = ap.GlobalStateSchema.NumUint + rd.GlobalStateSchemaNumByteSlice = ap.GlobalStateSchema.NumByteSlice + rd.ExtraProgramPages = ap.ExtraProgramPages + rd.ResourceFlags |= ResourceFlagsOwnership + if !haveHoldings { + rd.ResourceFlags |= ResourceFlagsNotHolding + } + rd.ResourceFlags &= ^ResourceFlagsEmptyApp + if rd.IsEmptyAppFields() { + rd.ResourceFlags |= ResourceFlagsEmptyApp + } +} + +// GetAppParams getter for app params. +func (rd *ResourcesData) GetAppParams() basics.AppParams { + return basics.AppParams{ + ApprovalProgram: rd.ApprovalProgram, + ClearStateProgram: rd.ClearStateProgram, + GlobalState: rd.GlobalState, + StateSchemas: basics.StateSchemas{ + LocalStateSchema: basics.StateSchema{ + NumUint: rd.LocalStateSchemaNumUint, + NumByteSlice: rd.LocalStateSchemaNumByteSlice, + }, + GlobalStateSchema: basics.StateSchema{ + NumUint: rd.GlobalStateSchemaNumUint, + NumByteSlice: rd.GlobalStateSchemaNumByteSlice, + }, + }, + ExtraProgramPages: rd.ExtraProgramPages, + } +} + +// SetAssetData setter for asset data. +func (rd *ResourcesData) SetAssetData(ap ledgercore.AssetParamsDelta, ah ledgercore.AssetHoldingDelta) { + if ah.Holding != nil { + rd.SetAssetHolding(*ah.Holding) + } else if ah.Deleted { + rd.ClearAssetHolding() + } + if ap.Params != nil { + rd.SetAssetParams(*ap.Params, rd.IsHolding()) + } else if ap.Deleted { + rd.ClearAssetParams() + } +} + +// SetAppData setter for app data. +func (rd *ResourcesData) SetAppData(ap ledgercore.AppParamsDelta, al ledgercore.AppLocalStateDelta) { + if al.LocalState != nil { + rd.SetAppLocalState(*al.LocalState) + } else if al.Deleted { + rd.ClearAppLocalState() + } + if ap.Params != nil { + rd.SetAppParams(*ap.Params, rd.IsHolding()) + } else if ap.Deleted { + rd.ClearAppParams() + } +} + +// Before compares the round numbers of two persistedAccountData and determines if the current persistedAccountData +// happened before the other. +func (pac *PersistedAccountData) Before(other *PersistedAccountData) bool { + return pac.Round < other.Round +} + +// Before compares the round numbers of two persistedResourcesData and determines if the current persistedResourcesData +// happened before the other. +func (prd *PersistedResourcesData) Before(other *PersistedResourcesData) bool { + return prd.Round < other.Round +} + +// Before compares the round numbers of two persistedKVData and determines if the current persistedKVData +// happened before the other. +func (prd PersistedKVData) Before(other *PersistedKVData) bool { + return prd.Round < other.Round +} + +// Before compares the round numbers of two persistedAccountData and determines if the current persistedAccountData +// happened before the other. +func (pac *PersistedOnlineAccountData) Before(other *PersistedOnlineAccountData) bool { + return pac.UpdRound < other.UpdRound +} + +// Encode the transaction tail data into a serialized form, and return the serialized data +// as well as the hash of the data. +func (t *TxTailRound) Encode() ([]byte, crypto.Digest) { + tailData := protocol.Encode(t) + hash := crypto.Hash(tailData) + return tailData, hash +} + +// TODO: this is currently public just for a test in txtail_test.go + +// TxTailRoundFromBlock creates a TxTailRound for the given block +func TxTailRoundFromBlock(blk bookkeeping.Block) (*TxTailRound, error) { + payset, err := blk.DecodePaysetFlat() + if err != nil { + return nil, err + } + + tail := &TxTailRound{} + + tail.TxnIDs = make([]transactions.Txid, len(payset)) + tail.LastValid = make([]basics.Round, len(payset)) + tail.Hdr = blk.BlockHeader + + for txIdxtxid, txn := range payset { + tail.TxnIDs[txIdxtxid] = txn.ID() + tail.LastValid[txIdxtxid] = txn.Txn.LastValid + if txn.Txn.Lease != [32]byte{} { + tail.Leases = append(tail.Leases, TxTailRoundLease{ + Sender: txn.Txn.Sender, + Lease: txn.Txn.Lease, + TxnIdx: uint64(txIdxtxid), + }) + } + } + return tail, nil +} + +// AccountDataResources calls outputResourceCb with the data resources for the specified account. +func AccountDataResources( + ctx context.Context, + accountData *basics.AccountData, rowid int64, + outputResourceCb func(ctx context.Context, rowid int64, cidx basics.CreatableIndex, rd *ResourcesData) error, +) error { + // handle all the assets we can find: + for aidx, holding := range accountData.Assets { + var rd ResourcesData + rd.SetAssetHolding(holding) + if ap, has := accountData.AssetParams[aidx]; has { + rd.SetAssetParams(ap, true) + delete(accountData.AssetParams, aidx) + } + err := outputResourceCb(ctx, rowid, basics.CreatableIndex(aidx), &rd) + if err != nil { + return err + } + } + for aidx, aparams := range accountData.AssetParams { + var rd ResourcesData + rd.SetAssetParams(aparams, false) + err := outputResourceCb(ctx, rowid, basics.CreatableIndex(aidx), &rd) + if err != nil { + return err + } + } + + // handle all the applications we can find: + for aidx, localState := range accountData.AppLocalStates { + var rd ResourcesData + rd.SetAppLocalState(localState) + if ap, has := accountData.AppParams[aidx]; has { + rd.SetAppParams(ap, true) + delete(accountData.AppParams, aidx) + } + err := outputResourceCb(ctx, rowid, basics.CreatableIndex(aidx), &rd) + if err != nil { + return err + } + } + for aidx, aparams := range accountData.AppParams { + var rd ResourcesData + rd.SetAppParams(aparams, false) + err := outputResourceCb(ctx, rowid, basics.CreatableIndex(aidx), &rd) + if err != nil { + return err + } + } + + return nil +} diff --git a/ledger/store/data_test.go b/ledger/store/data_test.go new file mode 100644 index 0000000000..8e360b4bdd --- /dev/null +++ b/ledger/store/data_test.go @@ -0,0 +1,1254 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "reflect" + "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/ledger/ledgercore" + ledgertesting "github.com/algorand/go-algorand/ledger/testing" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" +) + +func TestResourcesDataApp(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + a := require.New(t) + + rd := ResourcesData{} + a.False(rd.IsApp()) + a.True(rd.IsEmpty()) + + rd = MakeResourcesData(1) + a.False(rd.IsApp()) + a.False(rd.IsHolding()) + a.False(rd.IsOwning()) + a.True(rd.IsEmpty()) + + // check empty + appParamsEmpty := basics.AppParams{} + rd = ResourcesData{} + rd.SetAppParams(appParamsEmpty, false) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.True(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + a.Equal(appParamsEmpty, rd.GetAppParams()) + + appLocalEmpty := basics.AppLocalState{} + rd = ResourcesData{} + rd.SetAppLocalState(appLocalEmpty) + a.True(rd.IsApp()) + a.True(rd.IsHolding()) + a.True(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + a.Equal(appLocalEmpty, rd.GetAppLocalState()) + + // check both empty + rd = ResourcesData{} + rd.SetAppLocalState(appLocalEmpty) + rd.SetAppParams(appParamsEmpty, true) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.True(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + a.Equal(appParamsEmpty, rd.GetAppParams()) + a.Equal(appLocalEmpty, rd.GetAppLocalState()) + + // Since some steps use randomly generated input, the test is run N times + // to cover a larger search space of inputs. + for i := 0; i < 1000; i++ { + // check empty states + non-empty params + appParams := ledgertesting.RandomAppParams() + rd = ResourcesData{} + rd.SetAppLocalState(appLocalEmpty) + rd.SetAppParams(appParams, true) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + a.Equal(appParams, rd.GetAppParams()) + a.Equal(appLocalEmpty, rd.GetAppLocalState()) + + appState := ledgertesting.RandomAppLocalState() + rd.SetAppLocalState(appState) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + a.Equal(appParams, rd.GetAppParams()) + a.Equal(appState, rd.GetAppLocalState()) + + // check ClearAppLocalState + rd.ClearAppLocalState() + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.False(rd.IsHolding()) + a.False(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + a.Equal(appParams, rd.GetAppParams()) + a.Equal(appLocalEmpty, rd.GetAppLocalState()) + + // check ClearAppParams + rd.SetAppLocalState(appState) + rd.ClearAppParams() + a.True(rd.IsApp()) + a.False(rd.IsOwning()) + a.True(rd.IsHolding()) + if appState.Schema.NumEntries() == 0 { + a.True(rd.IsEmptyAppFields()) + } else { + a.False(rd.IsEmptyAppFields()) + } + a.False(rd.IsEmpty()) + a.Equal(appParamsEmpty, rd.GetAppParams()) + a.Equal(appState, rd.GetAppLocalState()) + + // check both clear + rd.ClearAppLocalState() + a.False(rd.IsApp()) + a.False(rd.IsOwning()) + a.False(rd.IsHolding()) + a.True(rd.IsEmptyAppFields()) + a.True(rd.IsEmpty()) + a.Equal(appParamsEmpty, rd.GetAppParams()) + a.Equal(appLocalEmpty, rd.GetAppLocalState()) + + // check params clear when non-empty params and empty holding + rd = ResourcesData{} + rd.SetAppLocalState(appLocalEmpty) + rd.SetAppParams(appParams, true) + rd.ClearAppParams() + a.True(rd.IsApp()) + a.False(rd.IsOwning()) + a.True(rd.IsHolding()) + a.True(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + a.Equal(appParamsEmpty, rd.GetAppParams()) + a.Equal(appLocalEmpty, rd.GetAppLocalState()) + + rd = ResourcesData{} + rd.SetAppLocalState(appLocalEmpty) + a.True(rd.IsEmptyAppFields()) + a.True(rd.IsApp()) + a.False(rd.IsEmpty()) + a.Equal(rd.ResourceFlags, ResourceFlagsEmptyApp) + rd.ClearAppLocalState() + a.False(rd.IsApp()) + a.True(rd.IsEmptyAppFields()) + a.True(rd.IsEmpty()) + a.Equal(rd.ResourceFlags, ResourceFlagsNotHolding) + + // check migration flow (AccountDataResources) + // 1. both exist and empty + rd = MakeResourcesData(0) + rd.SetAppLocalState(appLocalEmpty) + rd.SetAppParams(appParamsEmpty, true) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.True(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + + // 2. both exist and not empty + rd = MakeResourcesData(0) + rd.SetAppLocalState(appState) + rd.SetAppParams(appParams, true) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + + // 3. both exist: holding not empty, param is empty + rd = MakeResourcesData(0) + rd.SetAppLocalState(appState) + rd.SetAppParams(appParamsEmpty, true) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + if appState.Schema.NumEntries() == 0 { + a.True(rd.IsEmptyAppFields()) + } else { + a.False(rd.IsEmptyAppFields()) + } + a.False(rd.IsEmpty()) + + // 4. both exist: holding empty, param is not empty + rd = MakeResourcesData(0) + rd.SetAppLocalState(appLocalEmpty) + rd.SetAppParams(appParams, true) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + + // 5. holding does not exist and params is empty + rd = MakeResourcesData(0) + rd.SetAppParams(appParamsEmpty, false) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.False(rd.IsHolding()) + a.True(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + + // 6. holding does not exist and params is not empty + rd = MakeResourcesData(0) + rd.SetAppParams(appParams, false) + a.True(rd.IsApp()) + a.True(rd.IsOwning()) + a.False(rd.IsHolding()) + a.False(rd.IsEmptyAppFields()) + a.False(rd.IsEmpty()) + + // 7. holding exist and not empty and params does not exist + rd = MakeResourcesData(0) + rd.SetAppLocalState(appState) + a.True(rd.IsApp()) + a.False(rd.IsOwning()) + a.True(rd.IsHolding()) + if appState.Schema.NumEntries() == 0 { + a.True(rd.IsEmptyAppFields()) + } else { + a.False(rd.IsEmptyAppFields()) + } + a.False(rd.IsEmpty()) + + // 8. both do not exist + rd = MakeResourcesData(0) + a.False(rd.IsApp()) + a.False(rd.IsOwning()) + a.False(rd.IsHolding()) + a.True(rd.IsEmptyAppFields()) + a.True(rd.IsEmpty()) + } +} + +func TestResourcesDataAsset(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + rd := ResourcesData{} + a.False(rd.IsAsset()) + a.True(rd.IsEmpty()) + + rd = MakeResourcesData(1) + a.False(rd.IsAsset()) + a.False(rd.IsHolding()) + a.False(rd.IsOwning()) + a.True(rd.IsEmpty()) + + // check empty + assetParamsEmpty := basics.AssetParams{} + rd = ResourcesData{} + rd.SetAssetParams(assetParamsEmpty, false) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.True(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + a.Equal(assetParamsEmpty, rd.GetAssetParams()) + + assetHoldingEmpty := basics.AssetHolding{} + rd = ResourcesData{} + rd.SetAssetHolding(assetHoldingEmpty) + a.True(rd.IsAsset()) + a.True(rd.IsHolding()) + a.True(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) + + // check both empty + rd = ResourcesData{} + rd.SetAssetHolding(assetHoldingEmpty) + rd.SetAssetParams(assetParamsEmpty, true) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.True(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + a.Equal(assetParamsEmpty, rd.GetAssetParams()) + a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) + + // check empty states + non-empty params + assetParams := ledgertesting.RandomAssetParams() + rd = ResourcesData{} + rd.SetAssetHolding(assetHoldingEmpty) + rd.SetAssetParams(assetParams, true) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + a.Equal(assetParams, rd.GetAssetParams()) + a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) + + assetHolding := ledgertesting.RandomAssetHolding(true) + rd.SetAssetHolding(assetHolding) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + a.Equal(assetParams, rd.GetAssetParams()) + a.Equal(assetHolding, rd.GetAssetHolding()) + + // check ClearAssetHolding + rd.ClearAssetHolding() + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.False(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + a.Equal(assetParams, rd.GetAssetParams()) + a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) + + // check ClearAssetParams + rd.SetAssetHolding(assetHolding) + rd.ClearAssetParams() + a.True(rd.IsAsset()) + a.False(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + a.Equal(assetParamsEmpty, rd.GetAssetParams()) + a.Equal(assetHolding, rd.GetAssetHolding()) + + // check both clear + rd.ClearAssetHolding() + a.False(rd.IsAsset()) + a.False(rd.IsOwning()) + a.False(rd.IsHolding()) + a.True(rd.IsEmptyAssetFields()) + a.True(rd.IsEmpty()) + a.Equal(assetParamsEmpty, rd.GetAssetParams()) + a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) + + // check params clear when non-empty params and empty holding + rd = ResourcesData{} + rd.SetAssetHolding(assetHoldingEmpty) + rd.SetAssetParams(assetParams, true) + rd.ClearAssetParams() + a.True(rd.IsAsset()) + a.False(rd.IsOwning()) + a.True(rd.IsHolding()) + a.True(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + a.Equal(assetParamsEmpty, rd.GetAssetParams()) + a.Equal(assetHoldingEmpty, rd.GetAssetHolding()) + + rd = ResourcesData{} + rd.SetAssetHolding(assetHoldingEmpty) + a.True(rd.IsEmptyAssetFields()) + a.True(rd.IsAsset()) + a.False(rd.IsEmpty()) + a.Equal(rd.ResourceFlags, ResourceFlagsEmptyAsset) + rd.ClearAssetHolding() + a.False(rd.IsAsset()) + a.True(rd.IsEmptyAssetFields()) + a.True(rd.IsEmpty()) + a.Equal(rd.ResourceFlags, ResourceFlagsNotHolding) + + // check migration operations (AccountDataResources) + // 1. both exist and empty + rd = MakeResourcesData(0) + rd.SetAssetHolding(assetHoldingEmpty) + rd.SetAssetParams(assetParamsEmpty, true) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.True(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + + // 2. both exist and not empty + rd = MakeResourcesData(0) + rd.SetAssetHolding(assetHolding) + rd.SetAssetParams(assetParams, true) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + + // 3. both exist: holding not empty, param is empty + rd = MakeResourcesData(0) + rd.SetAssetHolding(assetHolding) + rd.SetAssetParams(assetParamsEmpty, true) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + + // 4. both exist: holding empty, param is not empty + rd = MakeResourcesData(0) + rd.SetAssetHolding(assetHoldingEmpty) + rd.SetAssetParams(assetParams, true) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + + // 5. holding does not exist and params is empty + rd = MakeResourcesData(0) + rd.SetAssetParams(assetParamsEmpty, false) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.False(rd.IsHolding()) + a.True(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + + // 6. holding does not exist and params is not empty + rd = MakeResourcesData(0) + rd.SetAssetParams(assetParams, false) + a.True(rd.IsAsset()) + a.True(rd.IsOwning()) + a.False(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + + // 7. holding exist and not empty and params does not exist + rd = MakeResourcesData(0) + rd.SetAssetHolding(assetHolding) + a.True(rd.IsAsset()) + a.False(rd.IsOwning()) + a.True(rd.IsHolding()) + a.False(rd.IsEmptyAssetFields()) + a.False(rd.IsEmpty()) + + // 8. both do not exist + rd = MakeResourcesData(0) + a.False(rd.IsAsset()) + a.False(rd.IsOwning()) + a.False(rd.IsHolding()) + a.True(rd.IsEmptyAssetFields()) + a.True(rd.IsEmpty()) +} + +// TestResourcesDataSetData checks combinations of old/new values when +// updating resourceData from resourceDelta +func TestResourcesDataSetData(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + type deltaCode int + const ( + tri deltaCode = iota + 1 + del + emp + act + ) + + // apply deltas encoded as deltaCode to a base ResourcesData for both apps and assets + apply := func(t *testing.T, base ResourcesData, testType basics.CreatableType, pcode, hcode deltaCode) ResourcesData { + if testType == basics.AssetCreatable { + var p ledgercore.AssetParamsDelta + var h ledgercore.AssetHoldingDelta + switch pcode { + case tri: + break + case del: + p = ledgercore.AssetParamsDelta{Deleted: true} + case emp: + p = ledgercore.AssetParamsDelta{Params: &basics.AssetParams{}} + case act: + p = ledgercore.AssetParamsDelta{Params: &basics.AssetParams{Total: 1000}} + default: + t.Logf("invalid pcode: %d", pcode) + t.Fail() + } + switch hcode { + case tri: + break + case del: + h = ledgercore.AssetHoldingDelta{Deleted: true} + case emp: + h = ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{}} + case act: + h = ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 555}} + default: + t.Logf("invalid hcode: %d", hcode) + t.Fail() + } + base.SetAssetData(p, h) + } else { + var p ledgercore.AppParamsDelta + var h ledgercore.AppLocalStateDelta + switch pcode { + case tri: + break + case del: + p = ledgercore.AppParamsDelta{Deleted: true} + case emp: + p = ledgercore.AppParamsDelta{Params: &basics.AppParams{}} + case act: + p = ledgercore.AppParamsDelta{Params: &basics.AppParams{ClearStateProgram: []byte{4, 5, 6}}} + default: + t.Logf("invalid pcode: %d", pcode) + t.Fail() + } + switch hcode { + case tri: + break + case del: + h = ledgercore.AppLocalStateDelta{Deleted: true} + case emp: + h = ledgercore.AppLocalStateDelta{LocalState: &basics.AppLocalState{}} + case act: + h = ledgercore.AppLocalStateDelta{LocalState: &basics.AppLocalState{Schema: basics.StateSchema{NumByteSlice: 5}}} + default: + t.Logf("invalid hcode: %d", hcode) + t.Fail() + } + base.SetAppData(p, h) + } + + return base + } + + itb := func(i int) (b bool) { + return i != 0 + } + + type testcase struct { + p deltaCode + h deltaCode + isAsset int + isOwning int + isHolding int + isEmptyFields int + isEmpty int + } + + empty := func(testType basics.CreatableType) ResourcesData { + return MakeResourcesData(0) + } + emptyParamsNoHolding := func(testType basics.CreatableType) ResourcesData { + rd := MakeResourcesData(0) + if testType == basics.AssetCreatable { + rd.SetAssetParams(basics.AssetParams{}, false) + } else { + rd.SetAppParams(basics.AppParams{}, false) + } + return rd + } + emptyParamsEmptyHolding := func(testType basics.CreatableType) ResourcesData { + rd := MakeResourcesData(0) + if testType == basics.AssetCreatable { + rd.SetAssetHolding(basics.AssetHolding{}) + rd.SetAssetParams(basics.AssetParams{}, true) + } else { + rd.SetAppLocalState(basics.AppLocalState{}) + rd.SetAppParams(basics.AppParams{}, true) + } + return rd + } + emptyParamsNotEmptyHolding := func(testType basics.CreatableType) ResourcesData { + rd := MakeResourcesData(0) + if testType == basics.AssetCreatable { + rd.SetAssetHolding(basics.AssetHolding{Amount: 111}) + rd.SetAssetParams(basics.AssetParams{}, true) + } else { + rd.SetAppLocalState(basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}) + rd.SetAppParams(basics.AppParams{}, true) + } + return rd + } + paramsNoHolding := func(testType basics.CreatableType) ResourcesData { + rd := MakeResourcesData(0) + if testType == basics.AssetCreatable { + rd.SetAssetParams(basics.AssetParams{Total: 222}, false) + } else { + rd.SetAppParams(basics.AppParams{ApprovalProgram: []byte{1, 2, 3}}, false) + } + return rd + } + paramsEmptyHolding := func(testType basics.CreatableType) ResourcesData { + rd := MakeResourcesData(0) + if testType == basics.AssetCreatable { + rd.SetAssetHolding(basics.AssetHolding{}) + rd.SetAssetParams(basics.AssetParams{Total: 222}, true) + } else { + rd.SetAppLocalState(basics.AppLocalState{}) + rd.SetAppParams(basics.AppParams{ApprovalProgram: []byte{1, 2, 3}}, true) + } + return rd + } + paramsAndHolding := func(testType basics.CreatableType) ResourcesData { + rd := MakeResourcesData(0) + if testType == basics.AssetCreatable { + rd.SetAssetHolding(basics.AssetHolding{Amount: 111}) + rd.SetAssetParams(basics.AssetParams{Total: 222}, true) + } else { + rd.SetAppLocalState(basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}) + rd.SetAppParams(basics.AppParams{ApprovalProgram: []byte{1, 2, 3}}, true) + } + return rd + } + noParamsEmptyHolding := func(testType basics.CreatableType) ResourcesData { + rd := MakeResourcesData(0) + if testType == basics.AssetCreatable { + rd.SetAssetHolding(basics.AssetHolding{}) + } else { + rd.SetAppLocalState(basics.AppLocalState{}) + } + return rd + } + noParamsNotEmptyHolding := func(testType basics.CreatableType) ResourcesData { + rd := MakeResourcesData(0) + if testType == basics.AssetCreatable { + rd.SetAssetHolding(basics.AssetHolding{Amount: 111}) + } else { + rd.SetAppLocalState(basics.AppLocalState{Schema: basics.StateSchema{NumUint: 10}}) + } + return rd + } + + var tests = []struct { + name string + baseRD func(testType basics.CreatableType) ResourcesData + testcases []testcase + }{ + { + "empty_base", empty, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 0, 0, 0, 1, 1}, + {del, tri, 0, 0, 0, 1, 1}, + {emp, tri, 1, 1, 0, 1, 0}, + {act, tri, 1, 1, 0, 0, 0}, + + {tri, del, 0, 0, 0, 1, 1}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 0, 1, 1, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 0, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + + { + "empty_params_no_holding", emptyParamsNoHolding, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 1, 1, 0, 1, 0}, + {del, tri, 0, 0, 0, 1, 1}, + {emp, tri, 1, 1, 0, 1, 0}, + {act, tri, 1, 1, 0, 0, 0}, + + {tri, del, 1, 1, 0, 1, 0}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 1, 1, 1, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 1, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + { + "empty_params_empty_holding", emptyParamsEmptyHolding, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 1, 1, 1, 1, 0}, + {del, tri, 1, 0, 1, 1, 0}, + {emp, tri, 1, 1, 1, 1, 0}, + {act, tri, 1, 1, 1, 0, 0}, + + {tri, del, 1, 1, 0, 1, 0}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 1, 1, 1, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 1, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + { + "empty_params_not_empty_holding", emptyParamsNotEmptyHolding, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 1, 1, 1, 0, 0}, + {del, tri, 1, 0, 1, 0, 0}, + {emp, tri, 1, 1, 1, 0, 0}, + {act, tri, 1, 1, 1, 0, 0}, + + {tri, del, 1, 1, 0, 1, 0}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 1, 1, 1, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 1, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + { + "params_no_holding", paramsNoHolding, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 1, 1, 0, 0, 0}, + {del, tri, 0, 0, 0, 1, 1}, + {emp, tri, 1, 1, 0, 1, 0}, + {act, tri, 1, 1, 0, 0, 0}, + + {tri, del, 1, 1, 0, 0, 0}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 1, 1, 0, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 1, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + { + "params_empty_holding", paramsEmptyHolding, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 1, 1, 1, 0, 0}, + {del, tri, 1, 0, 1, 1, 0}, + {emp, tri, 1, 1, 1, 1, 0}, + {act, tri, 1, 1, 1, 0, 0}, + + {tri, del, 1, 1, 0, 0, 0}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 1, 1, 0, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 1, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + { + "params_and_holding", paramsAndHolding, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 1, 1, 1, 0, 0}, + {del, tri, 1, 0, 1, 0, 0}, + {emp, tri, 1, 1, 1, 0, 0}, + {act, tri, 1, 1, 1, 0, 0}, + + {tri, del, 1, 1, 0, 0, 0}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 1, 1, 0, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 1, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + { + "no_params_empty_holding", noParamsEmptyHolding, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 1, 0, 1, 1, 0}, + {del, tri, 1, 0, 1, 1, 0}, + {emp, tri, 1, 1, 1, 1, 0}, + {act, tri, 1, 1, 1, 0, 0}, + + {tri, del, 0, 0, 0, 1, 1}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 0, 1, 1, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 0, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + { + "no_params_not_empty_holding", noParamsNotEmptyHolding, + []testcase{ + // IsAsset, IsOwning, IsHolding, IsEmptyAssetFields, IsEmpty + {tri, tri, 1, 0, 1, 0, 0}, + {del, tri, 1, 0, 1, 0, 0}, + {emp, tri, 1, 1, 1, 0, 0}, + {act, tri, 1, 1, 1, 0, 0}, + + {tri, del, 0, 0, 0, 1, 1}, + {del, del, 0, 0, 0, 1, 1}, + {emp, del, 1, 1, 0, 1, 0}, + {act, del, 1, 1, 0, 0, 0}, + + {tri, emp, 1, 0, 1, 1, 0}, + {del, emp, 1, 0, 1, 1, 0}, + {emp, emp, 1, 1, 1, 1, 0}, + {act, emp, 1, 1, 1, 0, 0}, + + {tri, act, 1, 0, 1, 0, 0}, + {del, act, 1, 0, 1, 0, 0}, + {emp, act, 1, 1, 1, 0, 0}, + {act, act, 1, 1, 1, 0, 0}, + }, + }, + } + for _, testType := range []basics.CreatableType{basics.AssetCreatable, basics.AppCreatable} { + for _, test := range tests { + var testTypeStr string + if testType == basics.AssetCreatable { + testTypeStr = "asset" + } else { + testTypeStr = "app" + } + t.Run(fmt.Sprintf("test_%s_%s", testTypeStr, test.name), func(t *testing.T) { + for i, ts := range test.testcases { + t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) { + rd := test.baseRD(testType) + rd = apply(t, rd, testType, ts.p, ts.h) + if testType == basics.AssetCreatable { + a.Equal(itb(ts.isAsset), rd.IsAsset()) + a.Equal(itb(ts.isEmptyFields), rd.IsEmptyAssetFields()) + a.False(rd.IsApp()) + a.True(rd.IsEmptyAppFields()) + } else { + a.Equal(itb(ts.isAsset), rd.IsApp()) + a.Equal(itb(ts.isEmptyFields), rd.IsEmptyAppFields()) + a.False(rd.IsAsset()) + a.True(rd.IsEmptyAssetFields()) + } + a.Equal(itb(ts.isOwning), rd.IsOwning()) + a.Equal(itb(ts.isHolding), rd.IsHolding()) + a.Equal(itb(ts.isEmpty), rd.IsEmpty()) + }) + } + }) + } + } +} + +// TestResourceDataRoundtripConversion ensures that basics.AppLocalState, basics.AppParams, +// basics.AssetHolding, and basics.AssetParams can be converted to resourcesData and back without +// losing any data. It uses reflection to be sure that no new fields are omitted. +// +// In other words, this test makes sure any new fields in basics.AppLocalState, basics.AppParams, +// basics.AssetHolding, or basics.AssetParam also get added to resourcesData. +func TestResourceDataRoundtripConversion(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + t.Run("basics.AppLocalState", func(t *testing.T) { + t.Parallel() + for i := 0; i < 1000; i++ { + randObj, _ := protocol.RandomizeObject(&basics.AppLocalState{}) + basicsAppLocalState := *randObj.(*basics.AppLocalState) + + var data ResourcesData + data.SetAppLocalState(basicsAppLocalState) + roundTripAppLocalState := data.GetAppLocalState() + + require.Equal(t, basicsAppLocalState, roundTripAppLocalState) + } + }) + + t.Run("basics.AppParams", func(t *testing.T) { + t.Parallel() + for i := 0; i < 1000; i++ { + randObj, _ := protocol.RandomizeObject(&basics.AppParams{}) + basicsAppParams := *randObj.(*basics.AppParams) + + for _, haveHoldings := range []bool{true, false} { + var data ResourcesData + data.SetAppParams(basicsAppParams, haveHoldings) + roundTripAppParams := data.GetAppParams() + + require.Equal(t, basicsAppParams, roundTripAppParams) + } + } + }) + + t.Run("basics.AssetHolding", func(t *testing.T) { + t.Parallel() + for i := 0; i < 1000; i++ { + randObj, _ := protocol.RandomizeObject(&basics.AssetHolding{}) + basicsAssetHolding := *randObj.(*basics.AssetHolding) + + var data ResourcesData + data.SetAssetHolding(basicsAssetHolding) + roundTripAssetHolding := data.GetAssetHolding() + + require.Equal(t, basicsAssetHolding, roundTripAssetHolding) + } + }) + + t.Run("basics.AssetParams", func(t *testing.T) { + t.Parallel() + for i := 0; i < 1000; i++ { + randObj, _ := protocol.RandomizeObject(&basics.AssetParams{}) + basicsAssetParams := *randObj.(*basics.AssetParams) + + for _, haveHoldings := range []bool{true, false} { + var data ResourcesData + data.SetAssetParams(basicsAssetParams, haveHoldings) + roundTripAssetParams := data.GetAssetParams() + + require.Equal(t, basicsAssetParams, roundTripAssetParams) + } + } + }) +} + +// TestBaseAccountDataRoundtripConversion ensures that baseAccountData can be converted to +// ledgercore.AccountData and basics.AccountData and back without losing any data. It uses +// reflection to be sure that no new fields are omitted. +// +// In other words, this test makes sure any new fields in baseAccountData also get added to +// ledgercore.AccountData and basics.AccountData. You should add a manual override in this test if +// the field really only belongs in baseAccountData. +func TestBaseAccountDataRoundtripConversion(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + t.Run("ledgercore.AccountData", func(t *testing.T) { + t.Parallel() + for i := 0; i < 1000; i++ { + randObj, _ := protocol.RandomizeObject(&BaseAccountData{}) + baseAccount := *randObj.(*BaseAccountData) + + ledgercoreAccount := baseAccount.GetLedgerCoreAccountData() + var roundTripAccount BaseAccountData + roundTripAccount.SetCoreAccountData(&ledgercoreAccount) + + // Manually set UpdateRound, since it is lost in GetLedgerCoreAccountData + roundTripAccount.UpdateRound = baseAccount.UpdateRound + + require.Equal(t, baseAccount, roundTripAccount) + } + }) + + t.Run("basics.AccountData", func(t *testing.T) { + t.Parallel() + for i := 0; i < 1000; i++ { + randObj, _ := protocol.RandomizeObject(&BaseAccountData{}) + baseAccount := *randObj.(*BaseAccountData) + + basicsAccount := baseAccount.GetAccountData() + var roundTripAccount BaseAccountData + roundTripAccount.SetAccountData(&basicsAccount) + + // Manually set UpdateRound, since it is lost in GetAccountData + roundTripAccount.UpdateRound = baseAccount.UpdateRound + + // Manually set resources, since resource information is lost in GetAccountData + roundTripAccount.TotalAssetParams = baseAccount.TotalAssetParams + roundTripAccount.TotalAssets = baseAccount.TotalAssets + roundTripAccount.TotalAppLocalStates = baseAccount.TotalAppLocalStates + roundTripAccount.TotalAppParams = baseAccount.TotalAppParams + + require.Equal(t, baseAccount, roundTripAccount) + } + }) +} + +// TestBasicsAccountDataRoundtripConversion ensures that basics.AccountData can be converted to +// baseAccountData and back without losing any data. It uses reflection to be sure that this test is +// always up-to-date with new fields. +// +// In other words, this test makes sure any new fields in basics.AccountData also get added to +// baseAccountData. +func TestBasicsAccountDataRoundtripConversion(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + for i := 0; i < 1000; i++ { + randObj, _ := protocol.RandomizeObject(&basics.AccountData{}) + basicsAccount := *randObj.(*basics.AccountData) + + var baseAccount BaseAccountData + baseAccount.SetAccountData(&basicsAccount) + roundTripAccount := baseAccount.GetAccountData() + + // Manually set resources, since GetAccountData doesn't attempt to restore them + roundTripAccount.AssetParams = basicsAccount.AssetParams + roundTripAccount.Assets = basicsAccount.Assets + roundTripAccount.AppLocalStates = basicsAccount.AppLocalStates + roundTripAccount.AppParams = basicsAccount.AppParams + + require.Equal(t, basicsAccount, roundTripAccount) + require.Equal(t, uint64(len(roundTripAccount.AssetParams)), baseAccount.TotalAssetParams) + require.Equal(t, uint64(len(roundTripAccount.Assets)), baseAccount.TotalAssets) + require.Equal(t, uint64(len(roundTripAccount.AppLocalStates)), baseAccount.TotalAppLocalStates) + require.Equal(t, uint64(len(roundTripAccount.AppParams)), baseAccount.TotalAppParams) + } +} + +// TestLedgercoreAccountDataRoundtripConversion ensures that ledgercore.AccountData can be converted +// to baseAccountData and back without losing any data. It uses reflection to be sure that no new +// fields are omitted. +// +// In other words, this test makes sure any new fields in ledgercore.AccountData also get added to +// baseAccountData. +func TestLedgercoreAccountDataRoundtripConversion(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + for i := 0; i < 1000; i++ { + randObj, _ := protocol.RandomizeObject(&ledgercore.AccountData{}) + ledgercoreAccount := *randObj.(*ledgercore.AccountData) + + var baseAccount BaseAccountData + baseAccount.SetCoreAccountData(&ledgercoreAccount) + roundTripAccount := baseAccount.GetLedgerCoreAccountData() + + require.Equal(t, ledgercoreAccount, roundTripAccount) + } +} + +func TestBaseAccountDataIsEmpty(t *testing.T) { + partitiontest.PartitionTest(t) + positiveTesting := func(t *testing.T) { + var ba BaseAccountData + require.True(t, ba.IsEmpty()) + for i := 0; i < 20; i++ { + h := crypto.Hash([]byte{byte(i)}) + rnd := binary.BigEndian.Uint64(h[:]) + ba.UpdateRound = rnd + require.True(t, ba.IsEmpty()) + } + } + var empty BaseAccountData + negativeTesting := func(t *testing.T) { + for i := 0; i < 10000; i++ { + randObj, _ := protocol.RandomizeObjectField(&BaseAccountData{}) + ba := randObj.(*BaseAccountData) + if *ba == empty || ba.UpdateRound != 0 { + continue + } + require.False(t, ba.IsEmpty(), "base account : %v", ba) + } + } + structureTesting := func(t *testing.T) { + encoding, err := json.Marshal(&empty) + zeros32 := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" + expectedEncoding := `{"Status":0,"MicroAlgos":{"Raw":0},"RewardsBase":0,"RewardedMicroAlgos":{"Raw":0},"AuthAddr":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ","TotalAppSchemaNumUint":0,"TotalAppSchemaNumByteSlice":0,"TotalExtraAppPages":0,"TotalAssetParams":0,"TotalAssets":0,"TotalAppParams":0,"TotalAppLocalStates":0,"TotalBoxes":0,"TotalBoxBytes":0,"VoteID":[` + zeros32 + `],"SelectionID":[` + zeros32 + `],"VoteFirstValid":0,"VoteLastValid":0,"VoteKeyDilution":0,"StateProofID":[` + zeros32 + `,` + zeros32 + `],"UpdateRound":0}` + require.NoError(t, err) + require.Equal(t, expectedEncoding, string(encoding)) + } + t.Run("Positive", positiveTesting) + t.Run("Negative", negativeTesting) + t.Run("Structure", structureTesting) + +} + +func TestBaseOnlineAccountDataIsEmpty(t *testing.T) { + partitiontest.PartitionTest(t) + + positiveTesting := func(t *testing.T) { + var ba BaseOnlineAccountData + require.True(t, ba.IsEmpty()) + require.True(t, ba.IsVotingEmpty()) + ba.MicroAlgos.Raw = 100 + require.True(t, ba.IsVotingEmpty()) + ba.RewardsBase = 200 + require.True(t, ba.IsVotingEmpty()) + } + var empty BaseOnlineAccountData + negativeTesting := func(t *testing.T) { + for i := 0; i < 10; i++ { + randObj, _ := protocol.RandomizeObjectField(&BaseOnlineAccountData{}) + ba := randObj.(*BaseOnlineAccountData) + if *ba == empty { + continue + } + require.False(t, ba.IsEmpty(), "base account : %v", ba) + break + } + { + var ba BaseOnlineAccountData + ba.MicroAlgos.Raw = 100 + require.False(t, ba.IsEmpty()) + } + { + var ba BaseOnlineAccountData + ba.RewardsBase = 200 + require.False(t, ba.IsEmpty()) + } + } + structureTesting := func(t *testing.T) { + encoding, err := json.Marshal(&empty) + zeros32 := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" + expectedEncoding := `{"VoteID":[` + zeros32 + `],"SelectionID":[` + zeros32 + `],"VoteFirstValid":0,"VoteLastValid":0,"VoteKeyDilution":0,"StateProofID":[` + zeros32 + `,` + zeros32 + `],"MicroAlgos":{"Raw":0},"RewardsBase":0}` + require.NoError(t, err) + require.Equal(t, expectedEncoding, string(encoding)) + } + t.Run("Positive", positiveTesting) + t.Run("Negative", negativeTesting) + t.Run("Structure", structureTesting) + +} + +func TestBaseOnlineAccountDataGettersSetters(t *testing.T) { + partitiontest.PartitionTest(t) + + proto := config.Consensus[protocol.ConsensusCurrentVersion] + addr := ledgertesting.RandomAddress() + data := ledgertesting.RandomAccountData(1) + data.Status = basics.Online + crypto.RandBytes(data.VoteID[:]) + crypto.RandBytes(data.SelectionID[:]) + crypto.RandBytes(data.StateProofID[:]) + data.VoteFirstValid = basics.Round(crypto.RandUint64()) + data.VoteLastValid = basics.Round(crypto.RandUint64()) // int64 is the max sqlite can store + data.VoteKeyDilution = crypto.RandUint64() + + var ba BaseOnlineAccountData + ad := ledgercore.ToAccountData(data) + ba.SetCoreAccountData(&ad) + + require.Equal(t, data.MicroAlgos, ba.MicroAlgos) + require.Equal(t, data.RewardsBase, ba.RewardsBase) + require.Equal(t, data.VoteID, ba.VoteID) + require.Equal(t, data.SelectionID, ba.SelectionID) + require.Equal(t, data.VoteFirstValid, ba.VoteFirstValid) + require.Equal(t, data.VoteLastValid, ba.VoteLastValid) + require.Equal(t, data.VoteKeyDilution, ba.VoteKeyDilution) + require.Equal(t, data.StateProofID, ba.StateProofID) + + normBalance := basics.NormalizedOnlineAccountBalance(basics.Online, data.RewardsBase, data.MicroAlgos, proto) + require.Equal(t, normBalance, ba.NormalizedOnlineBalance(proto)) + oa := ba.GetOnlineAccount(addr, normBalance) + + require.Equal(t, addr, oa.Address) + require.Equal(t, ba.MicroAlgos, oa.MicroAlgos) + require.Equal(t, ba.RewardsBase, oa.RewardsBase) + require.Equal(t, normBalance, oa.NormalizedOnlineBalance) + require.Equal(t, ba.VoteFirstValid, oa.VoteFirstValid) + require.Equal(t, ba.VoteLastValid, oa.VoteLastValid) + require.Equal(t, ba.StateProofID, oa.StateProofID) + + rewardsLevel := uint64(1) + microAlgos, _, _ := basics.WithUpdatedRewards( + proto, basics.Online, oa.MicroAlgos, basics.MicroAlgos{}, ba.RewardsBase, rewardsLevel, + ) + oad := ba.GetOnlineAccountData(proto, rewardsLevel) + + require.Equal(t, microAlgos, oad.MicroAlgosWithRewards) + require.Equal(t, ba.VoteID, oad.VoteID) + require.Equal(t, ba.SelectionID, oad.SelectionID) + require.Equal(t, ba.StateProofID, oad.StateProofID) + require.Equal(t, ba.VoteFirstValid, oad.VoteFirstValid) + require.Equal(t, ba.VoteLastValid, oad.VoteLastValid) + require.Equal(t, ba.VoteKeyDilution, oad.VoteKeyDilution) +} + +func TestBaseVotingDataGettersSetters(t *testing.T) { + partitiontest.PartitionTest(t) + + data := ledgertesting.RandomAccountData(1) + data.Status = basics.Online + crypto.RandBytes(data.VoteID[:]) + crypto.RandBytes(data.SelectionID[:]) + crypto.RandBytes(data.StateProofID[:]) + data.VoteFirstValid = basics.Round(crypto.RandUint64()) + data.VoteLastValid = basics.Round(crypto.RandUint64()) // int64 is the max sqlite can store + data.VoteKeyDilution = crypto.RandUint64() + + var bv BaseVotingData + require.True(t, bv.IsEmpty()) + + ad := ledgercore.ToAccountData(data) + bv.SetCoreAccountData(&ad) + + require.False(t, bv.IsEmpty()) + require.Equal(t, data.VoteID, bv.VoteID) + require.Equal(t, data.SelectionID, bv.SelectionID) + require.Equal(t, data.VoteFirstValid, bv.VoteFirstValid) + require.Equal(t, data.VoteLastValid, bv.VoteLastValid) + require.Equal(t, data.VoteKeyDilution, bv.VoteKeyDilution) + require.Equal(t, data.StateProofID, bv.StateProofID) +} + +func TestBaseOnlineAccountDataReflect(t *testing.T) { + partitiontest.PartitionTest(t) + + require.Equal(t, 4, reflect.TypeOf(BaseOnlineAccountData{}).NumField(), "update all getters and setters for baseOnlineAccountData and change the field count") +} + +func TestBaseVotingDataReflect(t *testing.T) { + partitiontest.PartitionTest(t) + + require.Equal(t, 7, reflect.TypeOf(BaseVotingData{}).NumField(), "update all getters and setters for baseVotingData and change the field count") +} diff --git a/ledger/store/hashing.go b/ledger/store/hashing.go new file mode 100644 index 0000000000..40a5b6c311 --- /dev/null +++ b/ledger/store/hashing.go @@ -0,0 +1,135 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "encoding/binary" + "fmt" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" +) + +// HashKind enumerates the possible data types hashed into a catchpoint merkle +// trie. Each merkle trie hash includes the HashKind byte at a known-offset. +// By encoding HashKind at a known-offset, it's possible for hash readers to +// disambiguate the hashed resource. +// +//go:generate stringer -type=HashKind +//msgp:ignore HashKind +type HashKind byte + +// Defines known kinds of hashes. Changing an enum ordinal value is a +// breaking change. +const ( + AccountHK HashKind = iota + AssetHK + AppHK + KvHK +) + +// HashKindEncodingIndex defines the []byte offset where the hash kind is +// encoded. +const HashKindEncodingIndex = 4 + +// AccountHashBuilder calculates the hash key used for the trie by combining the account address and the account data +func AccountHashBuilder(addr basics.Address, accountData basics.AccountData, encodedAccountData []byte) []byte { + hash := make([]byte, 4+crypto.DigestSize) + // write out the lowest 32 bits of the reward base. This should improve the caching of the trie by allowing + // recent updated to be in-cache, and "older" nodes will be left alone. + for i, rewards := 3, accountData.RewardsBase; i >= 0; i, rewards = i-1, rewards>>8 { + // the following takes the rewards & 255 -> hash[i] + hash[i] = byte(rewards) + } + entryHash := crypto.Hash(append(addr[:], encodedAccountData[:]...)) + copy(hash[4:], entryHash[:]) + return hash[:] +} + +// AccountHashBuilderV6 calculates the hash key used for the trie by combining the account address and the account data +func AccountHashBuilderV6(addr basics.Address, accountData *BaseAccountData, encodedAccountData []byte) []byte { + hashIntPrefix := accountData.UpdateRound + if hashIntPrefix == 0 { + hashIntPrefix = accountData.RewardsBase + } + hash := hashBufV6(hashIntPrefix, AccountHK) + // write out the lowest 32 bits of the reward base. This should improve the caching of the trie by allowing + // recent updated to be in-cache, and "older" nodes will be left alone. + + prehash := make([]byte, crypto.DigestSize+len(encodedAccountData)) + copy(prehash[:], addr[:]) + copy(prehash[crypto.DigestSize:], encodedAccountData[:]) + + return finishV6(hash, prehash) +} + +// ResourcesHashBuilderV6 calculates the hash key used for the trie by combining the creatable's resource data and its index +func ResourcesHashBuilderV6(rd *ResourcesData, addr basics.Address, cidx basics.CreatableIndex, updateRound uint64, encodedResourceData []byte) ([]byte, error) { + hk, err := rdGetCreatableHashKind(rd, addr, cidx) + if err != nil { + return nil, err + } + + hash := hashBufV6(updateRound, hk) + + prehash := make([]byte, 8+crypto.DigestSize+len(encodedResourceData)) + copy(prehash[:], addr[:]) + binary.LittleEndian.PutUint64(prehash[crypto.DigestSize:], uint64(cidx)) + copy(prehash[crypto.DigestSize+8:], encodedResourceData[:]) + + return finishV6(hash, prehash), nil +} + +func rdGetCreatableHashKind(rd *ResourcesData, a basics.Address, ci basics.CreatableIndex) (HashKind, error) { + if rd.IsAsset() { + return AssetHK, nil + } else if rd.IsApp() { + return AppHK, nil + } + return AccountHK, fmt.Errorf("unknown creatable for addr %s, aidx %d, data %v", a.String(), ci, rd) +} + +// KvHashBuilderV6 calculates the hash key used for the trie by combining the key and value +func KvHashBuilderV6(key string, value []byte) []byte { + hash := hashBufV6(0, KvHK) + + prehash := make([]byte, len(key)+len(value)) + copy(prehash[:], key) + copy(prehash[len(key):], value) + + return finishV6(hash, prehash) +} + +func hashBufV6(affinity uint64, kind HashKind) []byte { + hash := make([]byte, 4+crypto.DigestSize) + // write out the lowest 32 bits of the affinity value. This should improve + // the caching of the trie by allowing recent updates to be in-cache, and + // "older" nodes will be left alone. + for i, prefix := 3, affinity; i >= 0; i, prefix = i-1, prefix>>8 { + // the following takes the prefix & 255 -> hash[i] + hash[i] = byte(prefix) + } + hash[HashKindEncodingIndex] = byte(kind) + return hash +} + +func finishV6(v6hash []byte, prehash []byte) []byte { + entryHash := crypto.Hash(prehash) + copy(v6hash[5:], entryHash[1:]) + return v6hash[:] + +} diff --git a/ledger/store/hashkind_string.go b/ledger/store/hashkind_string.go new file mode 100644 index 0000000000..3b60c2f1e9 --- /dev/null +++ b/ledger/store/hashkind_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -type=HashKind"; DO NOT EDIT. + +package store + +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[AccountHK-0] + _ = x[AssetHK-1] + _ = x[AppHK-2] + _ = x[KvHK-3] +} + +const _HashKind_name = "AccountHKAssetHKAppHKKvHK" + +var _HashKind_index = [...]uint8{0, 9, 16, 21, 25} + +func (i HashKind) String() string { + if i >= HashKind(len(_HashKind_index)-1) { + return "HashKind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _HashKind_name[_HashKind_index[i]:_HashKind_index[i+1]] +} diff --git a/ledger/store/interface.go b/ledger/store/interface.go new file mode 100644 index 0000000000..c9f06ee53d --- /dev/null +++ b/ledger/store/interface.go @@ -0,0 +1,109 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "context" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" +) + +// AccountsWriter is the write interface for: +// - accounts, resources, app kvs, creatables +type AccountsWriter interface { + InsertAccount(addr basics.Address, normBalance uint64, data BaseAccountData) (rowid int64, err error) + DeleteAccount(rowid int64) (rowsAffected int64, err error) + UpdateAccount(rowid int64, normBalance uint64, data BaseAccountData) (rowsAffected int64, err error) + + InsertResource(addrid int64, aidx basics.CreatableIndex, data ResourcesData) (rowid int64, err error) + DeleteResource(addrid int64, aidx basics.CreatableIndex) (rowsAffected int64, err error) + UpdateResource(addrid int64, aidx basics.CreatableIndex, data ResourcesData) (rowsAffected int64, err error) + + UpsertKvPair(key string, value []byte) error + DeleteKvPair(key string) error + + InsertCreatable(cidx basics.CreatableIndex, ctype basics.CreatableType, creator []byte) (rowid int64, err error) + DeleteCreatable(cidx basics.CreatableIndex, ctype basics.CreatableType) (rowsAffected int64, err error) + + Close() +} + +// AccountsReader is the read interface for: +// - accounts, resources, app kvs, creatables +type AccountsReader interface { + ListCreatables(maxIdx basics.CreatableIndex, maxResults uint64, ctype basics.CreatableType) (results []basics.CreatableLocator, dbRound basics.Round, err error) + + LookupAccount(addr basics.Address) (data PersistedAccountData, err error) + + LookupResources(addr basics.Address, aidx basics.CreatableIndex, ctype basics.CreatableType) (data PersistedResourcesData, err error) + LookupAllResources(addr basics.Address) (data []PersistedResourcesData, rnd basics.Round, err error) + + LookupKeyValue(key string) (pv PersistedKVData, err error) + LookupKeysByPrefix(prefix string, maxKeyNum uint64, results map[string]bool, resultCount uint64) (round basics.Round, err error) + + LookupCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (addr basics.Address, ok bool, dbRound basics.Round, err error) + + Close() +} + +// OnlineAccountsWriter is the write interface for: +// - online accounts +type OnlineAccountsWriter interface { + InsertOnlineAccount(addr basics.Address, normBalance uint64, data BaseOnlineAccountData, updRound uint64, voteLastValid uint64) (rowid int64, err error) + + Close() +} + +// OnlineAccountsReader is the read interface for: +// - online accounts +type OnlineAccountsReader interface { + LookupOnline(addr basics.Address, rnd basics.Round) (data PersistedOnlineAccountData, err error) + LookupOnlineTotalsHistory(round basics.Round) (basics.MicroAlgos, error) + LookupOnlineHistory(addr basics.Address) (result []PersistedOnlineAccountData, rnd basics.Round, err error) + + Close() +} + +// CatchpointWriter is the write interface for: +// - catchpoints +type CatchpointWriter interface { + StoreCatchpoint(ctx context.Context, round basics.Round, fileName string, catchpoint string, fileSize int64) (err error) + + WriteCatchpointStateUint64(ctx context.Context, stateName CatchpointState, setValue uint64) (err error) + WriteCatchpointStateString(ctx context.Context, stateName CatchpointState, setValue string) (err error) + + InsertUnfinishedCatchpoint(ctx context.Context, round basics.Round, blockHash crypto.Digest) error + DeleteUnfinishedCatchpoint(ctx context.Context, round basics.Round) error + DeleteOldCatchpointFirstStageInfo(ctx context.Context, maxRoundToDelete basics.Round) error + + DeleteStoredCatchpoints(ctx context.Context, dbDirectory string) (err error) +} + +// CatchpointReader is the read interface for: +// - catchpoints +type CatchpointReader interface { + GetCatchpoint(ctx context.Context, round basics.Round) (fileName string, catchpoint string, fileSize int64, err error) + GetOldestCatchpointFiles(ctx context.Context, fileCount int, filesToKeep int) (fileNames map[basics.Round]string, err error) + + ReadCatchpointStateUint64(ctx context.Context, stateName CatchpointState) (val uint64, err error) + ReadCatchpointStateString(ctx context.Context, stateName CatchpointState) (val string, err error) + + SelectUnfinishedCatchpoints(ctx context.Context) ([]UnfinishedCatchpointRecord, error) + SelectCatchpointFirstStageInfo(ctx context.Context, round basics.Round) (CatchpointFirstStageInfo, bool /*exists*/, error) + SelectOldCatchpointFirstStageInfoRounds(ctx context.Context, maxRound basics.Round) ([]basics.Round, error) +} diff --git a/ledger/store/merkle_commiter.go b/ledger/store/merkle_commiter.go new file mode 100644 index 0000000000..3d837c495a --- /dev/null +++ b/ledger/store/merkle_commiter.go @@ -0,0 +1,75 @@ +// Copyright (C) 2019-2022 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 store + +import "database/sql" + +// MerkleCommitter allows storing and loading merkletrie pages from a sqlite database. +// +//msgp:ignore MerkleCommitter +type MerkleCommitter struct { + tx *sql.Tx + deleteStmt *sql.Stmt + insertStmt *sql.Stmt + selectStmt *sql.Stmt +} + +// MakeMerkleCommitter creates a MerkleCommitter object that implements the merkletrie.Committer interface allowing storing and loading +// merkletrie pages from a sqlite database. +func MakeMerkleCommitter(tx *sql.Tx, staging bool) (mc *MerkleCommitter, err error) { + mc = &MerkleCommitter{tx: tx} + accountHashesTable := "accounthashes" + if staging { + accountHashesTable = "catchpointaccounthashes" + } + mc.deleteStmt, err = tx.Prepare("DELETE FROM " + accountHashesTable + " WHERE id=?") + if err != nil { + return nil, err + } + mc.insertStmt, err = tx.Prepare("INSERT OR REPLACE INTO " + accountHashesTable + "(id, data) VALUES(?, ?)") + if err != nil { + return nil, err + } + mc.selectStmt, err = tx.Prepare("SELECT data FROM " + accountHashesTable + " WHERE id = ?") + if err != nil { + return nil, err + } + return mc, nil +} + +// StorePage is the merkletrie.Committer interface implementation, stores a single page in a sqlite database table. +func (mc *MerkleCommitter) StorePage(page uint64, content []byte) error { + if len(content) == 0 { + _, err := mc.deleteStmt.Exec(page) + return err + } + _, err := mc.insertStmt.Exec(page, content) + return err +} + +// LoadPage is the merkletrie.Committer interface implementation, load a single page from a sqlite database table. +func (mc *MerkleCommitter) LoadPage(page uint64) (content []byte, err error) { + err = mc.selectStmt.QueryRow(page).Scan(&content) + if err == sql.ErrNoRows { + content = nil + err = nil + return + } else if err != nil { + return nil, err + } + return content, nil +} diff --git a/ledger/store/msgp_gen.go b/ledger/store/msgp_gen.go new file mode 100644 index 0000000000..58d544780e --- /dev/null +++ b/ledger/store/msgp_gen.go @@ -0,0 +1,2583 @@ +package store + +// Code generated by github.com/algorand/msgp DO NOT EDIT. + +import ( + "github.com/algorand/msgp/msgp" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" +) + +// The following msgp objects are implemented in this file: +// BaseAccountData +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// BaseOnlineAccountData +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// BaseVotingData +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// CatchpointFirstStageInfo +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// ResourceFlags +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// ResourcesData +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// TxTailRound +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// TxTailRoundLease +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + +// MarshalMsg implements msgp.Marshaler +func (z *BaseAccountData) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(21) + var zb0001Mask uint32 /* 23 bits */ + if (*z).BaseVotingData.VoteID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x1 + } + if (*z).BaseVotingData.SelectionID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).BaseVotingData.VoteFirstValid.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x4 + } + if (*z).BaseVotingData.VoteLastValid.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x8 + } + if (*z).BaseVotingData.VoteKeyDilution == 0 { + zb0001Len-- + zb0001Mask |= 0x10 + } + if (*z).BaseVotingData.StateProofID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x20 + } + if (*z).Status.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x100 + } + if (*z).MicroAlgos.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x200 + } + if (*z).RewardsBase == 0 { + zb0001Len-- + zb0001Mask |= 0x400 + } + if (*z).RewardedMicroAlgos.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x800 + } + if (*z).AuthAddr.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x1000 + } + if (*z).TotalAppSchemaNumUint == 0 { + zb0001Len-- + zb0001Mask |= 0x2000 + } + if (*z).TotalAppSchemaNumByteSlice == 0 { + zb0001Len-- + zb0001Mask |= 0x4000 + } + if (*z).TotalExtraAppPages == 0 { + zb0001Len-- + zb0001Mask |= 0x8000 + } + if (*z).TotalAssetParams == 0 { + zb0001Len-- + zb0001Mask |= 0x10000 + } + if (*z).TotalAssets == 0 { + zb0001Len-- + zb0001Mask |= 0x20000 + } + if (*z).TotalAppParams == 0 { + zb0001Len-- + zb0001Mask |= 0x40000 + } + if (*z).TotalAppLocalStates == 0 { + zb0001Len-- + zb0001Mask |= 0x80000 + } + if (*z).TotalBoxes == 0 { + zb0001Len-- + zb0001Mask |= 0x100000 + } + if (*z).TotalBoxBytes == 0 { + zb0001Len-- + zb0001Mask |= 0x200000 + } + if (*z).UpdateRound == 0 { + zb0001Len-- + zb0001Mask |= 0x400000 + } + // variable map header, size zb0001Len + o = msgp.AppendMapHeader(o, zb0001Len) + if zb0001Len != 0 { + if (zb0001Mask & 0x1) == 0 { // if not empty + // string "A" + o = append(o, 0xa1, 0x41) + o = (*z).BaseVotingData.VoteID.MarshalMsg(o) + } + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "B" + o = append(o, 0xa1, 0x42) + o = (*z).BaseVotingData.SelectionID.MarshalMsg(o) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "C" + o = append(o, 0xa1, 0x43) + o = (*z).BaseVotingData.VoteFirstValid.MarshalMsg(o) + } + if (zb0001Mask & 0x8) == 0 { // if not empty + // string "D" + o = append(o, 0xa1, 0x44) + o = (*z).BaseVotingData.VoteLastValid.MarshalMsg(o) + } + if (zb0001Mask & 0x10) == 0 { // if not empty + // string "E" + o = append(o, 0xa1, 0x45) + o = msgp.AppendUint64(o, (*z).BaseVotingData.VoteKeyDilution) + } + if (zb0001Mask & 0x20) == 0 { // if not empty + // string "F" + o = append(o, 0xa1, 0x46) + o = (*z).BaseVotingData.StateProofID.MarshalMsg(o) + } + if (zb0001Mask & 0x100) == 0 { // if not empty + // string "a" + o = append(o, 0xa1, 0x61) + o = (*z).Status.MarshalMsg(o) + } + if (zb0001Mask & 0x200) == 0 { // if not empty + // string "b" + o = append(o, 0xa1, 0x62) + o = (*z).MicroAlgos.MarshalMsg(o) + } + if (zb0001Mask & 0x400) == 0 { // if not empty + // string "c" + o = append(o, 0xa1, 0x63) + o = msgp.AppendUint64(o, (*z).RewardsBase) + } + if (zb0001Mask & 0x800) == 0 { // if not empty + // string "d" + o = append(o, 0xa1, 0x64) + o = (*z).RewardedMicroAlgos.MarshalMsg(o) + } + if (zb0001Mask & 0x1000) == 0 { // if not empty + // string "e" + o = append(o, 0xa1, 0x65) + o = (*z).AuthAddr.MarshalMsg(o) + } + if (zb0001Mask & 0x2000) == 0 { // if not empty + // string "f" + o = append(o, 0xa1, 0x66) + o = msgp.AppendUint64(o, (*z).TotalAppSchemaNumUint) + } + if (zb0001Mask & 0x4000) == 0 { // if not empty + // string "g" + o = append(o, 0xa1, 0x67) + o = msgp.AppendUint64(o, (*z).TotalAppSchemaNumByteSlice) + } + if (zb0001Mask & 0x8000) == 0 { // if not empty + // string "h" + o = append(o, 0xa1, 0x68) + o = msgp.AppendUint32(o, (*z).TotalExtraAppPages) + } + if (zb0001Mask & 0x10000) == 0 { // if not empty + // string "i" + o = append(o, 0xa1, 0x69) + o = msgp.AppendUint64(o, (*z).TotalAssetParams) + } + if (zb0001Mask & 0x20000) == 0 { // if not empty + // string "j" + o = append(o, 0xa1, 0x6a) + o = msgp.AppendUint64(o, (*z).TotalAssets) + } + if (zb0001Mask & 0x40000) == 0 { // if not empty + // string "k" + o = append(o, 0xa1, 0x6b) + o = msgp.AppendUint64(o, (*z).TotalAppParams) + } + if (zb0001Mask & 0x80000) == 0 { // if not empty + // string "l" + o = append(o, 0xa1, 0x6c) + o = msgp.AppendUint64(o, (*z).TotalAppLocalStates) + } + if (zb0001Mask & 0x100000) == 0 { // if not empty + // string "m" + o = append(o, 0xa1, 0x6d) + o = msgp.AppendUint64(o, (*z).TotalBoxes) + } + if (zb0001Mask & 0x200000) == 0 { // if not empty + // string "n" + o = append(o, 0xa1, 0x6e) + o = msgp.AppendUint64(o, (*z).TotalBoxBytes) + } + if (zb0001Mask & 0x400000) == 0 { // if not empty + // string "z" + o = append(o, 0xa1, 0x7a) + o = msgp.AppendUint64(o, (*z).UpdateRound) + } + } + return +} + +func (_ *BaseAccountData) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*BaseAccountData) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *BaseAccountData) 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-- + bts, err = (*z).Status.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Status") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "MicroAlgos") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "RewardsBase") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).RewardedMicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "RewardedMicroAlgos") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).AuthAddr.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AuthAddr") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalAppSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchemaNumUint") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalAppSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchemaNumByteSlice") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalExtraAppPages, bts, err = msgp.ReadUint32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalExtraAppPages") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalAssetParams, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAssetParams") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalAssets, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAssets") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalAppParams, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppParams") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalAppLocalStates, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppLocalStates") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalBoxes, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalBoxes") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalBoxBytes, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalBoxBytes") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.VoteID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteID") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.SelectionID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "SelectionID") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.VoteFirstValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.VoteLastValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).BaseVotingData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.StateProofID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "StateProofID") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).UpdateRound, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "UpdateRound") + 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) = BaseAccountData{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "a": + bts, err = (*z).Status.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Status") + return + } + case "b": + bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "MicroAlgos") + return + } + case "c": + (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "RewardsBase") + return + } + case "d": + bts, err = (*z).RewardedMicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "RewardedMicroAlgos") + return + } + case "e": + bts, err = (*z).AuthAddr.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AuthAddr") + return + } + case "f": + (*z).TotalAppSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchemaNumUint") + return + } + case "g": + (*z).TotalAppSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchemaNumByteSlice") + return + } + case "h": + (*z).TotalExtraAppPages, bts, err = msgp.ReadUint32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalExtraAppPages") + return + } + case "i": + (*z).TotalAssetParams, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAssetParams") + return + } + case "j": + (*z).TotalAssets, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAssets") + return + } + case "k": + (*z).TotalAppParams, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppParams") + return + } + case "l": + (*z).TotalAppLocalStates, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppLocalStates") + return + } + case "m": + (*z).TotalBoxes, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalBoxes") + return + } + case "n": + (*z).TotalBoxBytes, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalBoxBytes") + return + } + case "A": + bts, err = (*z).BaseVotingData.VoteID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteID") + return + } + case "B": + bts, err = (*z).BaseVotingData.SelectionID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "SelectionID") + return + } + case "C": + bts, err = (*z).BaseVotingData.VoteFirstValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteFirstValid") + return + } + case "D": + bts, err = (*z).BaseVotingData.VoteLastValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteLastValid") + return + } + case "E": + (*z).BaseVotingData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "VoteKeyDilution") + return + } + case "F": + bts, err = (*z).BaseVotingData.StateProofID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "StateProofID") + return + } + case "z": + (*z).UpdateRound, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "UpdateRound") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *BaseAccountData) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*BaseAccountData) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *BaseAccountData) Msgsize() (s int) { + s = 3 + 2 + (*z).Status.Msgsize() + 2 + (*z).MicroAlgos.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).RewardedMicroAlgos.Msgsize() + 2 + (*z).AuthAddr.Msgsize() + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint32Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + (*z).BaseVotingData.VoteID.Msgsize() + 2 + (*z).BaseVotingData.SelectionID.Msgsize() + 2 + (*z).BaseVotingData.VoteFirstValid.Msgsize() + 2 + (*z).BaseVotingData.VoteLastValid.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).BaseVotingData.StateProofID.Msgsize() + 2 + msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z *BaseAccountData) MsgIsZero() bool { + return ((*z).Status.MsgIsZero()) && ((*z).MicroAlgos.MsgIsZero()) && ((*z).RewardsBase == 0) && ((*z).RewardedMicroAlgos.MsgIsZero()) && ((*z).AuthAddr.MsgIsZero()) && ((*z).TotalAppSchemaNumUint == 0) && ((*z).TotalAppSchemaNumByteSlice == 0) && ((*z).TotalExtraAppPages == 0) && ((*z).TotalAssetParams == 0) && ((*z).TotalAssets == 0) && ((*z).TotalAppParams == 0) && ((*z).TotalAppLocalStates == 0) && ((*z).TotalBoxes == 0) && ((*z).TotalBoxBytes == 0) && ((*z).BaseVotingData.VoteID.MsgIsZero()) && ((*z).BaseVotingData.SelectionID.MsgIsZero()) && ((*z).BaseVotingData.VoteFirstValid.MsgIsZero()) && ((*z).BaseVotingData.VoteLastValid.MsgIsZero()) && ((*z).BaseVotingData.VoteKeyDilution == 0) && ((*z).BaseVotingData.StateProofID.MsgIsZero()) && ((*z).UpdateRound == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z *BaseOnlineAccountData) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(8) + var zb0001Mask uint16 /* 10 bits */ + if (*z).BaseVotingData.VoteID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x1 + } + if (*z).BaseVotingData.SelectionID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).BaseVotingData.VoteFirstValid.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x4 + } + if (*z).BaseVotingData.VoteLastValid.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x8 + } + if (*z).BaseVotingData.VoteKeyDilution == 0 { + zb0001Len-- + zb0001Mask |= 0x10 + } + if (*z).BaseVotingData.StateProofID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x20 + } + if (*z).MicroAlgos.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x40 + } + if (*z).RewardsBase == 0 { + zb0001Len-- + zb0001Mask |= 0x80 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x1) == 0 { // if not empty + // string "A" + o = append(o, 0xa1, 0x41) + o = (*z).BaseVotingData.VoteID.MarshalMsg(o) + } + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "B" + o = append(o, 0xa1, 0x42) + o = (*z).BaseVotingData.SelectionID.MarshalMsg(o) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "C" + o = append(o, 0xa1, 0x43) + o = (*z).BaseVotingData.VoteFirstValid.MarshalMsg(o) + } + if (zb0001Mask & 0x8) == 0 { // if not empty + // string "D" + o = append(o, 0xa1, 0x44) + o = (*z).BaseVotingData.VoteLastValid.MarshalMsg(o) + } + if (zb0001Mask & 0x10) == 0 { // if not empty + // string "E" + o = append(o, 0xa1, 0x45) + o = msgp.AppendUint64(o, (*z).BaseVotingData.VoteKeyDilution) + } + if (zb0001Mask & 0x20) == 0 { // if not empty + // string "F" + o = append(o, 0xa1, 0x46) + o = (*z).BaseVotingData.StateProofID.MarshalMsg(o) + } + if (zb0001Mask & 0x40) == 0 { // if not empty + // string "Y" + o = append(o, 0xa1, 0x59) + o = (*z).MicroAlgos.MarshalMsg(o) + } + if (zb0001Mask & 0x80) == 0 { // if not empty + // string "Z" + o = append(o, 0xa1, 0x5a) + o = msgp.AppendUint64(o, (*z).RewardsBase) + } + } + return +} + +func (_ *BaseOnlineAccountData) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*BaseOnlineAccountData) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *BaseOnlineAccountData) 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-- + bts, err = (*z).BaseVotingData.VoteID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteID") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.SelectionID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "SelectionID") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.VoteFirstValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.VoteLastValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).BaseVotingData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BaseVotingData.StateProofID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "StateProofID") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "MicroAlgos") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "RewardsBase") + 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) = BaseOnlineAccountData{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "A": + bts, err = (*z).BaseVotingData.VoteID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteID") + return + } + case "B": + bts, err = (*z).BaseVotingData.SelectionID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "SelectionID") + return + } + case "C": + bts, err = (*z).BaseVotingData.VoteFirstValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteFirstValid") + return + } + case "D": + bts, err = (*z).BaseVotingData.VoteLastValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteLastValid") + return + } + case "E": + (*z).BaseVotingData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "VoteKeyDilution") + return + } + case "F": + bts, err = (*z).BaseVotingData.StateProofID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "StateProofID") + return + } + case "Y": + bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "MicroAlgos") + return + } + case "Z": + (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "RewardsBase") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *BaseOnlineAccountData) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*BaseOnlineAccountData) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *BaseOnlineAccountData) Msgsize() (s int) { + s = 1 + 2 + (*z).BaseVotingData.VoteID.Msgsize() + 2 + (*z).BaseVotingData.SelectionID.Msgsize() + 2 + (*z).BaseVotingData.VoteFirstValid.Msgsize() + 2 + (*z).BaseVotingData.VoteLastValid.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).BaseVotingData.StateProofID.Msgsize() + 2 + (*z).MicroAlgos.Msgsize() + 2 + msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z *BaseOnlineAccountData) MsgIsZero() bool { + return ((*z).BaseVotingData.VoteID.MsgIsZero()) && ((*z).BaseVotingData.SelectionID.MsgIsZero()) && ((*z).BaseVotingData.VoteFirstValid.MsgIsZero()) && ((*z).BaseVotingData.VoteLastValid.MsgIsZero()) && ((*z).BaseVotingData.VoteKeyDilution == 0) && ((*z).BaseVotingData.StateProofID.MsgIsZero()) && ((*z).MicroAlgos.MsgIsZero()) && ((*z).RewardsBase == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z *BaseVotingData) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(6) + var zb0001Mask uint8 /* 7 bits */ + if (*z).VoteID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x1 + } + if (*z).SelectionID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).VoteFirstValid.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x4 + } + if (*z).VoteLastValid.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x8 + } + if (*z).VoteKeyDilution == 0 { + zb0001Len-- + zb0001Mask |= 0x10 + } + if (*z).StateProofID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x20 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x1) == 0 { // if not empty + // string "A" + o = append(o, 0xa1, 0x41) + o = (*z).VoteID.MarshalMsg(o) + } + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "B" + o = append(o, 0xa1, 0x42) + o = (*z).SelectionID.MarshalMsg(o) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "C" + o = append(o, 0xa1, 0x43) + o = (*z).VoteFirstValid.MarshalMsg(o) + } + if (zb0001Mask & 0x8) == 0 { // if not empty + // string "D" + o = append(o, 0xa1, 0x44) + o = (*z).VoteLastValid.MarshalMsg(o) + } + if (zb0001Mask & 0x10) == 0 { // if not empty + // string "E" + o = append(o, 0xa1, 0x45) + o = msgp.AppendUint64(o, (*z).VoteKeyDilution) + } + if (zb0001Mask & 0x20) == 0 { // if not empty + // string "F" + o = append(o, 0xa1, 0x46) + o = (*z).StateProofID.MarshalMsg(o) + } + } + return +} + +func (_ *BaseVotingData) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*BaseVotingData) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *BaseVotingData) 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-- + bts, err = (*z).VoteID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteID") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).SelectionID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "SelectionID") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).VoteFirstValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).VoteLastValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).StateProofID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "StateProofID") + 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) = BaseVotingData{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "A": + bts, err = (*z).VoteID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteID") + return + } + case "B": + bts, err = (*z).SelectionID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "SelectionID") + return + } + case "C": + bts, err = (*z).VoteFirstValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteFirstValid") + return + } + case "D": + bts, err = (*z).VoteLastValid.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteLastValid") + return + } + case "E": + (*z).VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "VoteKeyDilution") + return + } + case "F": + bts, err = (*z).StateProofID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "StateProofID") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *BaseVotingData) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*BaseVotingData) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *BaseVotingData) Msgsize() (s int) { + s = 1 + 2 + (*z).VoteID.Msgsize() + 2 + (*z).SelectionID.Msgsize() + 2 + (*z).VoteFirstValid.Msgsize() + 2 + (*z).VoteLastValid.Msgsize() + 2 + msgp.Uint64Size + 2 + (*z).StateProofID.Msgsize() + return +} + +// MsgIsZero returns whether this is a zero value +func (z *BaseVotingData) MsgIsZero() bool { + return ((*z).VoteID.MsgIsZero()) && ((*z).SelectionID.MsgIsZero()) && ((*z).VoteFirstValid.MsgIsZero()) && ((*z).VoteLastValid.MsgIsZero()) && ((*z).VoteKeyDilution == 0) && ((*z).StateProofID.MsgIsZero()) +} + +// MarshalMsg implements msgp.Marshaler +func (z *CatchpointFirstStageInfo) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(6) + var zb0001Mask uint8 /* 7 bits */ + if (*z).Totals.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).TotalAccounts == 0 { + zb0001Len-- + zb0001Mask |= 0x4 + } + if (*z).BiggestChunkLen == 0 { + zb0001Len-- + zb0001Mask |= 0x8 + } + if (*z).TotalChunks == 0 { + zb0001Len-- + zb0001Mask |= 0x10 + } + if (*z).TotalKVs == 0 { + zb0001Len-- + zb0001Mask |= 0x20 + } + if (*z).TrieBalancesHash.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x40 + } + // 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 = (*z).Totals.MarshalMsg(o) + } + 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 "biggestChunk" + o = append(o, 0xac, 0x62, 0x69, 0x67, 0x67, 0x65, 0x73, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b) + o = msgp.AppendUint64(o, (*z).BiggestChunkLen) + } + if (zb0001Mask & 0x10) == 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 & 0x20) == 0 { // if not empty + // string "kvsCount" + o = append(o, 0xa8, 0x6b, 0x76, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74) + o = msgp.AppendUint64(o, (*z).TotalKVs) + } + if (zb0001Mask & 0x40) == 0 { // if not empty + // string "trieBalancesHash" + o = append(o, 0xb0, 0x74, 0x72, 0x69, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x48, 0x61, 0x73, 0x68) + o = (*z).TrieBalancesHash.MarshalMsg(o) + } + } + return +} + +func (_ *CatchpointFirstStageInfo) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*CatchpointFirstStageInfo) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *CatchpointFirstStageInfo) 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-- + bts, err = (*z).Totals.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Totals") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).TrieBalancesHash.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TrieBalancesHash") + return + } + } + 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).TotalKVs, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalKVs") + 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).BiggestChunkLen, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "BiggestChunkLen") + 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) = CatchpointFirstStageInfo{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "accountTotals": + bts, err = (*z).Totals.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Totals") + return + } + case "trieBalancesHash": + bts, err = (*z).TrieBalancesHash.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "TrieBalancesHash") + return + } + case "accountsCount": + (*z).TotalAccounts, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAccounts") + return + } + case "kvsCount": + (*z).TotalKVs, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalKVs") + return + } + case "chunksCount": + (*z).TotalChunks, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalChunks") + return + } + case "biggestChunk": + (*z).BiggestChunkLen, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "BiggestChunkLen") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *CatchpointFirstStageInfo) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*CatchpointFirstStageInfo) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *CatchpointFirstStageInfo) Msgsize() (s int) { + s = 1 + 14 + (*z).Totals.Msgsize() + 17 + (*z).TrieBalancesHash.Msgsize() + 14 + msgp.Uint64Size + 9 + msgp.Uint64Size + 12 + msgp.Uint64Size + 13 + msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z *CatchpointFirstStageInfo) MsgIsZero() bool { + return ((*z).Totals.MsgIsZero()) && ((*z).TrieBalancesHash.MsgIsZero()) && ((*z).TotalAccounts == 0) && ((*z).TotalKVs == 0) && ((*z).TotalChunks == 0) && ((*z).BiggestChunkLen == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z ResourceFlags) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendUint8(o, uint8(z)) + return +} + +func (_ ResourceFlags) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(ResourceFlags) + if !ok { + _, ok = (z).(*ResourceFlags) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *ResourceFlags) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint8 + zb0001, bts, err = msgp.ReadUint8Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = ResourceFlags(zb0001) + } + o = bts + return +} + +func (_ *ResourceFlags) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*ResourceFlags) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z ResourceFlags) Msgsize() (s int) { + s = msgp.Uint8Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z ResourceFlags) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z *ResourcesData) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0002Len := uint32(26) + var zb0002Mask uint32 /* 27 bits */ + if (*z).Total == 0 { + zb0002Len-- + zb0002Mask |= 0x2 + } + if (*z).Decimals == 0 { + zb0002Len-- + zb0002Mask |= 0x4 + } + if (*z).DefaultFrozen == false { + zb0002Len-- + zb0002Mask |= 0x8 + } + if (*z).UnitName == "" { + zb0002Len-- + zb0002Mask |= 0x10 + } + if (*z).AssetName == "" { + zb0002Len-- + zb0002Mask |= 0x20 + } + if (*z).URL == "" { + zb0002Len-- + zb0002Mask |= 0x40 + } + if (*z).MetadataHash == ([32]byte{}) { + zb0002Len-- + zb0002Mask |= 0x80 + } + if (*z).Manager.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x100 + } + if (*z).Reserve.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x200 + } + if (*z).Freeze.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x400 + } + if (*z).Clawback.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x800 + } + if (*z).Amount == 0 { + zb0002Len-- + zb0002Mask |= 0x1000 + } + if (*z).Frozen == false { + zb0002Len-- + zb0002Mask |= 0x2000 + } + if (*z).SchemaNumUint == 0 { + zb0002Len-- + zb0002Mask |= 0x4000 + } + if (*z).SchemaNumByteSlice == 0 { + zb0002Len-- + zb0002Mask |= 0x8000 + } + if (*z).KeyValue.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x10000 + } + if len((*z).ApprovalProgram) == 0 { + zb0002Len-- + zb0002Mask |= 0x20000 + } + if len((*z).ClearStateProgram) == 0 { + zb0002Len-- + zb0002Mask |= 0x40000 + } + if (*z).GlobalState.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x80000 + } + if (*z).LocalStateSchemaNumUint == 0 { + zb0002Len-- + zb0002Mask |= 0x100000 + } + if (*z).LocalStateSchemaNumByteSlice == 0 { + zb0002Len-- + zb0002Mask |= 0x200000 + } + if (*z).GlobalStateSchemaNumUint == 0 { + zb0002Len-- + zb0002Mask |= 0x400000 + } + if (*z).GlobalStateSchemaNumByteSlice == 0 { + zb0002Len-- + zb0002Mask |= 0x800000 + } + if (*z).ExtraProgramPages == 0 { + zb0002Len-- + zb0002Mask |= 0x1000000 + } + if (*z).ResourceFlags == 0 { + zb0002Len-- + zb0002Mask |= 0x2000000 + } + if (*z).UpdateRound == 0 { + zb0002Len-- + zb0002Mask |= 0x4000000 + } + // variable map header, size zb0002Len + o = msgp.AppendMapHeader(o, zb0002Len) + if zb0002Len != 0 { + if (zb0002Mask & 0x2) == 0 { // if not empty + // string "a" + o = append(o, 0xa1, 0x61) + o = msgp.AppendUint64(o, (*z).Total) + } + if (zb0002Mask & 0x4) == 0 { // if not empty + // string "b" + o = append(o, 0xa1, 0x62) + o = msgp.AppendUint32(o, (*z).Decimals) + } + if (zb0002Mask & 0x8) == 0 { // if not empty + // string "c" + o = append(o, 0xa1, 0x63) + o = msgp.AppendBool(o, (*z).DefaultFrozen) + } + if (zb0002Mask & 0x10) == 0 { // if not empty + // string "d" + o = append(o, 0xa1, 0x64) + o = msgp.AppendString(o, (*z).UnitName) + } + if (zb0002Mask & 0x20) == 0 { // if not empty + // string "e" + o = append(o, 0xa1, 0x65) + o = msgp.AppendString(o, (*z).AssetName) + } + if (zb0002Mask & 0x40) == 0 { // if not empty + // string "f" + o = append(o, 0xa1, 0x66) + o = msgp.AppendString(o, (*z).URL) + } + if (zb0002Mask & 0x80) == 0 { // if not empty + // string "g" + o = append(o, 0xa1, 0x67) + o = msgp.AppendBytes(o, ((*z).MetadataHash)[:]) + } + if (zb0002Mask & 0x100) == 0 { // if not empty + // string "h" + o = append(o, 0xa1, 0x68) + o = (*z).Manager.MarshalMsg(o) + } + if (zb0002Mask & 0x200) == 0 { // if not empty + // string "i" + o = append(o, 0xa1, 0x69) + o = (*z).Reserve.MarshalMsg(o) + } + if (zb0002Mask & 0x400) == 0 { // if not empty + // string "j" + o = append(o, 0xa1, 0x6a) + o = (*z).Freeze.MarshalMsg(o) + } + if (zb0002Mask & 0x800) == 0 { // if not empty + // string "k" + o = append(o, 0xa1, 0x6b) + o = (*z).Clawback.MarshalMsg(o) + } + if (zb0002Mask & 0x1000) == 0 { // if not empty + // string "l" + o = append(o, 0xa1, 0x6c) + o = msgp.AppendUint64(o, (*z).Amount) + } + if (zb0002Mask & 0x2000) == 0 { // if not empty + // string "m" + o = append(o, 0xa1, 0x6d) + o = msgp.AppendBool(o, (*z).Frozen) + } + if (zb0002Mask & 0x4000) == 0 { // if not empty + // string "n" + o = append(o, 0xa1, 0x6e) + o = msgp.AppendUint64(o, (*z).SchemaNumUint) + } + if (zb0002Mask & 0x8000) == 0 { // if not empty + // string "o" + o = append(o, 0xa1, 0x6f) + o = msgp.AppendUint64(o, (*z).SchemaNumByteSlice) + } + if (zb0002Mask & 0x10000) == 0 { // if not empty + // string "p" + o = append(o, 0xa1, 0x70) + o = (*z).KeyValue.MarshalMsg(o) + } + if (zb0002Mask & 0x20000) == 0 { // if not empty + // string "q" + o = append(o, 0xa1, 0x71) + o = msgp.AppendBytes(o, (*z).ApprovalProgram) + } + if (zb0002Mask & 0x40000) == 0 { // if not empty + // string "r" + o = append(o, 0xa1, 0x72) + o = msgp.AppendBytes(o, (*z).ClearStateProgram) + } + if (zb0002Mask & 0x80000) == 0 { // if not empty + // string "s" + o = append(o, 0xa1, 0x73) + o = (*z).GlobalState.MarshalMsg(o) + } + if (zb0002Mask & 0x100000) == 0 { // if not empty + // string "t" + o = append(o, 0xa1, 0x74) + o = msgp.AppendUint64(o, (*z).LocalStateSchemaNumUint) + } + if (zb0002Mask & 0x200000) == 0 { // if not empty + // string "u" + o = append(o, 0xa1, 0x75) + o = msgp.AppendUint64(o, (*z).LocalStateSchemaNumByteSlice) + } + if (zb0002Mask & 0x400000) == 0 { // if not empty + // string "v" + o = append(o, 0xa1, 0x76) + o = msgp.AppendUint64(o, (*z).GlobalStateSchemaNumUint) + } + if (zb0002Mask & 0x800000) == 0 { // if not empty + // string "w" + o = append(o, 0xa1, 0x77) + o = msgp.AppendUint64(o, (*z).GlobalStateSchemaNumByteSlice) + } + if (zb0002Mask & 0x1000000) == 0 { // if not empty + // string "x" + o = append(o, 0xa1, 0x78) + o = msgp.AppendUint32(o, (*z).ExtraProgramPages) + } + if (zb0002Mask & 0x2000000) == 0 { // if not empty + // string "y" + o = append(o, 0xa1, 0x79) + o = msgp.AppendUint8(o, uint8((*z).ResourceFlags)) + } + if (zb0002Mask & 0x4000000) == 0 { // if not empty + // string "z" + o = append(o, 0xa1, 0x7a) + o = msgp.AppendUint64(o, (*z).UpdateRound) + } + } + return +} + +func (_ *ResourcesData) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*ResourcesData) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *ResourcesData) 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 { + zb0002-- + (*z).Amount, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Amount") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).Frozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Frozen") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).SchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "SchemaNumUint") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).SchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "SchemaNumByteSlice") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = (*z).KeyValue.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KeyValue") + return + } + } + if zb0002 > 0 { + zb0002-- + var zb0004 int + zb0004, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") + return + } + if zb0004 > config.MaxAvailableAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0004), uint64(config.MaxAvailableAppProgramLen)) + return + } + (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") + return + } + } + if zb0002 > 0 { + zb0002-- + var zb0005 int + zb0005, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") + return + } + if zb0005 > config.MaxAvailableAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0005), uint64(config.MaxAvailableAppProgramLen)) + return + } + (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = (*z).GlobalState.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalState") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).LocalStateSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchemaNumUint") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).LocalStateSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchemaNumByteSlice") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).GlobalStateSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchemaNumUint") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).GlobalStateSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchemaNumByteSlice") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).ExtraProgramPages, bts, err = msgp.ReadUint32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ExtraProgramPages") + return + } + } + if zb0002 > 0 { + zb0002-- + { + var zb0006 uint8 + zb0006, bts, err = msgp.ReadUint8Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ResourceFlags") + return + } + (*z).ResourceFlags = ResourceFlags(zb0006) + } + } + if zb0002 > 0 { + zb0002-- + (*z).UpdateRound, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "UpdateRound") + 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) = ResourcesData{} + } + for zb0002 > 0 { + zb0002-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "a": + (*z).Total, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Total") + return + } + case "b": + (*z).Decimals, bts, err = msgp.ReadUint32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Decimals") + return + } + case "c": + (*z).DefaultFrozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "DefaultFrozen") + return + } + case "d": + (*z).UnitName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "UnitName") + return + } + case "e": + (*z).AssetName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AssetName") + return + } + case "f": + (*z).URL, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "URL") + return + } + case "g": + bts, err = msgp.ReadExactBytes(bts, ((*z).MetadataHash)[:]) + if err != nil { + err = msgp.WrapError(err, "MetadataHash") + return + } + case "h": + bts, err = (*z).Manager.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Manager") + return + } + case "i": + bts, err = (*z).Reserve.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Reserve") + return + } + case "j": + bts, err = (*z).Freeze.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Freeze") + return + } + case "k": + bts, err = (*z).Clawback.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Clawback") + return + } + case "l": + (*z).Amount, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Amount") + return + } + case "m": + (*z).Frozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Frozen") + return + } + case "n": + (*z).SchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "SchemaNumUint") + return + } + case "o": + (*z).SchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "SchemaNumByteSlice") + return + } + case "p": + bts, err = (*z).KeyValue.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "KeyValue") + return + } + case "q": + var zb0007 int + zb0007, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "ApprovalProgram") + return + } + if zb0007 > config.MaxAvailableAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0007), uint64(config.MaxAvailableAppProgramLen)) + return + } + (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) + if err != nil { + err = msgp.WrapError(err, "ApprovalProgram") + return + } + case "r": + var zb0008 int + zb0008, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "ClearStateProgram") + return + } + if zb0008 > config.MaxAvailableAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0008), uint64(config.MaxAvailableAppProgramLen)) + return + } + (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) + if err != nil { + err = msgp.WrapError(err, "ClearStateProgram") + return + } + case "s": + bts, err = (*z).GlobalState.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalState") + return + } + case "t": + (*z).LocalStateSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchemaNumUint") + return + } + case "u": + (*z).LocalStateSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchemaNumByteSlice") + return + } + case "v": + (*z).GlobalStateSchemaNumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchemaNumUint") + return + } + case "w": + (*z).GlobalStateSchemaNumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchemaNumByteSlice") + return + } + case "x": + (*z).ExtraProgramPages, bts, err = msgp.ReadUint32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ExtraProgramPages") + return + } + case "y": + { + var zb0009 uint8 + zb0009, bts, err = msgp.ReadUint8Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ResourceFlags") + return + } + (*z).ResourceFlags = ResourceFlags(zb0009) + } + case "z": + (*z).UpdateRound, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "UpdateRound") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *ResourcesData) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*ResourcesData) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *ResourcesData) Msgsize() (s int) { + s = 3 + 2 + msgp.Uint64Size + 2 + msgp.Uint32Size + 2 + msgp.BoolSize + 2 + msgp.StringPrefixSize + len((*z).UnitName) + 2 + msgp.StringPrefixSize + len((*z).AssetName) + 2 + msgp.StringPrefixSize + len((*z).URL) + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 2 + (*z).Manager.Msgsize() + 2 + (*z).Reserve.Msgsize() + 2 + (*z).Freeze.Msgsize() + 2 + (*z).Clawback.Msgsize() + 2 + msgp.Uint64Size + 2 + msgp.BoolSize + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + (*z).KeyValue.Msgsize() + 2 + msgp.BytesPrefixSize + len((*z).ApprovalProgram) + 2 + msgp.BytesPrefixSize + len((*z).ClearStateProgram) + 2 + (*z).GlobalState.Msgsize() + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint64Size + 2 + msgp.Uint32Size + 2 + msgp.Uint8Size + 2 + msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z *ResourcesData) 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()) && ((*z).Amount == 0) && ((*z).Frozen == false) && ((*z).SchemaNumUint == 0) && ((*z).SchemaNumByteSlice == 0) && ((*z).KeyValue.MsgIsZero()) && (len((*z).ApprovalProgram) == 0) && (len((*z).ClearStateProgram) == 0) && ((*z).GlobalState.MsgIsZero()) && ((*z).LocalStateSchemaNumUint == 0) && ((*z).LocalStateSchemaNumByteSlice == 0) && ((*z).GlobalStateSchemaNumUint == 0) && ((*z).GlobalStateSchemaNumByteSlice == 0) && ((*z).ExtraProgramPages == 0) && ((*z).ResourceFlags == 0) && ((*z).UpdateRound == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z *TxTailRound) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0004Len := uint32(4) + var zb0004Mask uint8 /* 5 bits */ + if (*z).Hdr.MsgIsZero() { + zb0004Len-- + zb0004Mask |= 0x2 + } + if len((*z).TxnIDs) == 0 { + zb0004Len-- + zb0004Mask |= 0x4 + } + if len((*z).Leases) == 0 { + zb0004Len-- + zb0004Mask |= 0x8 + } + if len((*z).LastValid) == 0 { + zb0004Len-- + zb0004Mask |= 0x10 + } + // variable map header, size zb0004Len + o = append(o, 0x80|uint8(zb0004Len)) + if zb0004Len != 0 { + if (zb0004Mask & 0x2) == 0 { // if not empty + // string "h" + o = append(o, 0xa1, 0x68) + o = (*z).Hdr.MarshalMsg(o) + } + if (zb0004Mask & 0x4) == 0 { // if not empty + // string "i" + o = append(o, 0xa1, 0x69) + if (*z).TxnIDs == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).TxnIDs))) + } + for zb0001 := range (*z).TxnIDs { + o = (*z).TxnIDs[zb0001].MarshalMsg(o) + } + } + if (zb0004Mask & 0x8) == 0 { // if not empty + // string "l" + o = append(o, 0xa1, 0x6c) + if (*z).Leases == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).Leases))) + } + for zb0003 := range (*z).Leases { + o = (*z).Leases[zb0003].MarshalMsg(o) + } + } + if (zb0004Mask & 0x10) == 0 { // if not empty + // string "v" + o = append(o, 0xa1, 0x76) + if (*z).LastValid == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).LastValid))) + } + for zb0002 := range (*z).LastValid { + o = (*z).LastValid[zb0002].MarshalMsg(o) + } + } + } + return +} + +func (_ *TxTailRound) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*TxTailRound) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *TxTailRound) 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-- + var zb0006 int + var zb0007 bool + zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TxnIDs") + return + } + if zb0007 { + (*z).TxnIDs = nil + } else if (*z).TxnIDs != nil && cap((*z).TxnIDs) >= zb0006 { + (*z).TxnIDs = ((*z).TxnIDs)[:zb0006] + } else { + (*z).TxnIDs = make([]transactions.Txid, zb0006) + } + for zb0001 := range (*z).TxnIDs { + bts, err = (*z).TxnIDs[zb0001].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TxnIDs", zb0001) + return + } + } + } + if zb0004 > 0 { + zb0004-- + var zb0008 int + var zb0009 bool + zb0008, zb0009, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LastValid") + return + } + if zb0009 { + (*z).LastValid = nil + } else if (*z).LastValid != nil && cap((*z).LastValid) >= zb0008 { + (*z).LastValid = ((*z).LastValid)[:zb0008] + } else { + (*z).LastValid = make([]basics.Round, zb0008) + } + for zb0002 := range (*z).LastValid { + bts, err = (*z).LastValid[zb0002].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LastValid", zb0002) + return + } + } + } + if zb0004 > 0 { + zb0004-- + var zb0010 int + var zb0011 bool + zb0010, zb0011, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Leases") + return + } + if zb0011 { + (*z).Leases = nil + } else if (*z).Leases != nil && cap((*z).Leases) >= zb0010 { + (*z).Leases = ((*z).Leases)[:zb0010] + } else { + (*z).Leases = make([]TxTailRoundLease, zb0010) + } + for zb0003 := range (*z).Leases { + bts, err = (*z).Leases[zb0003].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Leases", zb0003) + return + } + } + } + if zb0004 > 0 { + zb0004-- + bts, err = (*z).Hdr.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Hdr") + 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) = TxTailRound{} + } + for zb0004 > 0 { + zb0004-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "i": + var zb0012 int + var zb0013 bool + zb0012, zb0013, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "TxnIDs") + return + } + if zb0013 { + (*z).TxnIDs = nil + } else if (*z).TxnIDs != nil && cap((*z).TxnIDs) >= zb0012 { + (*z).TxnIDs = ((*z).TxnIDs)[:zb0012] + } else { + (*z).TxnIDs = make([]transactions.Txid, zb0012) + } + for zb0001 := range (*z).TxnIDs { + bts, err = (*z).TxnIDs[zb0001].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "TxnIDs", zb0001) + return + } + } + case "v": + var zb0014 int + var zb0015 bool + zb0014, zb0015, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "LastValid") + return + } + if zb0015 { + (*z).LastValid = nil + } else if (*z).LastValid != nil && cap((*z).LastValid) >= zb0014 { + (*z).LastValid = ((*z).LastValid)[:zb0014] + } else { + (*z).LastValid = make([]basics.Round, zb0014) + } + for zb0002 := range (*z).LastValid { + bts, err = (*z).LastValid[zb0002].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "LastValid", zb0002) + return + } + } + case "l": + var zb0016 int + var zb0017 bool + zb0016, zb0017, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Leases") + return + } + if zb0017 { + (*z).Leases = nil + } else if (*z).Leases != nil && cap((*z).Leases) >= zb0016 { + (*z).Leases = ((*z).Leases)[:zb0016] + } else { + (*z).Leases = make([]TxTailRoundLease, zb0016) + } + for zb0003 := range (*z).Leases { + bts, err = (*z).Leases[zb0003].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Leases", zb0003) + return + } + } + case "h": + bts, err = (*z).Hdr.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Hdr") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *TxTailRound) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*TxTailRound) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *TxTailRound) Msgsize() (s int) { + s = 1 + 2 + msgp.ArrayHeaderSize + for zb0001 := range (*z).TxnIDs { + s += (*z).TxnIDs[zb0001].Msgsize() + } + s += 2 + msgp.ArrayHeaderSize + for zb0002 := range (*z).LastValid { + s += (*z).LastValid[zb0002].Msgsize() + } + s += 2 + msgp.ArrayHeaderSize + for zb0003 := range (*z).Leases { + s += (*z).Leases[zb0003].Msgsize() + } + s += 2 + (*z).Hdr.Msgsize() + return +} + +// MsgIsZero returns whether this is a zero value +func (z *TxTailRound) MsgIsZero() bool { + return (len((*z).TxnIDs) == 0) && (len((*z).LastValid) == 0) && (len((*z).Leases) == 0) && ((*z).Hdr.MsgIsZero()) +} + +// MarshalMsg implements msgp.Marshaler +func (z *TxTailRoundLease) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0002Len := uint32(3) + var zb0002Mask uint8 /* 4 bits */ + if (*z).TxnIdx == 0 { + zb0002Len-- + zb0002Mask |= 0x1 + } + if (*z).Lease == ([32]byte{}) { + zb0002Len-- + zb0002Mask |= 0x4 + } + if (*z).Sender.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x8 + } + // variable map header, size zb0002Len + o = append(o, 0x80|uint8(zb0002Len)) + if zb0002Len != 0 { + if (zb0002Mask & 0x1) == 0 { // if not empty + // string "TxnIdx" + o = append(o, 0xa6, 0x54, 0x78, 0x6e, 0x49, 0x64, 0x78) + o = msgp.AppendUint64(o, (*z).TxnIdx) + } + if (zb0002Mask & 0x4) == 0 { // if not empty + // string "l" + o = append(o, 0xa1, 0x6c) + o = msgp.AppendBytes(o, ((*z).Lease)[:]) + } + if (zb0002Mask & 0x8) == 0 { // if not empty + // string "s" + o = append(o, 0xa1, 0x73) + o = (*z).Sender.MarshalMsg(o) + } + } + return +} + +func (_ *TxTailRoundLease) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*TxTailRoundLease) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *TxTailRoundLease) 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-- + bts, err = (*z).Sender.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Sender") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = msgp.ReadExactBytes(bts, ((*z).Lease)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Lease") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).TxnIdx, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TxnIdx") + 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) = TxTailRoundLease{} + } + for zb0002 > 0 { + zb0002-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "s": + bts, err = (*z).Sender.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Sender") + return + } + case "l": + bts, err = msgp.ReadExactBytes(bts, ((*z).Lease)[:]) + if err != nil { + err = msgp.WrapError(err, "Lease") + return + } + case "TxnIdx": + (*z).TxnIdx, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TxnIdx") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *TxTailRoundLease) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*TxTailRoundLease) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *TxTailRoundLease) Msgsize() (s int) { + s = 1 + 2 + (*z).Sender.Msgsize() + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 7 + msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z *TxTailRoundLease) MsgIsZero() bool { + return ((*z).Sender.MsgIsZero()) && ((*z).Lease == ([32]byte{})) && ((*z).TxnIdx == 0) +} diff --git a/ledger/store/msgp_gen_test.go b/ledger/store/msgp_gen_test.go new file mode 100644 index 0000000000..c903eec33d --- /dev/null +++ b/ledger/store/msgp_gen_test.go @@ -0,0 +1,435 @@ +//go:build !skip_msgp_testing +// +build !skip_msgp_testing + +package store + +// Code generated by github.com/algorand/msgp DO NOT EDIT. + +import ( + "testing" + + "github.com/algorand/msgp/msgp" + + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" +) + +func TestMarshalUnmarshalBaseAccountData(t *testing.T) { + partitiontest.PartitionTest(t) + v := BaseAccountData{} + bts := v.MarshalMsg(nil) + 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 TestRandomizedEncodingBaseAccountData(t *testing.T) { + protocol.RunEncodingTest(t, &BaseAccountData{}) +} + +func BenchmarkMarshalMsgBaseAccountData(b *testing.B) { + v := BaseAccountData{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgBaseAccountData(b *testing.B) { + v := BaseAccountData{} + 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 BenchmarkUnmarshalBaseAccountData(b *testing.B) { + v := BaseAccountData{} + 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 TestMarshalUnmarshalBaseOnlineAccountData(t *testing.T) { + partitiontest.PartitionTest(t) + v := BaseOnlineAccountData{} + bts := v.MarshalMsg(nil) + 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 TestRandomizedEncodingBaseOnlineAccountData(t *testing.T) { + protocol.RunEncodingTest(t, &BaseOnlineAccountData{}) +} + +func BenchmarkMarshalMsgBaseOnlineAccountData(b *testing.B) { + v := BaseOnlineAccountData{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgBaseOnlineAccountData(b *testing.B) { + v := BaseOnlineAccountData{} + 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 BenchmarkUnmarshalBaseOnlineAccountData(b *testing.B) { + v := BaseOnlineAccountData{} + 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 TestMarshalUnmarshalBaseVotingData(t *testing.T) { + partitiontest.PartitionTest(t) + v := BaseVotingData{} + bts := v.MarshalMsg(nil) + 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 TestRandomizedEncodingBaseVotingData(t *testing.T) { + protocol.RunEncodingTest(t, &BaseVotingData{}) +} + +func BenchmarkMarshalMsgBaseVotingData(b *testing.B) { + v := BaseVotingData{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgBaseVotingData(b *testing.B) { + v := BaseVotingData{} + 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 BenchmarkUnmarshalBaseVotingData(b *testing.B) { + v := BaseVotingData{} + 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 TestMarshalUnmarshalCatchpointFirstStageInfo(t *testing.T) { + partitiontest.PartitionTest(t) + v := CatchpointFirstStageInfo{} + bts := v.MarshalMsg(nil) + 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 TestRandomizedEncodingCatchpointFirstStageInfo(t *testing.T) { + protocol.RunEncodingTest(t, &CatchpointFirstStageInfo{}) +} + +func BenchmarkMarshalMsgCatchpointFirstStageInfo(b *testing.B) { + v := CatchpointFirstStageInfo{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgCatchpointFirstStageInfo(b *testing.B) { + v := CatchpointFirstStageInfo{} + 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 BenchmarkUnmarshalCatchpointFirstStageInfo(b *testing.B) { + v := CatchpointFirstStageInfo{} + 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 TestMarshalUnmarshalResourcesData(t *testing.T) { + partitiontest.PartitionTest(t) + v := ResourcesData{} + bts := v.MarshalMsg(nil) + 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 TestRandomizedEncodingResourcesData(t *testing.T) { + protocol.RunEncodingTest(t, &ResourcesData{}) +} + +func BenchmarkMarshalMsgResourcesData(b *testing.B) { + v := ResourcesData{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgResourcesData(b *testing.B) { + v := ResourcesData{} + 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 BenchmarkUnmarshalResourcesData(b *testing.B) { + v := ResourcesData{} + 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 TestMarshalUnmarshalTxTailRound(t *testing.T) { + partitiontest.PartitionTest(t) + v := TxTailRound{} + bts := v.MarshalMsg(nil) + 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 TestRandomizedEncodingTxTailRound(t *testing.T) { + protocol.RunEncodingTest(t, &TxTailRound{}) +} + +func BenchmarkMarshalMsgTxTailRound(b *testing.B) { + v := TxTailRound{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgTxTailRound(b *testing.B) { + v := TxTailRound{} + 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 BenchmarkUnmarshalTxTailRound(b *testing.B) { + v := TxTailRound{} + 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 TestMarshalUnmarshalTxTailRoundLease(t *testing.T) { + partitiontest.PartitionTest(t) + v := TxTailRoundLease{} + bts := v.MarshalMsg(nil) + 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 TestRandomizedEncodingTxTailRoundLease(t *testing.T) { + protocol.RunEncodingTest(t, &TxTailRoundLease{}) +} + +func BenchmarkMarshalMsgTxTailRoundLease(b *testing.B) { + v := TxTailRoundLease{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgTxTailRoundLease(b *testing.B) { + v := TxTailRoundLease{} + 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 BenchmarkUnmarshalTxTailRoundLease(b *testing.B) { + v := TxTailRoundLease{} + 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/ledger/store/schema.go b/ledger/store/schema.go new file mode 100644 index 0000000000..84bab3f1c0 --- /dev/null +++ b/ledger/store/schema.go @@ -0,0 +1,916 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "bytes" + "context" + "database/sql" + "encoding/hex" + "fmt" + "time" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto/merklesignature" + "github.com/algorand/go-algorand/crypto/merkletrie" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store/blockdb" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" + "github.com/mattn/go-sqlite3" +) + +var accountsSchema = []string{ + `CREATE TABLE IF NOT EXISTS acctrounds ( + id string primary key, + rnd integer)`, + `CREATE TABLE IF NOT EXISTS accounttotals ( + id string primary key, + online integer, + onlinerewardunits integer, + offline integer, + offlinerewardunits integer, + notparticipating integer, + notparticipatingrewardunits integer, + rewardslevel integer)`, + `CREATE TABLE IF NOT EXISTS accountbase ( + address blob primary key, + data blob)`, + `CREATE TABLE IF NOT EXISTS assetcreators ( + asset integer primary key, + creator blob)`, + `CREATE TABLE IF NOT EXISTS storedcatchpoints ( + round integer primary key, + filename text NOT NULL, + catchpoint text NOT NULL, + filesize size NOT NULL, + pinned integer NOT NULL)`, + `CREATE TABLE IF NOT EXISTS accounthashes ( + id integer primary key, + data blob)`, + `CREATE TABLE IF NOT EXISTS catchpointstate ( + id string primary key, + intval integer, + 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`, +} + +// createNormalizedOnlineBalanceIndexOnline handles onlineaccounts/catchpointonlineaccounts tables +func createNormalizedOnlineBalanceIndexOnline(idxname string, tablename string) string { + return fmt.Sprintf(`CREATE INDEX IF NOT EXISTS %s + ON %s ( normalizedonlinebalance, address )`, idxname, tablename) +} + +// createUniqueAddressBalanceIndex is sql query to create a uninque index on `address`. +func createUniqueAddressBalanceIndex(idxname string, tablename string) string { + return fmt.Sprintf(`CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (address)`, idxname, tablename) +} + +// createNormalizedOnlineBalanceIndex handles accountbase/catchpointbalances tables +func createNormalizedOnlineBalanceIndex(idxname string, tablename string) string { + return fmt.Sprintf(`CREATE INDEX IF NOT EXISTS %s + ON %s ( normalizedonlinebalance, address, data ) WHERE normalizedonlinebalance>0`, idxname, tablename) +} + +var createOnlineAccountIndex = []string{ + `ALTER TABLE accountbase + ADD COLUMN normalizedonlinebalance INTEGER`, + createNormalizedOnlineBalanceIndex("onlineaccountbals", "accountbase"), +} + +var createResourcesTable = []string{ + `CREATE TABLE IF NOT EXISTS resources ( + addrid INTEGER NOT NULL, + aidx INTEGER NOT NULL, + data BLOB NOT NULL, + PRIMARY KEY (addrid, aidx) ) WITHOUT ROWID`, +} + +var createBoxTable = []string{ + `CREATE TABLE IF NOT EXISTS kvstore ( + key blob primary key, + value blob)`, +} + +var createOnlineAccountsTable = []string{ + `CREATE TABLE IF NOT EXISTS onlineaccounts ( + address BLOB NOT NULL, + updround INTEGER NOT NULL, + normalizedonlinebalance INTEGER NOT NULL, + votelastvalid INTEGER NOT NULL, + data BLOB NOT NULL, + PRIMARY KEY (address, updround) )`, + createNormalizedOnlineBalanceIndexOnline("onlineaccountnorm", "onlineaccounts"), +} + +var createTxTailTable = []string{ + `CREATE TABLE IF NOT EXISTS txtail ( + rnd INTEGER PRIMARY KEY NOT NULL, + data BLOB NOT NULL)`, +} + +var createOnlineRoundParamsTable = []string{ + `CREATE TABLE IF NOT EXISTS onlineroundparamstail( + rnd INTEGER NOT NULL PRIMARY KEY, + data BLOB NOT NULL)`, // contains a msgp encoded OnlineRoundParamsData +} + +// Table containing some metadata for a future catchpoint. The `info` column +// contains a serialized object of type catchpointFirstStageInfo. +const createCatchpointFirstStageInfoTable = ` + CREATE TABLE IF NOT EXISTS catchpointfirststageinfo ( + round integer primary key NOT NULL, + info BLOB NOT NULL)` + +const createUnfinishedCatchpointsTable = ` + CREATE TABLE IF NOT EXISTS unfinishedcatchpoints ( + round integer primary key NOT NULL, + blockhash blob NOT NULL)` + +var accountsResetExprs = []string{ + `DROP TABLE IF EXISTS acctrounds`, + `DROP TABLE IF EXISTS accounttotals`, + `DROP TABLE IF EXISTS accountbase`, + `DROP TABLE IF EXISTS kvstore`, + `DROP TABLE IF EXISTS assetcreators`, + `DROP TABLE IF EXISTS storedcatchpoints`, + `DROP TABLE IF EXISTS catchpointstate`, + `DROP TABLE IF EXISTS accounthashes`, + `DROP TABLE IF EXISTS resources`, + `DROP TABLE IF EXISTS onlineaccounts`, + `DROP TABLE IF EXISTS txtail`, + `DROP TABLE IF EXISTS onlineroundparamstail`, + `DROP TABLE IF EXISTS catchpointfirststageinfo`, + `DROP TABLE IF EXISTS unfinishedcatchpoints`, +} + +// 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(9) + +// accountsInit fills the database using tx with initAccounts if the +// database has not been initialized yet. +// +// accountsInit returns nil if either it has initialized the database +// correctly, or if the database has already been initialized. +func accountsInit(tx *sql.Tx, initAccounts map[basics.Address]basics.AccountData, proto config.ConsensusParams) (newDatabase bool, err error) { + for _, tableCreate := range accountsSchema { + _, err = tx.Exec(tableCreate) + if err != nil { + return + } + } + + // 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 + } + } + } else if err != nil { + return + } + + _, err = tx.Exec("INSERT INTO acctrounds (id, rnd) VALUES ('acctbase', 0)") + if err == nil { + var ot basics.OverflowTracker + var totals ledgercore.AccountTotals + + for addr, data := range initAccounts { + _, err = tx.Exec("INSERT INTO accountbase (address, data) VALUES (?, ?)", + addr[:], protocol.Encode(&data)) + if err != nil { + return true, err + } + + ad := ledgercore.ToAccountData(data) + totals.AddAccount(proto, ad, &ot) + } + + if ot.Overflowed { + return true, fmt.Errorf("overflow computing totals") + } + + arw := NewAccountsSQLReaderWriter(tx) + err = arw.AccountsPutTotals(totals, false) + if err != nil { + return true, err + } + newDatabase = true + } else { + serr, ok := err.(sqlite3.Error) + // serr.Code is sqlite.ErrConstraint if the database has already been initialized; + // in that case, ignore the error and return nil. + if !ok || serr.Code != sqlite3.ErrConstraint { + return + } + + } + + return newDatabase, nil +} + +// accountsAddNormalizedBalance adds the normalizedonlinebalance column +// to the accountbase table. +func accountsAddNormalizedBalance(tx *sql.Tx, proto config.ConsensusParams) error { + var exists bool + err := tx.QueryRow("SELECT 1 FROM pragma_table_info('accountbase') WHERE name='normalizedonlinebalance'").Scan(&exists) + if err == nil { + // Already exists. + return nil + } + if err != sql.ErrNoRows { + return err + } + + for _, stmt := range createOnlineAccountIndex { + _, err := tx.Exec(stmt) + if err != nil { + return err + } + } + + rows, err := tx.Query("SELECT address, data FROM accountbase") + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var addrbuf []byte + var buf []byte + err = rows.Scan(&addrbuf, &buf) + if err != nil { + return err + } + + var data basics.AccountData + err = protocol.Decode(buf, &data) + if err != nil { + return err + } + + normBalance := data.NormalizedOnlineBalance(proto) + if normBalance > 0 { + _, err = tx.Exec("UPDATE accountbase SET normalizedonlinebalance=? WHERE address=?", normBalance, addrbuf) + if err != nil { + return err + } + } + } + + return rows.Err() +} + +// accountsCreateResourceTable creates the resource table in the database. +func accountsCreateResourceTable(ctx context.Context, tx *sql.Tx) error { + var exists bool + err := tx.QueryRowContext(ctx, "SELECT 1 FROM pragma_table_info('resources') WHERE name='addrid'").Scan(&exists) + if err == nil { + // Already exists. + return nil + } + if err != sql.ErrNoRows { + return err + } + for _, stmt := range createResourcesTable { + _, err = tx.ExecContext(ctx, stmt) + if err != nil { + return err + } + } + return nil +} + +func accountsCreateOnlineAccountsTable(ctx context.Context, tx *sql.Tx) error { + var exists bool + err := tx.QueryRowContext(ctx, "SELECT 1 FROM pragma_table_info('onlineaccounts') WHERE name='address'").Scan(&exists) + if err == nil { + // Already exists. + return nil + } + if err != sql.ErrNoRows { + return err + } + for _, stmt := range createOnlineAccountsTable { + _, err = tx.ExecContext(ctx, stmt) + if err != nil { + return err + } + } + return nil +} + +// accountsCreateBoxTable creates the KVStore table for box-storage in the database. +func accountsCreateBoxTable(ctx context.Context, tx *sql.Tx) error { + var exists bool + err := tx.QueryRow("SELECT 1 FROM pragma_table_info('kvstore') WHERE name='key'").Scan(&exists) + if err == nil { + // already exists + return nil + } + if err != sql.ErrNoRows { + return err + } + for _, stmt := range createBoxTable { + _, err = tx.ExecContext(ctx, stmt) + if err != nil { + return err + } + } + return nil +} + +func accountsCreateTxTailTable(ctx context.Context, tx *sql.Tx) (err error) { + for _, stmt := range createTxTailTable { + _, err = tx.ExecContext(ctx, stmt) + if err != nil { + return + } + } + return nil +} + +func accountsCreateOnlineRoundParamsTable(ctx context.Context, tx *sql.Tx) (err error) { + for _, stmt := range createOnlineRoundParamsTable { + _, err = tx.ExecContext(ctx, stmt) + if err != nil { + return + } + } + return nil +} + +func accountsCreateCatchpointFirstStageInfoTable(ctx context.Context, e db.Executable) error { + _, err := e.ExecContext(ctx, createCatchpointFirstStageInfoTable) + return err +} + +func accountsCreateUnfinishedCatchpointsTable(ctx context.Context, e db.Executable) error { + _, err := e.ExecContext(ctx, createUnfinishedCatchpointsTable) + return err +} + +// performResourceTableMigration migrate the database to use the resources table. +func performResourceTableMigration(ctx context.Context, tx *sql.Tx, log func(processed, total uint64)) (err error) { + now := time.Now().UnixNano() + idxnameBalances := fmt.Sprintf("onlineaccountbals_idx_%d", now) + idxnameAddress := fmt.Sprintf("accountbase_address_idx_%d", now) + + createNewAcctBase := []string{ + `CREATE TABLE IF NOT EXISTS accountbase_resources_migration ( + addrid INTEGER PRIMARY KEY NOT NULL, + address blob NOT NULL, + data blob, + normalizedonlinebalance INTEGER )`, + createNormalizedOnlineBalanceIndex(idxnameBalances, "accountbase_resources_migration"), + createUniqueAddressBalanceIndex(idxnameAddress, "accountbase_resources_migration"), + } + + applyNewAcctBase := []string{ + `ALTER TABLE accountbase RENAME TO accountbase_old`, + `ALTER TABLE accountbase_resources_migration RENAME TO accountbase`, + `DROP TABLE IF EXISTS accountbase_old`, + } + + for _, stmt := range createNewAcctBase { + _, err = tx.ExecContext(ctx, stmt) + if err != nil { + return err + } + } + var insertNewAcctBase *sql.Stmt + var insertResources *sql.Stmt + var insertNewAcctBaseNormBal *sql.Stmt + insertNewAcctBase, err = tx.PrepareContext(ctx, "INSERT INTO accountbase_resources_migration(address, data) VALUES(?, ?)") + if err != nil { + return err + } + defer insertNewAcctBase.Close() + + insertNewAcctBaseNormBal, err = tx.PrepareContext(ctx, "INSERT INTO accountbase_resources_migration(address, data, normalizedonlinebalance) VALUES(?, ?, ?)") + if err != nil { + return err + } + defer insertNewAcctBaseNormBal.Close() + + insertResources, err = tx.PrepareContext(ctx, "INSERT INTO resources(addrid, aidx, data) VALUES(?, ?, ?)") + if err != nil { + return err + } + defer insertResources.Close() + + var rows *sql.Rows + rows, err = tx.QueryContext(ctx, "SELECT address, data, normalizedonlinebalance FROM accountbase ORDER BY address") + if err != nil { + return err + } + defer rows.Close() + + var insertRes sql.Result + var rowID int64 + var rowsAffected int64 + var processedAccounts uint64 + var totalBaseAccounts uint64 + + arw := NewAccountsSQLReaderWriter(tx) + totalBaseAccounts, err = arw.TotalAccounts(ctx) + if err != nil { + return err + } + for rows.Next() { + var addrbuf []byte + var encodedAcctData []byte + var normBal sql.NullInt64 + err = rows.Scan(&addrbuf, &encodedAcctData, &normBal) + if err != nil { + return err + } + + var accountData basics.AccountData + err = protocol.Decode(encodedAcctData, &accountData) + if err != nil { + return err + } + var newAccountData BaseAccountData + newAccountData.SetAccountData(&accountData) + encodedAcctData = protocol.Encode(&newAccountData) + + if normBal.Valid { + insertRes, err = insertNewAcctBaseNormBal.ExecContext(ctx, addrbuf, encodedAcctData, normBal.Int64) + } else { + insertRes, err = insertNewAcctBase.ExecContext(ctx, addrbuf, encodedAcctData) + } + + if err != nil { + return err + } + rowsAffected, err = insertRes.RowsAffected() + if err != nil { + return err + } + if rowsAffected != 1 { + return fmt.Errorf("number of affected rows is not 1 - %d", rowsAffected) + } + rowID, err = insertRes.LastInsertId() + if err != nil { + return err + } + insertResourceCallback := func(ctx context.Context, rowID int64, cidx basics.CreatableIndex, rd *ResourcesData) error { + var err error + if rd != nil { + encodedData := protocol.Encode(rd) + _, err = insertResources.ExecContext(ctx, rowID, cidx, encodedData) + } + return err + } + err = AccountDataResources(ctx, &accountData, rowID, insertResourceCallback) + if err != nil { + return err + } + processedAccounts++ + if log != nil { + log(processedAccounts, totalBaseAccounts) + } + } + + // if the above loop was abrupt by an error, test it now. + if err = rows.Err(); err != nil { + return err + } + + for _, stmt := range applyNewAcctBase { + _, err = tx.Exec(stmt) + if err != nil { + return err + } + } + return nil +} + +func performTxTailTableMigration(ctx context.Context, tx *sql.Tx, blockDb db.Accessor) (err error) { + if tx == nil { + return nil + } + + arw := NewAccountsSQLReaderWriter(tx) + dbRound, err := arw.AccountsRound() + if err != nil { + return fmt.Errorf("latest block number cannot be retrieved : %w", err) + } + + // load the latest MaxTxnLife rounds in the txtail and store these in the txtail. + // when migrating there is only MaxTxnLife blocks in the block DB + // since the original txTail.commmittedUpTo preserved only (rnd+1)-MaxTxnLife = 1000 blocks back + err = blockDb.Atomic(func(ctx context.Context, blockTx *sql.Tx) error { + latestBlockRound, err := blockdb.BlockLatest(blockTx) + if err != nil { + return fmt.Errorf("latest block number cannot be retrieved : %w", err) + } + latestHdr, err := blockdb.BlockGetHdr(blockTx, dbRound) + if err != nil { + return fmt.Errorf("latest block header %d cannot be retrieved : %w", dbRound, err) + } + + proto := config.Consensus[latestHdr.CurrentProtocol] + maxTxnLife := basics.Round(proto.MaxTxnLife) + deeperBlockHistory := basics.Round(proto.DeeperBlockHeaderHistory) + // firstRound is either maxTxnLife + deeperBlockHistory back from the latest for regular init + // or maxTxnLife + deeperBlockHistory + CatchpointLookback back for catchpoint apply. + // Try to check the earliest available and start from there. + firstRound := (latestBlockRound + 1).SubSaturate(maxTxnLife + deeperBlockHistory + basics.Round(proto.CatchpointLookback)) + // we don't need to have the txtail for round 0. + if firstRound == basics.Round(0) { + firstRound++ + } + if _, err := blockdb.BlockGet(blockTx, firstRound); err != nil { + // looks like not catchpoint but a regular migration, start from maxTxnLife + deeperBlockHistory back + firstRound = (latestBlockRound + 1).SubSaturate(maxTxnLife + deeperBlockHistory) + if firstRound == basics.Round(0) { + firstRound++ + } + } + tailRounds := make([][]byte, 0, maxTxnLife) + for rnd := firstRound; rnd <= dbRound; rnd++ { + blk, err := blockdb.BlockGet(blockTx, rnd) + if err != nil { + return fmt.Errorf("block for round %d ( %d - %d ) cannot be retrieved : %w", rnd, firstRound, dbRound, err) + } + + tail, err := TxTailRoundFromBlock(blk) + if err != nil { + return err + } + + encodedTail, _ := tail.Encode() + tailRounds = append(tailRounds, encodedTail) + } + + return arw.TxtailNewRound(ctx, firstRound, tailRounds, firstRound) + }) + + return err +} + +func performOnlineRoundParamsTailMigration(ctx context.Context, tx *sql.Tx, blockDb db.Accessor, newDatabase bool, initProto protocol.ConsensusVersion) (err error) { + arw := NewAccountsSQLReaderWriter(tx) + totals, err := arw.AccountsTotals(ctx, false) + if err != nil { + return err + } + rnd, err := arw.AccountsRound() + if err != nil { + return err + } + var currentProto protocol.ConsensusVersion + if newDatabase { + currentProto = initProto + } else { + err = blockDb.Atomic(func(ctx context.Context, blockTx *sql.Tx) error { + hdr, err := blockdb.BlockGetHdr(blockTx, rnd) + if err != nil { + return err + } + currentProto = hdr.CurrentProtocol + return nil + }) + if err != nil { + return err + } + } + onlineRoundParams := []ledgercore.OnlineRoundParamsData{ + { + OnlineSupply: totals.Online.Money.Raw, + RewardsLevel: totals.RewardsLevel, + CurrentProtocol: currentProto, + }, + } + return arw.AccountsPutOnlineRoundParams(onlineRoundParams, rnd) +} + +func performOnlineAccountsTableMigration(ctx context.Context, tx *sql.Tx, progress func(processed, total uint64), log logging.Logger) (err error) { + + var insertOnlineAcct *sql.Stmt + insertOnlineAcct, err = tx.PrepareContext(ctx, "INSERT INTO onlineaccounts(address, data, normalizedonlinebalance, updround, votelastvalid) VALUES(?, ?, ?, ?, ?)") + if err != nil { + return err + } + defer insertOnlineAcct.Close() + + var updateAcct *sql.Stmt + updateAcct, err = tx.PrepareContext(ctx, "UPDATE accountbase SET data = ? WHERE addrid = ?") + if err != nil { + return err + } + defer updateAcct.Close() + + var rows *sql.Rows + rows, err = tx.QueryContext(ctx, "SELECT addrid, address, data, normalizedonlinebalance FROM accountbase") + if err != nil { + return err + } + defer rows.Close() + + var insertRes sql.Result + var updateRes sql.Result + var rowsAffected int64 + var processedAccounts uint64 + var totalOnlineBaseAccounts uint64 + + arw := NewAccountsSQLReaderWriter(tx) + totalOnlineBaseAccounts, err = arw.TotalAccounts(ctx) + var total uint64 + err = tx.QueryRowContext(ctx, "SELECT count(1) FROM accountbase").Scan(&total) + if err != nil { + if err != sql.ErrNoRows { + return err + } + total = 0 + err = nil + } + + checkSQLResult := func(e error, res sql.Result) (err error) { + if e != nil { + err = e + return + } + rowsAffected, err = res.RowsAffected() + if err != nil { + return err + } + if rowsAffected != 1 { + return fmt.Errorf("number of affected rows is not 1 - %d", rowsAffected) + } + return nil + } + + type acctState struct { + old BaseAccountData + oldEnc []byte + new BaseAccountData + newEnc []byte + } + acctRehash := make(map[basics.Address]acctState) + var addr basics.Address + + for rows.Next() { + var addrid sql.NullInt64 + var addrbuf []byte + var encodedAcctData []byte + var normBal sql.NullInt64 + err = rows.Scan(&addrid, &addrbuf, &encodedAcctData, &normBal) + if err != nil { + return err + } + if len(addrbuf) != len(addr) { + err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) + return err + } + var ba BaseAccountData + err = protocol.Decode(encodedAcctData, &ba) + if err != nil { + return err + } + + // insert entries into online accounts table + if ba.Status == basics.Online { + if ba.MicroAlgos.Raw > 0 && !normBal.Valid { + copy(addr[:], addrbuf) + return fmt.Errorf("non valid norm balance for online account %s", addr.String()) + } + var baseOnlineAD BaseOnlineAccountData + baseOnlineAD.BaseVotingData = ba.BaseVotingData + baseOnlineAD.MicroAlgos = ba.MicroAlgos + baseOnlineAD.RewardsBase = ba.RewardsBase + encodedOnlineAcctData := protocol.Encode(&baseOnlineAD) + insertRes, err = insertOnlineAcct.ExecContext(ctx, addrbuf, encodedOnlineAcctData, normBal.Int64, ba.UpdateRound, baseOnlineAD.VoteLastValid) + err = checkSQLResult(err, insertRes) + if err != nil { + return err + } + } + + // remove stateproofID field for offline accounts + if ba.Status != basics.Online && !ba.StateProofID.IsEmpty() { + // store old data for account hash update + state := acctState{old: ba, oldEnc: encodedAcctData} + ba.StateProofID = merklesignature.Commitment{} + encodedOnlineAcctData := protocol.Encode(&ba) + copy(addr[:], addrbuf) + state.new = ba + state.newEnc = encodedOnlineAcctData + acctRehash[addr] = state + updateRes, err = updateAcct.ExecContext(ctx, encodedOnlineAcctData, addrid.Int64) + err = checkSQLResult(err, updateRes) + if err != nil { + return err + } + } + + processedAccounts++ + if progress != nil { + progress(processedAccounts, totalOnlineBaseAccounts) + } + } + if err = rows.Err(); err != nil { + return err + } + + // update accounthashes for the modified accounts + if len(acctRehash) > 0 { + var count uint64 + err := tx.QueryRow("SELECT count(1) FROM accounthashes").Scan(&count) + if err != nil { + return err + } + if count == 0 { + // no account hashes, done + return nil + } + + mc, err := MakeMerkleCommitter(tx, false) + if err != nil { + return nil + } + + trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) + if err != nil { + return fmt.Errorf("accountsInitialize was unable to MakeTrie: %v", err) + } + for addr, state := range acctRehash { + deleteHash := AccountHashBuilderV6(addr, &state.old, state.oldEnc) + deleted, err := trie.Delete(deleteHash) + if err != nil { + return fmt.Errorf("performOnlineAccountsTableMigration failed to delete hash '%s' from merkle trie for account %v: %w", hex.EncodeToString(deleteHash), addr, err) + } + if !deleted && log != nil { + log.Warnf("performOnlineAccountsTableMigration failed to delete hash '%s' from merkle trie for account %v", hex.EncodeToString(deleteHash), addr) + } + + addHash := AccountHashBuilderV6(addr, &state.new, state.newEnc) + added, err := trie.Add(addHash) + if err != nil { + return fmt.Errorf("performOnlineAccountsTableMigration attempted to add duplicate hash '%s' to merkle trie for account %v: %w", hex.EncodeToString(addHash), addr, err) + } + if !added && log != nil { + log.Warnf("performOnlineAccountsTableMigration attempted to add duplicate hash '%s' to merkle trie for account %v", hex.EncodeToString(addHash), addr) + } + } + _, err = trie.Commit() + if err != nil { + return err + } + } + + return nil +} + +// removeEmptyAccountData removes empty AccountData msgp-encoded entries from accountbase table +// and optionally returns list of addresses that were eliminated +func removeEmptyAccountData(tx *sql.Tx, queryAddresses bool) (num int64, addresses []basics.Address, err error) { + if queryAddresses { + rows, err := tx.Query("SELECT address FROM accountbase where length(data) = 1 and data = x'80'") // empty AccountData is 0x80 + if err != nil { + return 0, nil, err + } + defer rows.Close() + + for rows.Next() { + var addrbuf []byte + err = rows.Scan(&addrbuf) + if err != nil { + return 0, nil, err + } + var addr basics.Address + if len(addrbuf) != len(addr) { + err = fmt.Errorf("account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) + return 0, nil, err + } + copy(addr[:], addrbuf) + addresses = append(addresses, addr) + } + + // if the above loop was abrupted by an error, test it now. + if err = rows.Err(); err != nil { + return 0, nil, err + } + } + + result, err := tx.Exec("DELETE from accountbase where length(data) = 1 and data = x'80'") + if err != nil { + return 0, nil, err + } + num, err = result.RowsAffected() + if err != nil { + // something wrong on getting rows count but data deleted, ignore the error + num = int64(len(addresses)) + err = nil + } + return num, addresses, err +} + +// 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 warning + // message, which would let us know that it failed anyway. + _, err = db.ResetTransactionWarnDeadline(ctx, tx, time.Now().Add(time.Second)) + if err != nil { + return + } + } + + 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.Equal(preencodedAccountData, reencodedAccountData) { + // 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 +} diff --git a/ledger/store/schema_test.go b/ledger/store/schema_test.go new file mode 100644 index 0000000000..6074a3f5e7 --- /dev/null +++ b/ledger/store/schema_test.go @@ -0,0 +1,293 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "context" + "crypto/rand" + "database/sql" + "fmt" + "testing" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/crypto/merklesignature" + "github.com/algorand/go-algorand/crypto/merkletrie" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" + storetesting "github.com/algorand/go-algorand/ledger/store/testing" + ledgertesting "github.com/algorand/go-algorand/ledger/testing" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/algorand/go-algorand/util/db" + "github.com/stretchr/testify/require" +) + +func TestAccountsReencoding(t *testing.T) { + partitiontest.PartitionTest(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, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) + defer dbs.Close() + + secrets := crypto.GenerateOneTimeSignatureSecrets(15, 500) + pubVrfKey, _ := crypto.VrfKeygenFromSeed([32]byte{0, 1, 2, 3}) + var stateProofID merklesignature.Verifier + crypto.RandBytes(stateProofID.Commitment[:]) + + err := dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + AccountsInitTest(t, tx, make(map[basics.Address]basics.AccountData), protocol.ConsensusCurrentVersion) + + for _, oldAccData := range oldEncodedAccountsData { + addr := ledgertesting.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 := ledgertesting.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, + StateProofID: stateProofID.Commitment, + VoteFirstValid: basics.Round(0x000ffffffffffffff), + VoteLastValid: basics.Round(0x000ffffffffffffff), + VoteKeyDilution: 0x000ffffffffffffff, + AssetParams: map[basics.AssetIndex]basics.AssetParams{ + 0x000ffffffffffffff: { + 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: { + 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) +} + +// TestAccountDBTxTailLoad checks txtailNewRound and LoadTxTail delete and load right data +func TestAccountDBTxTailLoad(t *testing.T) { + partitiontest.PartitionTest(t) + + const inMem = true + dbs, _ := storetesting.DbOpenTest(t, inMem) + storetesting.SetDbLogging(t, dbs) + defer dbs.Close() + + tx, err := dbs.Wdb.Handle.Begin() + require.NoError(t, err) + defer tx.Rollback() + + arw := NewAccountsSQLReaderWriter(tx) + + err = accountsCreateTxTailTable(context.Background(), tx) + require.NoError(t, err) + + // insert 1500 rounds and retain past 1001 + startRound := basics.Round(1) + endRound := basics.Round(1500) + roundData := make([][]byte, 1500) + const retainSize = 1001 + for i := startRound; i <= endRound; i++ { + data := TxTailRound{Hdr: bookkeeping.BlockHeader{TimeStamp: int64(i)}} + roundData[i-1] = protocol.Encode(&data) + } + forgetBefore := (endRound + 1).SubSaturate(retainSize) + err = arw.TxtailNewRound(context.Background(), startRound, roundData, forgetBefore) + require.NoError(t, err) + + data, _, baseRound, err := arw.LoadTxTail(context.Background(), endRound) + require.NoError(t, err) + require.Len(t, data, retainSize) + require.Equal(t, basics.Round(endRound-retainSize+1), baseRound) // 500...1500 + + for i, entry := range data { + require.Equal(t, int64(i+int(baseRound)), entry.Hdr.TimeStamp) + } +} + +func TestRemoveOfflineStateProofID(t *testing.T) { + partitiontest.PartitionTest(t) + + accts := ledgertesting.RandomAccounts(20, true) + expectedAccts := make(map[basics.Address]basics.AccountData) + for addr, acct := range accts { + rand.Read(acct.StateProofID[:]) + accts[addr] = acct + + expectedAcct := acct + if acct.Status != basics.Online { + expectedAcct.StateProofID = merklesignature.Commitment{} + } + expectedAccts[addr] = expectedAcct + + } + + buildDB := func(accounts map[basics.Address]basics.AccountData) (db.Pair, *sql.Tx) { + dbs, _ := storetesting.DbOpenTest(t, true) + storetesting.SetDbLogging(t, dbs) + + tx, err := dbs.Wdb.Handle.Begin() + require.NoError(t, err) + + // this is the same seq as AccountsInitTest makes but it stops + // before the online accounts table creation to generate a trie and commit it + _, err = accountsInit(tx, accounts, config.Consensus[protocol.ConsensusCurrentVersion]) + require.NoError(t, err) + + err = accountsAddNormalizedBalance(tx, config.Consensus[protocol.ConsensusCurrentVersion]) + require.NoError(t, err) + + err = accountsCreateResourceTable(context.Background(), tx) + require.NoError(t, err) + + err = performResourceTableMigration(context.Background(), tx, nil) + require.NoError(t, err) + + return dbs, tx + } + + dbs, tx := buildDB(accts) + defer dbs.Close() + defer tx.Rollback() + + // make second copy of DB to prepare exepected/fixed merkle trie + expectedDBs, expectedTx := buildDB(expectedAccts) + defer expectedDBs.Close() + defer expectedTx.Rollback() + + // create account hashes + computeRootHash := func(tx *sql.Tx, expected bool) (crypto.Digest, error) { + rows, err := tx.Query("SELECT address, data FROM accountbase") + require.NoError(t, err) + defer rows.Close() + + mc, err := MakeMerkleCommitter(tx, false) + require.NoError(t, err) + trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) + require.NoError(t, err) + + var addr basics.Address + for rows.Next() { + var addrbuf []byte + var encodedAcctData []byte + err = rows.Scan(&addrbuf, &encodedAcctData) + require.NoError(t, err) + copy(addr[:], addrbuf) + var ba BaseAccountData + err = protocol.Decode(encodedAcctData, &ba) + require.NoError(t, err) + if expected && ba.Status != basics.Online { + require.Equal(t, merklesignature.Commitment{}, ba.StateProofID) + } + addHash := AccountHashBuilderV6(addr, &ba, encodedAcctData) + added, err := trie.Add(addHash) + require.NoError(t, err) + require.True(t, added) + } + _, err = trie.Evict(true) + require.NoError(t, err) + return trie.RootHash() + } + oldRoot, err := computeRootHash(tx, false) + require.NoError(t, err) + require.NotEmpty(t, oldRoot) + + expectedRoot, err := computeRootHash(expectedTx, true) + require.NoError(t, err) + require.NotEmpty(t, expectedRoot) + + err = accountsCreateOnlineAccountsTable(context.Background(), tx) + require.NoError(t, err) + err = performOnlineAccountsTableMigration(context.Background(), tx, nil, nil) + require.NoError(t, err) + + // get the new hash and ensure it does not match to the old one (data migrated) + mc, err := MakeMerkleCommitter(tx, false) + require.NoError(t, err) + trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) + require.NoError(t, err) + + newRoot, err := trie.RootHash() + require.NoError(t, err) + require.NotEmpty(t, newRoot) + + require.NotEqual(t, oldRoot, newRoot) + require.Equal(t, expectedRoot, newRoot) + + rows, err := tx.Query("SELECT addrid, data FROM accountbase") + require.NoError(t, err) + defer rows.Close() + + for rows.Next() { + var addrid sql.NullInt64 + var encodedAcctData []byte + err = rows.Scan(&addrid, &encodedAcctData) + require.NoError(t, err) + var ba BaseAccountData + err = protocol.Decode(encodedAcctData, &ba) + require.NoError(t, err) + if ba.Status != basics.Online { + require.True(t, ba.StateProofID.IsEmpty()) + } + } +} diff --git a/ledger/store/sql.go b/ledger/store/sql.go new file mode 100644 index 0000000000..ae8166faef --- /dev/null +++ b/ledger/store/sql.go @@ -0,0 +1,716 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "database/sql" + "fmt" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" +) + +// accountsDbQueries is used to cache a prepared SQL statement to look up +// the state of a single account. +type accountsDbQueries struct { + listCreatablesStmt *sql.Stmt + lookupAccountStmt *sql.Stmt + lookupResourcesStmt *sql.Stmt + lookupAllResourcesStmt *sql.Stmt + lookupKvPairStmt *sql.Stmt + lookupKeysByRangeStmt *sql.Stmt + lookupCreatorStmt *sql.Stmt +} + +type onlineAccountsDbQueries struct { + lookupOnlineStmt *sql.Stmt + lookupOnlineHistoryStmt *sql.Stmt + lookupOnlineTotalsStmt *sql.Stmt +} + +type accountsSQLWriter struct { + insertCreatableIdxStmt, deleteCreatableIdxStmt *sql.Stmt + deleteByRowIDStmt, insertStmt, updateStmt *sql.Stmt + deleteResourceStmt, insertResourceStmt, updateResourceStmt *sql.Stmt + deleteKvPairStmt, upsertKvPairStmt *sql.Stmt +} + +type onlineAccountsSQLWriter struct { + insertStmt, updateStmt *sql.Stmt +} + +// AccountsInitDbQueries constructs an AccountsReader backed by sql queries. +func AccountsInitDbQueries(q db.Queryable) (*accountsDbQueries, error) { + var err error + qs := &accountsDbQueries{} + + qs.listCreatablesStmt, err = q.Prepare("SELECT acctrounds.rnd, assetcreators.asset, assetcreators.creator FROM acctrounds LEFT JOIN assetcreators ON assetcreators.asset <= ? AND assetcreators.ctype = ? WHERE acctrounds.id='acctbase' ORDER BY assetcreators.asset desc LIMIT ?") + if err != nil { + return nil, err + } + + qs.lookupAccountStmt, err = q.Prepare("SELECT accountbase.rowid, acctrounds.rnd, accountbase.data FROM acctrounds LEFT JOIN accountbase ON address=? WHERE id='acctbase'") + if err != nil { + return nil, err + } + + qs.lookupResourcesStmt, err = q.Prepare("SELECT accountbase.rowid, acctrounds.rnd, resources.data FROM acctrounds LEFT JOIN accountbase ON accountbase.address = ? LEFT JOIN resources ON accountbase.rowid = resources.addrid AND resources.aidx = ? WHERE id='acctbase'") + if err != nil { + return nil, err + } + + qs.lookupAllResourcesStmt, err = q.Prepare("SELECT accountbase.rowid, acctrounds.rnd, resources.aidx, resources.data FROM acctrounds LEFT JOIN accountbase ON accountbase.address = ? LEFT JOIN resources ON accountbase.rowid = resources.addrid WHERE id='acctbase'") + if err != nil { + return nil, err + } + + qs.lookupKvPairStmt, err = q.Prepare("SELECT acctrounds.rnd, kvstore.value FROM acctrounds LEFT JOIN kvstore ON key = ? WHERE id='acctbase';") + if err != nil { + return nil, err + } + + qs.lookupKeysByRangeStmt, err = q.Prepare("SELECT acctrounds.rnd, kvstore.key FROM acctrounds LEFT JOIN kvstore ON kvstore.key >= ? AND kvstore.key < ? WHERE id='acctbase'") + if err != nil { + return nil, err + } + + qs.lookupCreatorStmt, err = q.Prepare("SELECT acctrounds.rnd, assetcreators.creator FROM acctrounds LEFT JOIN assetcreators ON asset = ? AND ctype = ? WHERE id='acctbase'") + if err != nil { + return nil, err + } + + return qs, nil +} + +// OnlineAccountsInitDbQueries constructs an OnlineAccountsReader backed by sql queries. +func OnlineAccountsInitDbQueries(r db.Queryable) (*onlineAccountsDbQueries, error) { + var err error + qs := &onlineAccountsDbQueries{} + + qs.lookupOnlineStmt, err = r.Prepare("SELECT onlineaccounts.rowid, onlineaccounts.updround, acctrounds.rnd, onlineaccounts.data FROM acctrounds LEFT JOIN onlineaccounts ON address=? AND updround <= ? WHERE id='acctbase' ORDER BY updround DESC LIMIT 1") + if err != nil { + return nil, err + } + + qs.lookupOnlineHistoryStmt, err = r.Prepare("SELECT onlineaccounts.rowid, onlineaccounts.updround, acctrounds.rnd, onlineaccounts.data FROM acctrounds LEFT JOIN onlineaccounts ON address=? WHERE id='acctbase' ORDER BY updround ASC") + if err != nil { + return nil, err + } + + qs.lookupOnlineTotalsStmt, err = r.Prepare("SELECT data FROM onlineroundparamstail WHERE rnd=?") + if err != nil { + return nil, err + } + return qs, nil +} + +// MakeOnlineAccountsSQLWriter constructs an OnlineAccountsWriter backed by sql queries. +func MakeOnlineAccountsSQLWriter(tx *sql.Tx, hasAccounts bool) (w *onlineAccountsSQLWriter, err error) { + w = new(onlineAccountsSQLWriter) + + if hasAccounts { + w.insertStmt, err = tx.Prepare("INSERT INTO onlineaccounts (address, normalizedonlinebalance, data, updround, votelastvalid) VALUES (?, ?, ?, ?, ?)") + if err != nil { + return + } + + w.updateStmt, err = tx.Prepare("UPDATE onlineaccounts SET normalizedonlinebalance = ?, data = ?, updround = ?, votelastvalid =? WHERE rowid = ?") + if err != nil { + return + } + } + + return +} + +// MakeAccountsSQLWriter constructs an AccountsWriter backed by sql queries. +func MakeAccountsSQLWriter(tx *sql.Tx, hasAccounts, hasResources, hasKvPairs, hasCreatables bool) (w *accountsSQLWriter, err error) { + w = new(accountsSQLWriter) + + if hasAccounts { + w.deleteByRowIDStmt, err = tx.Prepare("DELETE FROM accountbase WHERE rowid=?") + if err != nil { + return + } + + w.insertStmt, err = tx.Prepare("INSERT INTO accountbase (address, normalizedonlinebalance, data) VALUES (?, ?, ?)") + if err != nil { + return + } + + w.updateStmt, err = tx.Prepare("UPDATE accountbase SET normalizedonlinebalance = ?, data = ? WHERE rowid = ?") + if err != nil { + return + } + } + + if hasResources { + w.deleteResourceStmt, err = tx.Prepare("DELETE FROM resources WHERE addrid = ? AND aidx = ?") + if err != nil { + return + } + + w.insertResourceStmt, err = tx.Prepare("INSERT INTO resources(addrid, aidx, data) VALUES(?, ?, ?)") + if err != nil { + return + } + + w.updateResourceStmt, err = tx.Prepare("UPDATE resources SET data = ? WHERE addrid = ? AND aidx = ?") + if err != nil { + return + } + } + + if hasKvPairs { + w.upsertKvPairStmt, err = tx.Prepare("INSERT INTO kvstore (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value") + if err != nil { + return + } + + w.deleteKvPairStmt, err = tx.Prepare("DELETE FROM kvstore WHERE key=?") + if err != nil { + return + } + } + + if hasCreatables { + w.insertCreatableIdxStmt, err = tx.Prepare("INSERT INTO assetcreators (asset, creator, ctype) VALUES (?, ?, ?)") + if err != nil { + return + } + + w.deleteCreatableIdxStmt, err = tx.Prepare("DELETE FROM assetcreators WHERE asset=? AND ctype=?") + if err != nil { + return + } + } + return +} + +// ListCreatables returns an array of CreatableLocator which have CreatableIndex smaller or equal to maxIdx and are of the provided CreatableType. +func (qs *accountsDbQueries) ListCreatables(maxIdx basics.CreatableIndex, maxResults uint64, ctype basics.CreatableType) (results []basics.CreatableLocator, dbRound basics.Round, err error) { + err = db.Retry(func() error { + // Query for assets in range + rows, err := qs.listCreatablesStmt.Query(maxIdx, ctype, maxResults) + if err != nil { + return err + } + defer rows.Close() + + // For each row, copy into a new CreatableLocator and append to results + var buf []byte + var cl basics.CreatableLocator + var creatableIndex sql.NullInt64 + for rows.Next() { + err = rows.Scan(&dbRound, &creatableIndex, &buf) + if err != nil { + return err + } + if !creatableIndex.Valid { + // we received an entry without any index. This would happen only on the first entry when there are no creatables of the requested type. + break + } + cl.Index = basics.CreatableIndex(creatableIndex.Int64) + copy(cl.Creator[:], buf) + cl.Type = ctype + results = append(results, cl) + } + return nil + }) + return +} + +// sql.go has the following contradictory comments: + +// Reference types such as []byte are only valid until the next call to Scan +// and should not be retained. Their underlying memory is owned by the driver. +// If retention is necessary, copy their values before the next call to Scan. + +// If a dest argument has type *[]byte, Scan saves in that argument a +// copy of the corresponding data. The copy is owned by the caller and +// can be modified and held indefinitely. The copy can be avoided by +// using an argument of type *RawBytes instead; see the documentation +// for RawBytes for restrictions on its use. + +// After check source code, a []byte slice destination is definitely cloned. + +// LookupKeyValue returns the application boxed value associated with the key. +func (qs *accountsDbQueries) LookupKeyValue(key string) (pv PersistedKVData, err error) { + err = db.Retry(func() error { + var val []byte + // Cast to []byte to avoid interpretation as character string, see note in upsertKvPair + err := qs.lookupKvPairStmt.QueryRow([]byte(key)).Scan(&pv.Round, &val) + if err != nil { + // this should never happen; it indicates that we don't have a current round in the acctrounds table. + if err == sql.ErrNoRows { + // Return the zero value of data + err = fmt.Errorf("unable to query value for key %v : %w", key, err) + } + return err + } + if val != nil { // We got a non-null value, so it exists + pv.Value = val + return nil + } + // we don't have that key, just return pv with the database round (pv.value==nil) + return nil + }) + return +} + +// LookupKeysByPrefix returns a set of application boxed values matching the prefix. +func (qs *accountsDbQueries) LookupKeysByPrefix(prefix string, maxKeyNum uint64, results map[string]bool, resultCount uint64) (round basics.Round, err error) { + start, end := keyPrefixIntervalPreprocessing([]byte(prefix)) + if end == nil { + // Not an expected use case, it's asking for all keys, or all keys + // prefixed by some number of 0xFF bytes. + return 0, fmt.Errorf("lookup by strange prefix %#v", prefix) + } + err = db.Retry(func() error { + var rows *sql.Rows + rows, err = qs.lookupKeysByRangeStmt.Query(start, end) + if err != nil { + return err + } + defer rows.Close() + + var v sql.NullString + + for rows.Next() { + if resultCount == maxKeyNum { + return nil + } + err = rows.Scan(&round, &v) + if err != nil { + return err + } + if v.Valid { + if _, ok := results[v.String]; ok { + continue + } + results[v.String] = true + resultCount++ + } + } + return nil + }) + return +} + +// keyPrefixIntervalPreprocessing is implemented to generate an interval for DB queries that look up keys by prefix. +// Such DB query was designed this way, to trigger the binary search optimization in SQLITE3. +// The DB comparison for blob typed primary key is lexicographic, i.e., byte by byte. +// In this way, we can introduce an interval that a primary key should be >= some prefix, < some prefix increment. +// A corner case to consider is that, the prefix has last byte 0xFF, or the prefix is full of 0xFF. +// - The first case can be solved by carrying, e.g., prefix = 0x1EFF -> interval being >= 0x1EFF and < 0x1F +// - The second case can be solved by disregarding the upper limit, i.e., prefix = 0xFFFF -> interval being >= 0xFFFF +// Another corner case to consider is empty byte, []byte{} or nil. +// - In both cases, the results are interval >= "", i.e., returns []byte{} for prefix, and nil for prefixIncr. +func keyPrefixIntervalPreprocessing(prefix []byte) ([]byte, []byte) { + if prefix == nil { + prefix = []byte{} + } + prefixIncr := make([]byte, len(prefix)) + copy(prefixIncr, prefix) + for i := len(prefix) - 1; i >= 0; i-- { + currentByteIncr := int(prefix[i]) + 1 + if currentByteIncr > 0xFF { + prefixIncr = prefixIncr[:len(prefixIncr)-1] + continue + } + prefixIncr[i] = byte(currentByteIncr) + return prefix, prefixIncr + } + return prefix, nil +} + +// LookupCreator returns the address and round of the creator. +func (qs *accountsDbQueries) LookupCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (addr basics.Address, ok bool, dbRound basics.Round, err error) { + err = db.Retry(func() error { + var buf []byte + err := qs.lookupCreatorStmt.QueryRow(cidx, ctype).Scan(&dbRound, &buf) + + // this shouldn't happen unless we can't figure the round number. + if err == sql.ErrNoRows { + return fmt.Errorf("lookupCreator was unable to retrieve round number") + } + + // Some other database error + if err != nil { + return err + } + + if len(buf) > 0 { + ok = true + copy(addr[:], buf) + } + return nil + }) + return +} + +// LookupResources returns the requested resource. +func (qs *accountsDbQueries) LookupResources(addr basics.Address, aidx basics.CreatableIndex, ctype basics.CreatableType) (data PersistedResourcesData, err error) { + err = db.Retry(func() error { + var buf []byte + var rowid sql.NullInt64 + err := qs.lookupResourcesStmt.QueryRow(addr[:], aidx).Scan(&rowid, &data.Round, &buf) + if err == nil { + data.Aidx = aidx + if len(buf) > 0 && rowid.Valid { + data.Addrid = rowid.Int64 + err = protocol.Decode(buf, &data.Data) + if err != nil { + return err + } + if ctype == basics.AssetCreatable && !data.Data.IsAsset() { + return fmt.Errorf("lookupResources asked for an asset but got %v", data.Data) + } + if ctype == basics.AppCreatable && !data.Data.IsApp() { + return fmt.Errorf("lookupResources asked for an app but got %v", data.Data) + } + return nil + } + data.Data = MakeResourcesData(0) + // we don't have that account, just return the database round. + return nil + } + + // this should never happen; it indicates that we don't have a current round in the acctrounds table. + if err == sql.ErrNoRows { + // Return the zero value of data + return fmt.Errorf("unable to query resource data for address %v aidx %v ctype %v : %w", addr, aidx, ctype, err) + } + return err + }) + return +} + +// LookupAllResources returns all resources associated with the given address. +func (qs *accountsDbQueries) LookupAllResources(addr basics.Address) (data []PersistedResourcesData, rnd basics.Round, err error) { + err = db.Retry(func() error { + // Query for all resources + rows, err := qs.lookupAllResourcesStmt.Query(addr[:]) + if err != nil { + return err + } + defer rows.Close() + + var addrid, aidx sql.NullInt64 + var dbRound basics.Round + data = nil + var buf []byte + for rows.Next() { + err := rows.Scan(&addrid, &dbRound, &aidx, &buf) + if err != nil { + return err + } + if !addrid.Valid || !aidx.Valid { + // we received an entry without any index. This would happen only on the first entry when there are no resources for this address. + // ensure this is the first entry, set the round and return + if len(data) != 0 { + return fmt.Errorf("lookupAllResources: unexpected invalid result on non-first resource record: (%v, %v)", addrid.Valid, aidx.Valid) + } + rnd = dbRound + break + } + var resData ResourcesData + err = protocol.Decode(buf, &resData) + if err != nil { + return err + } + data = append(data, PersistedResourcesData{ + Addrid: addrid.Int64, + Aidx: basics.CreatableIndex(aidx.Int64), + Data: resData, + Round: dbRound, + }) + rnd = dbRound + } + return nil + }) + return +} + +// LookupAccount looks up for a the account data given it's address. It returns the persistedAccountData, which includes the current database round and the matching +// account data, if such was found. If no matching account data could be found for the given address, an empty account data would +// be retrieved. +func (qs *accountsDbQueries) LookupAccount(addr basics.Address) (data PersistedAccountData, err error) { + err = db.Retry(func() error { + var buf []byte + var rowid sql.NullInt64 + err := qs.lookupAccountStmt.QueryRow(addr[:]).Scan(&rowid, &data.Round, &buf) + if err == nil { + data.Addr = addr + if len(buf) > 0 && rowid.Valid { + data.Rowid = rowid.Int64 + err = protocol.Decode(buf, &data.AccountData) + return err + } + // we don't have that account, just return the database round. + return nil + } + + // this should never happen; it indicates that we don't have a current round in the acctrounds table. + if err == sql.ErrNoRows { + // Return the zero value of data + return fmt.Errorf("unable to query account data for address %v : %w", addr, err) + } + + return err + }) + return +} + +// LookupOnline returns the online account data for the given address. +func (qs *onlineAccountsDbQueries) LookupOnline(addr basics.Address, rnd basics.Round) (data PersistedOnlineAccountData, err error) { + err = db.Retry(func() error { + var buf []byte + var rowid sql.NullInt64 + var updround sql.NullInt64 + err := qs.lookupOnlineStmt.QueryRow(addr[:], rnd).Scan(&rowid, &updround, &data.Round, &buf) + if err == nil { + data.Addr = addr + if len(buf) > 0 && rowid.Valid && updround.Valid { + data.Rowid = rowid.Int64 + data.UpdRound = basics.Round(updround.Int64) + err = protocol.Decode(buf, &data.AccountData) + return err + } + // we don't have that account, just return the database round. + return nil + } + + // this should never happen; it indicates that we don't have a current round in the acctrounds table. + if err == sql.ErrNoRows { + // Return the zero value of data + return fmt.Errorf("unable to query online account data for address %v : %w", addr, err) + } + + return err + }) + return +} + +func (qs *onlineAccountsDbQueries) LookupOnlineTotalsHistory(round basics.Round) (basics.MicroAlgos, error) { + data := ledgercore.OnlineRoundParamsData{} + err := db.Retry(func() error { + row := qs.lookupOnlineTotalsStmt.QueryRow(round) + var buf []byte + err := row.Scan(&buf) + if err != nil { + return err + } + err = protocol.Decode(buf, &data) + if err != nil { + return err + } + return nil + }) + return basics.MicroAlgos{Raw: data.OnlineSupply}, err +} + +func (qs *onlineAccountsDbQueries) LookupOnlineHistory(addr basics.Address) (result []PersistedOnlineAccountData, rnd basics.Round, err error) { + err = db.Retry(func() error { + rows, err := qs.lookupOnlineHistoryStmt.Query(addr[:]) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var buf []byte + data := PersistedOnlineAccountData{} + err := rows.Scan(&data.Rowid, &data.UpdRound, &rnd, &buf) + if err != nil { + return err + } + err = protocol.Decode(buf, &data.AccountData) + if err != nil { + return err + } + data.Addr = addr + result = append(result, data) + } + return err + }) + return +} + +func (qs *accountsDbQueries) Close() { + preparedQueries := []**sql.Stmt{ + &qs.listCreatablesStmt, + &qs.lookupAccountStmt, + &qs.lookupResourcesStmt, + &qs.lookupAllResourcesStmt, + &qs.lookupKvPairStmt, + &qs.lookupKeysByRangeStmt, + &qs.lookupCreatorStmt, + } + for _, preparedQuery := range preparedQueries { + if (*preparedQuery) != nil { + (*preparedQuery).Close() + *preparedQuery = nil + } + } +} + +func (qs *onlineAccountsDbQueries) Close() { + preparedQueries := []**sql.Stmt{ + &qs.lookupOnlineStmt, + &qs.lookupOnlineHistoryStmt, + } + for _, preparedQuery := range preparedQueries { + if (*preparedQuery) != nil { + (*preparedQuery).Close() + *preparedQuery = nil + } + } +} + +func (w *accountsSQLWriter) Close() { + // Formatted to match the type definition above + preparedStmts := []**sql.Stmt{ + &w.insertCreatableIdxStmt, &w.deleteCreatableIdxStmt, + &w.deleteByRowIDStmt, &w.insertStmt, &w.updateStmt, + &w.deleteResourceStmt, &w.insertResourceStmt, &w.updateResourceStmt, + &w.deleteKvPairStmt, &w.upsertKvPairStmt, + } + + for _, stmt := range preparedStmts { + if (*stmt) != nil { + (*stmt).Close() + *stmt = nil + } + } + +} + +func (w *onlineAccountsSQLWriter) Close() { + if w.insertStmt != nil { + w.insertStmt.Close() + w.insertStmt = nil + } +} + +func (w accountsSQLWriter) InsertAccount(addr basics.Address, normBalance uint64, data BaseAccountData) (rowid int64, err error) { + result, err := w.insertStmt.Exec(addr[:], normBalance, protocol.Encode(&data)) + if err != nil { + return + } + rowid, err = result.LastInsertId() + return +} + +func (w accountsSQLWriter) DeleteAccount(rowid int64) (rowsAffected int64, err error) { + result, err := w.deleteByRowIDStmt.Exec(rowid) + if err != nil { + return + } + rowsAffected, err = result.RowsAffected() + return +} + +func (w accountsSQLWriter) UpdateAccount(rowid int64, normBalance uint64, data BaseAccountData) (rowsAffected int64, err error) { + result, err := w.updateStmt.Exec(normBalance, protocol.Encode(&data), rowid) + if err != nil { + return + } + rowsAffected, err = result.RowsAffected() + return +} + +func (w accountsSQLWriter) InsertResource(addrid int64, aidx basics.CreatableIndex, data ResourcesData) (rowid int64, err error) { + result, err := w.insertResourceStmt.Exec(addrid, aidx, protocol.Encode(&data)) + if err != nil { + return + } + rowid, err = result.LastInsertId() + return +} + +func (w accountsSQLWriter) DeleteResource(addrid int64, aidx basics.CreatableIndex) (rowsAffected int64, err error) { + result, err := w.deleteResourceStmt.Exec(addrid, aidx) + if err != nil { + return + } + rowsAffected, err = result.RowsAffected() + return +} + +func (w accountsSQLWriter) UpdateResource(addrid int64, aidx basics.CreatableIndex, data ResourcesData) (rowsAffected int64, err error) { + result, err := w.updateResourceStmt.Exec(protocol.Encode(&data), addrid, aidx) + if err != nil { + return + } + rowsAffected, err = result.RowsAffected() + return +} + +func (w accountsSQLWriter) UpsertKvPair(key string, value []byte) error { + // NOTE! If we are passing in `string`, then for `BoxKey` case, + // we might contain 0-byte in boxKey, coming from uint64 appID. + // The consequence would be DB key write in be cut off after such 0-byte. + // Casting `string` to `[]byte` avoids such trouble, and test: + // - `TestBoxNamesByAppIDs` in `acctupdates_test` + // relies on such modification. + result, err := w.upsertKvPairStmt.Exec([]byte(key), value) + if err != nil { + return err + } + _, err = result.LastInsertId() + return err +} + +func (w accountsSQLWriter) DeleteKvPair(key string) error { + // Cast to []byte to avoid interpretation as character string, see note in upsertKvPair + result, err := w.deleteKvPairStmt.Exec([]byte(key)) + if err != nil { + return err + } + _, err = result.RowsAffected() + return err +} + +func (w accountsSQLWriter) InsertCreatable(cidx basics.CreatableIndex, ctype basics.CreatableType, creator []byte) (rowid int64, err error) { + result, err := w.insertCreatableIdxStmt.Exec(cidx, creator, ctype) + if err != nil { + return + } + rowid, err = result.LastInsertId() + return +} + +func (w accountsSQLWriter) DeleteCreatable(cidx basics.CreatableIndex, ctype basics.CreatableType) (rowsAffected int64, err error) { + result, err := w.deleteCreatableIdxStmt.Exec(cidx, ctype) + if err != nil { + return + } + rowsAffected, err = result.RowsAffected() + return +} + +func (w onlineAccountsSQLWriter) InsertOnlineAccount(addr basics.Address, normBalance uint64, data BaseOnlineAccountData, updRound uint64, voteLastValid uint64) (rowid int64, err error) { + result, err := w.insertStmt.Exec(addr[:], normBalance, protocol.Encode(&data), updRound, voteLastValid) + if err != nil { + return + } + rowid, err = result.LastInsertId() + return +} diff --git a/ledger/store/sql_test.go b/ledger/store/sql_test.go new file mode 100644 index 0000000000..2eb96bfe65 --- /dev/null +++ b/ledger/store/sql_test.go @@ -0,0 +1,55 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "testing" + + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" +) + +func TestKeyPrefixIntervalPreprocessing(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testCases := []struct { + input []byte + outputPrefix []byte + outputPrefixIncr []byte + }{ + {input: []byte{0xAB, 0xCD}, outputPrefix: []byte{0xAB, 0xCD}, outputPrefixIncr: []byte{0xAB, 0xCE}}, + {input: []byte{0xFF}, outputPrefix: []byte{0xFF}, outputPrefixIncr: nil}, + {input: []byte{0xFE, 0xFF}, outputPrefix: []byte{0xFE, 0xFF}, outputPrefixIncr: []byte{0xFF}}, + {input: []byte{0xFF, 0xFF}, outputPrefix: []byte{0xFF, 0xFF}, outputPrefixIncr: nil}, + {input: []byte{0xAB, 0xCD}, outputPrefix: []byte{0xAB, 0xCD}, outputPrefixIncr: []byte{0xAB, 0xCE}}, + {input: []byte{0x1E, 0xFF, 0xFF}, outputPrefix: []byte{0x1E, 0xFF, 0xFF}, outputPrefixIncr: []byte{0x1F}}, + {input: []byte{0xFF, 0xFE, 0xFF, 0xFF}, outputPrefix: []byte{0xFF, 0xFE, 0xFF, 0xFF}, outputPrefixIncr: []byte{0xFF, 0xFF}}, + {input: []byte{0x00, 0xFF}, outputPrefix: []byte{0x00, 0xFF}, outputPrefixIncr: []byte{0x01}}, + {input: []byte(string("bx:123")), outputPrefix: []byte(string("bx:123")), outputPrefixIncr: []byte(string("bx:124"))}, + {input: []byte{}, outputPrefix: []byte{}, outputPrefixIncr: nil}, + {input: nil, outputPrefix: []byte{}, outputPrefixIncr: nil}, + {input: []byte{0x1E, 0xFF, 0xFF}, outputPrefix: []byte{0x1E, 0xFF, 0xFF}, outputPrefixIncr: []byte{0x1F}}, + {input: []byte{0xFF, 0xFE, 0xFF, 0xFF}, outputPrefix: []byte{0xFF, 0xFE, 0xFF, 0xFF}, outputPrefixIncr: []byte{0xFF, 0xFF}}, + {input: []byte{0x00, 0xFF}, outputPrefix: []byte{0x00, 0xFF}, outputPrefixIncr: []byte{0x01}}, + } + for _, tc := range testCases { + actualOutputPrefix, actualOutputPrefixIncr := keyPrefixIntervalPreprocessing(tc.input) + require.Equal(t, tc.outputPrefix, actualOutputPrefix) + require.Equal(t, tc.outputPrefixIncr, actualOutputPrefixIncr) + } +} diff --git a/ledger/store/testing.go b/ledger/store/testing.go new file mode 100644 index 0000000000..c637076c1d --- /dev/null +++ b/ledger/store/testing.go @@ -0,0 +1,99 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "context" + "database/sql" + "testing" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" + "github.com/stretchr/testify/require" +) + +// AccountsInitLightTest initializes an empty database for testing without the extra methods being called. +func AccountsInitLightTest(tb testing.TB, tx *sql.Tx, initAccounts map[basics.Address]basics.AccountData, proto config.ConsensusParams) (newDatabase bool, err error) { + newDB, err := accountsInit(tx, initAccounts, proto) + require.NoError(tb, err) + return newDB, err +} + +// AccountsInitTest initializes an empty database for testing. +func AccountsInitTest(tb testing.TB, tx *sql.Tx, initAccounts map[basics.Address]basics.AccountData, proto protocol.ConsensusVersion) (newDatabase bool) { + newDB, err := accountsInit(tx, initAccounts, config.Consensus[proto]) + require.NoError(tb, err) + + err = accountsAddNormalizedBalance(tx, config.Consensus[proto]) + require.NoError(tb, err) + + err = accountsCreateResourceTable(context.Background(), tx) + require.NoError(tb, err) + + err = performResourceTableMigration(context.Background(), tx, nil) + require.NoError(tb, err) + + err = accountsCreateOnlineAccountsTable(context.Background(), tx) + require.NoError(tb, err) + + err = accountsCreateTxTailTable(context.Background(), tx) + require.NoError(tb, err) + + err = performOnlineAccountsTableMigration(context.Background(), tx, nil, nil) + require.NoError(tb, err) + + // since this is a test that starts from genesis, there is no tail that needs to be migrated. + // we'll pass a nil here in order to ensure we still call this method, although it would + // be a noop. + err = performTxTailTableMigration(context.Background(), nil, db.Accessor{}) + require.NoError(tb, err) + + err = accountsCreateOnlineRoundParamsTable(context.Background(), tx) + require.NoError(tb, err) + + err = performOnlineRoundParamsTailMigration(context.Background(), tx, db.Accessor{}, true, proto) + require.NoError(tb, err) + + err = accountsCreateBoxTable(context.Background(), tx) + require.NoError(tb, err) + + return newDB +} + +// AccountsUpdateSchemaTest adds some empty tables for tests to work with a "v6" store. +func AccountsUpdateSchemaTest(ctx context.Context, tx *sql.Tx) (err error) { + if err := accountsCreateOnlineAccountsTable(ctx, tx); err != nil { + return err + } + if err := accountsCreateTxTailTable(ctx, tx); err != nil { + return err + } + if err := accountsCreateOnlineRoundParamsTable(ctx, tx); err != nil { + return err + } + if err := accountsCreateCatchpointFirstStageInfoTable(ctx, tx); err != nil { + return err + } + // this line creates kvstore table, even if it is not required in accountDBVersion 6 -> 7 + // or in later version where we need kvstore table, some tests will fail + if err := accountsCreateBoxTable(ctx, tx); err != nil { + return err + } + return nil +} diff --git a/ledger/store/testing/helpers.go b/ledger/store/testing/helpers.go new file mode 100644 index 0000000000..0fb5ec8246 --- /dev/null +++ b/ledger/store/testing/helpers.go @@ -0,0 +1,43 @@ +// Copyright (C) 2019-2022 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 testing + +import ( + "fmt" + "strings" + "testing" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/util/db" + "github.com/stretchr/testify/require" +) + +// DbOpenTest opens a db file for testing purposes. +func DbOpenTest(t testing.TB, inMemory bool) (db.Pair, string) { + fn := fmt.Sprintf("%s.%d", strings.ReplaceAll(t.Name(), "/", "."), crypto.RandUint64()) + dbs, err := db.OpenPair(fn, inMemory) + require.NoErrorf(t, err, "Filename : %s\nInMemory: %v", fn, inMemory) + return dbs, fn +} + +// SetDbLogging sets a testing logger on a database. +func SetDbLogging(t testing.TB, dbs db.Pair) { + dblogger := logging.TestingLog(t) + dbs.Rdb.SetLogger(dblogger) + dbs.Wdb.SetLogger(dblogger) +} diff --git a/ledger/store/trackerdbV2.go b/ledger/store/trackerdbV2.go new file mode 100644 index 0000000000..feb7604ca7 --- /dev/null +++ b/ledger/store/trackerdbV2.go @@ -0,0 +1,577 @@ +// Copyright (C) 2019-2022 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 store + +import ( + "context" + "database/sql" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/crypto/merkletrie" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" +) + +// TrackerDBParams contains parameters for initializing trackerDB +type TrackerDBParams struct { + InitAccounts map[basics.Address]basics.AccountData + InitProto protocol.ConsensusVersion + GenesisHash crypto.Digest + FromCatchpoint bool + CatchpointEnabled bool + DbPathPrefix string + BlockDb db.Pair +} + +type trackerDBSchemaInitializer struct { + TrackerDBParams + + // schemaVersion contains current db version + schemaVersion int32 + // vacuumOnStartup controls whether the accounts database would get vacuumed on startup. + vacuumOnStartup bool + // newDatabase indicates if the db is newly created + newDatabase bool + + log logging.Logger +} + +// TrackerDBInitParams params used during db init +type TrackerDBInitParams struct { + SchemaVersion int32 + VacuumOnStartup bool +} + +// RunMigrations initializes the accounts DB if needed and return current 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 RunMigrations(ctx context.Context, tx *sql.Tx, params TrackerDBParams, log logging.Logger, targetVersion int32) (mgr TrackerDBInitParams, err error) { + // check current database version. + dbVersion, err := db.GetUserVersion(ctx, tx) + if err != nil { + return TrackerDBInitParams{}, fmt.Errorf("trackerDBInitialize unable to read database schema version : %v", err) + } + + tu := trackerDBSchemaInitializer{ + TrackerDBParams: params, + schemaVersion: dbVersion, + log: log, + } + + // if database version is greater than supported by current binary, write a warning. This would keep the existing + // fallback behavior where we could use an older binary iff the schema happen to be backward compatible. + if tu.version() > targetVersion { + tu.log.Warnf("trackerDBInitialize database schema version is %d, but migration target version is %d", tu.version(), targetVersion) + } + + if tu.version() < targetVersion { + tu.log.Infof("trackerDBInitialize upgrading database schema from version %d to version %d", tu.version(), targetVersion) + // newDatabase is determined during the tables creations. If we're filling the database with accounts, + // then we set this variable to true, allowing some of the upgrades to be skipped. + for tu.version() < targetVersion { + tu.log.Infof("trackerDBInitialize performing upgrade from version %d", tu.version()) + // perform the initialization/upgrade + switch tu.version() { + case 0: + err = tu.upgradeDatabaseSchema0(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 0 : %v", err) + return + } + case 1: + err = tu.upgradeDatabaseSchema1(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 1 : %v", err) + return + } + case 2: + err = tu.upgradeDatabaseSchema2(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 2 : %v", err) + return + } + case 3: + err = tu.upgradeDatabaseSchema3(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 3 : %v", err) + return + } + case 4: + err = tu.upgradeDatabaseSchema4(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 4 : %v", err) + return + } + case 5: + err = tu.upgradeDatabaseSchema5(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 5 : %v", err) + return + } + case 6: + err = tu.upgradeDatabaseSchema6(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 6 : %v", err) + return + } + case 7: + err = tu.upgradeDatabaseSchema7(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 7 : %v", err) + return + } + case 8: + err = tu.upgradeDatabaseSchema8(ctx, tx) + if err != nil { + tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 8 : %v", err) + return + } + default: + return TrackerDBInitParams{}, fmt.Errorf("trackerDBInitialize unable to upgrade database from schema version %d", tu.schemaVersion) + } + } + tu.log.Infof("trackerDBInitialize database schema upgrade complete") + } + + return TrackerDBInitParams{tu.schemaVersion, tu.vacuumOnStartup}, nil +} + +func (tu *trackerDBSchemaInitializer) setVersion(ctx context.Context, tx *sql.Tx, version int32) (err error) { + oldVersion := tu.schemaVersion + tu.schemaVersion = version + _, err = db.SetUserVersion(ctx, tx, tu.schemaVersion) + if err != nil { + return fmt.Errorf("trackerDBInitialize unable to update database schema version from %d to %d: %v", oldVersion, version, err) + } + return nil +} + +func (tu trackerDBSchemaInitializer) version() int32 { + return tu.schemaVersion +} + +// 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 (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema0(ctx context.Context, tx *sql.Tx) (err error) { + tu.log.Infof("upgradeDatabaseSchema0 initializing schema") + tu.newDatabase, err = accountsInit(tx, tu.InitAccounts, config.Consensus[tu.InitProto]) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema0 unable to initialize schema : %v", err) + } + return tu.setVersion(ctx, tx, 1) +} + +// upgradeDatabaseSchema1 upgrades the database schema from version 1 to version 2 +// +// The schema updated to version 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 procedure 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 trackerDBInitialize would (if needed) rebuild the merkle 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 (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema1(ctx context.Context, tx *sql.Tx) (err error) { + var modifiedAccounts uint + if tu.newDatabase { + goto schemaUpdateComplete + } + + // update accounts encoding. + tu.log.Infof("upgradeDatabaseSchema1 verifying accounts data encoding") + modifiedAccounts, err = reencodeAccounts(ctx, tx) + if err != nil { + return err + } + + if modifiedAccounts > 0 { + crw := NewCatchpointSQLReaderWriter(tx) + arw := NewAccountsSQLReaderWriter(tx) + + tu.log.Infof("upgradeDatabaseSchema1 reencoded %d accounts", modifiedAccounts) + + tu.log.Infof("upgradeDatabaseSchema1 resetting account hashes") + // reset the merkle trie + err = arw.ResetAccountHashes(ctx) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema1 unable to reset account hashes : %v", err) + } + + tu.log.Infof("upgradeDatabaseSchema1 preparing queries") + tu.log.Infof("upgradeDatabaseSchema1 resetting prior catchpoints") + // delete the last catchpoint label if we have any. + err = crw.WriteCatchpointStateString(ctx, CatchpointStateLastCatchpoint, "") + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema1 unable to clear prior catchpoint : %v", err) + } + + tu.log.Infof("upgradeDatabaseSchema1 deleting stored catchpoints") + // delete catchpoints. + err = crw.DeleteStoredCatchpoints(ctx, tu.TrackerDBParams.DbPathPrefix) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema1 unable to delete stored catchpoints : %v", err) + } + } else { + tu.log.Infof("upgradeDatabaseSchema1 found that no accounts needed to be reencoded") + } + +schemaUpdateComplete: + return tu.setVersion(ctx, tx, 2) +} + +// upgradeDatabaseSchema2 upgrades the database schema from version 2 to version 3 +// +// This upgrade only enables the database vacuuming which will take place once the upgrade process is complete. +// If the user has already specified the OptimizeAccountsDatabaseOnStartup flag in the configuration file, this +// step becomes a no-op. +func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema2(ctx context.Context, tx *sql.Tx) (err error) { + if !tu.newDatabase { + tu.vacuumOnStartup = true + } + + // update version + return tu.setVersion(ctx, tx, 3) +} + +// upgradeDatabaseSchema3 upgrades the database schema from version 3 to version 4, +// adding the normalizedonlinebalance column to the accountbase table. +func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema3(ctx context.Context, tx *sql.Tx) (err error) { + err = accountsAddNormalizedBalance(tx, config.Consensus[tu.InitProto]) + if err != nil { + return err + } + + // update version + return tu.setVersion(ctx, tx, 4) +} + +// upgradeDatabaseSchema4 does not change the schema but migrates data: +// remove empty AccountData entries from accountbase table +func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema4(ctx context.Context, tx *sql.Tx) (err error) { + var numDeleted int64 + var addresses []basics.Address + + if tu.newDatabase { + goto done + } + + numDeleted, addresses, err = removeEmptyAccountData(tx, tu.CatchpointEnabled) + if err != nil { + return err + } + + if tu.CatchpointEnabled && len(addresses) > 0 { + mc, err := MakeMerkleCommitter(tx, false) + if err != nil { + // at this point record deleted and DB is pruned for account data + // if hash deletion fails just log it and do not abort startup + tu.log.Errorf("upgradeDatabaseSchema4: failed to create merkle committer: %v", err) + goto done + } + trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) + if err != nil { + tu.log.Errorf("upgradeDatabaseSchema4: failed to create merkle trie: %v", err) + goto done + } + + var totalHashesDeleted int + for _, addr := range addresses { + hash := AccountHashBuilder(addr, basics.AccountData{}, []byte{0x80}) + deleted, err := trie.Delete(hash) + if err != nil { + tu.log.Errorf("upgradeDatabaseSchema4: failed to delete hash '%s' from merkle trie for account %v: %v", hex.EncodeToString(hash), addr, err) + } else { + if !deleted { + tu.log.Warnf("upgradeDatabaseSchema4: failed to delete hash '%s' from merkle trie for account %v", hex.EncodeToString(hash), addr) + } else { + totalHashesDeleted++ + } + } + } + + if _, err = trie.Commit(); err != nil { + tu.log.Errorf("upgradeDatabaseSchema4: failed to commit changes to merkle trie: %v", err) + } + + tu.log.Infof("upgradeDatabaseSchema4: deleted %d hashes", totalHashesDeleted) + } + +done: + tu.log.Infof("upgradeDatabaseSchema4: deleted %d rows", numDeleted) + + return tu.setVersion(ctx, tx, 5) +} + +// upgradeDatabaseSchema5 upgrades the database schema from version 5 to version 6, +// adding the resources table and clearing empty catchpoint directories. +func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema5(ctx context.Context, tx *sql.Tx) (err error) { + arw := NewAccountsSQLReaderWriter(tx) + + err = accountsCreateResourceTable(ctx, tx) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema5 unable to create resources table : %v", err) + } + + err = removeEmptyDirsOnSchemaUpgrade(tu.TrackerDBParams.DbPathPrefix) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema5 unable to clear empty catchpoint directories : %v", err) + } + + var lastProgressInfoMsg time.Time + const progressLoggingInterval = 5 * time.Second + migrationProcessLog := func(processed, total uint64) { + if time.Since(lastProgressInfoMsg) < progressLoggingInterval { + return + } + lastProgressInfoMsg = time.Now() + tu.log.Infof("upgradeDatabaseSchema5 upgraded %d out of %d accounts [ %3.1f%% ]", processed, total, float64(processed)*100.0/float64(total)) + } + + err = performResourceTableMigration(ctx, tx, migrationProcessLog) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema5 unable to complete data migration : %v", err) + } + + // reset the merkle trie + err = arw.ResetAccountHashes(ctx) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema5 unable to reset account hashes : %v", err) + } + + // update version + return tu.setVersion(ctx, tx, 6) +} + +func (tu *trackerDBSchemaInitializer) deleteUnfinishedCatchpoint(ctx context.Context, tx *sql.Tx) error { + cts := NewCatchpointSQLReaderWriter(tx) + // Delete an unfinished catchpoint if there is one. + round, err := cts.ReadCatchpointStateUint64(ctx, catchpointStateWritingCatchpoint) + if err != nil { + return err + } + if round == 0 { + return nil + } + + relCatchpointFilePath := filepath.Join( + CatchpointDirName, + MakeCatchpointFilePath(basics.Round(round))) + err = RemoveSingleCatchpointFileFromDisk(tu.DbPathPrefix, relCatchpointFilePath) + if err != nil { + return err + } + + return cts.WriteCatchpointStateUint64(ctx, catchpointStateWritingCatchpoint, 0) +} + +// MakeCatchpointFilePath builds the path of a catchpoint file. +func MakeCatchpointFilePath(round basics.Round) string { + irnd := int64(round) / 256 + outStr := "" + for irnd > 0 { + outStr = filepath.Join(outStr, fmt.Sprintf("%02x", irnd%256)) + irnd = irnd / 256 + } + outStr = filepath.Join(outStr, strconv.FormatInt(int64(round), 10)+".catchpoint") + return outStr +} + +// upgradeDatabaseSchema6 upgrades the database schema from version 6 to version 7, +// adding a new onlineaccounts table +// TODO: onlineaccounts: upgrade as needed after switching to the final table version +func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema6(ctx context.Context, tx *sql.Tx) (err error) { + err = accountsCreateOnlineAccountsTable(ctx, tx) + if err != nil { + return err + } + + err = accountsCreateTxTailTable(ctx, tx) + if err != nil { + return err + } + + err = accountsCreateOnlineRoundParamsTable(ctx, tx) + if err != nil { + return err + } + + var lastProgressInfoMsg time.Time + const progressLoggingInterval = 5 * time.Second + + migrationProcessLog := func(processed, total uint64) { + if time.Since(lastProgressInfoMsg) < progressLoggingInterval { + return + } + lastProgressInfoMsg = time.Now() + tu.log.Infof("upgradeDatabaseSchema6 upgraded %d out of %d accounts [ %3.1f%% ]", processed, total, float64(processed)*100.0/float64(total)) + } + err = performOnlineAccountsTableMigration(ctx, tx, migrationProcessLog, tu.log) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema6 unable to complete online account data migration : %w", err) + } + + if !tu.newDatabase { + err = performTxTailTableMigration(ctx, tx, tu.BlockDb.Rdb) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema6 unable to complete transaction tail data migration : %w", err) + } + } + + err = performOnlineRoundParamsTailMigration(ctx, tx, tu.BlockDb.Rdb, tu.newDatabase, tu.InitProto) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema6 unable to complete online round params data migration : %w", err) + } + + err = tu.deleteUnfinishedCatchpoint(ctx, tx) + if err != nil { + return err + } + err = accountsCreateCatchpointFirstStageInfoTable(ctx, tx) + if err != nil { + return err + } + err = accountsCreateUnfinishedCatchpointsTable(ctx, tx) + if err != nil { + return err + } + + // update version + return tu.setVersion(ctx, tx, 7) +} + +// upgradeDatabaseSchema7 upgrades the database schema from version 7 to version 8. +// adding the kvstore table for box feature support. +func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema7(ctx context.Context, tx *sql.Tx) (err error) { + err = accountsCreateBoxTable(ctx, tx) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema7 unable to create kvstore through createTables : %v", err) + } + return tu.setVersion(ctx, tx, 8) +} + +// upgradeDatabaseSchema8 upgrades the database schema from version 8 to version 9, +// forcing a rebuild of the accounthashes table on betanet nodes. Otherwise it has no effect. +func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema8(ctx context.Context, tx *sql.Tx) (err error) { + arw := NewAccountsSQLReaderWriter(tx) + betanetGenesisHash, _ := crypto.DigestFromString("TBMBVTC7W24RJNNUZCF7LWZD2NMESGZEQSMPG5XQD7JY4O7JKVWQ") + if tu.GenesisHash == betanetGenesisHash && !tu.FromCatchpoint { + // reset hash round to 0, forcing catchpointTracker.initializeHashes to rebuild accounthashes + err = arw.UpdateAccountsHashRound(ctx, 0) + if err != nil { + return fmt.Errorf("upgradeDatabaseSchema8 unable to reset acctrounds table 'hashbase' round : %v", err) + } + } + return tu.setVersion(ctx, tx, 9) +} + +// Review: this is an odd method to have here + +// isDirEmpty returns if a given directory is empty or not. +func isDirEmpty(path string) (bool, error) { + dir, err := os.Open(path) + if err != nil { + return false, err + } + defer dir.Close() + _, err = dir.Readdirnames(1) + if err != io.EOF { + return false, err + } + return true, nil +} + +// GetEmptyDirs returns a slice of paths for empty directories which are located in PathToScan arg +func GetEmptyDirs(PathToScan string) ([]string, error) { + var emptyDir []string + err := filepath.Walk(PathToScan, func(path string, f os.FileInfo, errIn error) error { + if errIn != nil { + return errIn + } + if !f.IsDir() { + return nil + } + isEmpty, err := isDirEmpty(path) + if err != nil { + if os.IsNotExist(err) { + return filepath.SkipDir + } + return err + } + if isEmpty { + emptyDir = append(emptyDir, path) + } + return nil + }) + return emptyDir, err +} + +func removeEmptyDirsOnSchemaUpgrade(dbDirectory string) (err error) { + catchpointRootDir := filepath.Join(dbDirectory, CatchpointDirName) + if _, err := os.Stat(catchpointRootDir); os.IsNotExist(err) { + return nil + } + for { + emptyDirs, err := GetEmptyDirs(catchpointRootDir) + if err != nil { + return err + } + // There are no empty dirs + if len(emptyDirs) == 0 { + break + } + // only left with the root dir + if len(emptyDirs) == 1 && emptyDirs[0] == catchpointRootDir { + break + } + for _, emptyDirPath := range emptyDirs { + os.Remove(emptyDirPath) + } + } + return nil +} diff --git a/ledger/tracker.go b/ledger/tracker.go index 1945018efb..1612856444 100644 --- a/ledger/tracker.go +++ b/ledger/tracker.go @@ -31,6 +31,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/ledger/internal" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/protocol" @@ -246,12 +247,12 @@ type deferredCommitContext struct { compactKvDeltas map[string]modifiedKvValue compactCreatableDeltas map[basics.CreatableIndex]ledgercore.ModifiedCreatable - updatedPersistedAccounts []persistedAccountData - updatedPersistedResources map[basics.Address][]persistedResourcesData - updatedPersistedKVs map[string]persistedKVData + updatedPersistedAccounts []store.PersistedAccountData + updatedPersistedResources map[basics.Address][]store.PersistedResourcesData + updatedPersistedKVs map[string]store.PersistedKVData compactOnlineAccountDeltas compactOnlineAccountDeltas - updatedPersistedOnlineAccounts []persistedOnlineAccountData + updatedPersistedOnlineAccounts []store.PersistedOnlineAccountData updatingBalancesDuration time.Duration @@ -279,7 +280,8 @@ func (tr *trackerRegistry) initialize(l ledgerForTracker, trackers []ledgerTrack tr.log = l.trackerLog() err = tr.dbs.Rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - tr.dbRound, err = accountsRound(tx) + arw := store.NewAccountsSQLReaderWriter(tx) + tr.dbRound, err = arw.AccountsRound() return err }) @@ -509,6 +511,7 @@ func (tr *trackerRegistry) commitRound(dcc *deferredCommitContext) error { start := time.Now() ledgerCommitroundCount.Inc(nil) err := tr.dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + arw := store.NewAccountsSQLReaderWriter(tx) for _, lt := range tr.trackers { err0 := lt.commitRound(ctx, tx, dcc) if err0 != nil { @@ -516,7 +519,7 @@ func (tr *trackerRegistry) commitRound(dcc *deferredCommitContext) error { } } - return updateAccountsRound(tx, dbRound+basics.Round(offset)) + return arw.UpdateAccountsRound(dbRound + basics.Round(offset)) }) ledgerCommitroundMicros.AddMicrosecondsSince(start, nil) diff --git a/ledger/tracker_test.go b/ledger/tracker_test.go index 813c07461a..0f319ff555 100644 --- a/ledger/tracker_test.go +++ b/ledger/tracker_test.go @@ -90,7 +90,7 @@ func TestTrackerScheduleCommit(t *testing.T) { // prepare deltas and versions au.accountsMu.Lock() - au.deltas = make([]ledgercore.AccountDeltas, int(blockqRound)) + au.deltas = make([]ledgercore.StateDelta, int(blockqRound)) au.deltasAccum = make([]int, int(blockqRound)) au.versions = make([]protocol.ConsensusVersion, int(blockqRound)) ao.deltas = make([]ledgercore.AccountDeltas, int(blockqRound)) diff --git a/ledger/trackerdb.go b/ledger/trackerdb.go index 3b1f6ca017..8a6dda4bfe 100644 --- a/ledger/trackerdb.go +++ b/ledger/trackerdb.go @@ -19,55 +19,15 @@ package ledger import ( "context" "database/sql" - "encoding/hex" "fmt" - "io" - "os" - "path/filepath" - "time" - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/crypto" - - "github.com/algorand/go-algorand/crypto/merkletrie" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/logging" - "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-algorand/util/db" + "github.com/algorand/go-algorand/ledger/store" ) -type trackerDBParams struct { - initAccounts map[basics.Address]basics.AccountData - initProto protocol.ConsensusVersion - genesisHash crypto.Digest - fromCatchpoint bool - catchpointEnabled bool - dbPathPrefix string - blockDb db.Pair -} - -type trackerDBSchemaInitializer struct { - trackerDBParams - - // schemaVersion contains current db version - schemaVersion int32 - // vacuumOnStartup controls whether the accounts database would get vacuumed on startup. - vacuumOnStartup bool - // newDatabase indicates if the db is newly created - newDatabase bool - - log logging.Logger -} - -type trackerDBInitParams struct { - schemaVersion int32 - vacuumOnStartup bool -} - // trackerDBInitialize initializes the accounts DB if needed and return current 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 trackerDBInitialize(l ledgerForTracker, catchpointEnabled bool, dbPathPrefix string) (mgr trackerDBInitParams, err error) { +func trackerDBInitialize(l ledgerForTracker, catchpointEnabled bool, dbPathPrefix string) (mgr store.TrackerDBInitParams, err error) { dbs := l.trackerDB() bdbs := l.blockDB() log := l.trackerLog() @@ -80,32 +40,34 @@ func trackerDBInitialize(l ledgerForTracker, catchpointEnabled bool, dbPathPrefi } err = dbs.Wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { - tp := trackerDBParams{ - initAccounts: l.GenesisAccounts(), - initProto: l.GenesisProtoVersion(), - genesisHash: l.GenesisHash(), - fromCatchpoint: false, - catchpointEnabled: catchpointEnabled, - dbPathPrefix: dbPathPrefix, - blockDb: bdbs, + arw := store.NewAccountsSQLReaderWriter(tx) + + tp := store.TrackerDBParams{ + InitAccounts: l.GenesisAccounts(), + InitProto: l.GenesisProtoVersion(), + GenesisHash: l.GenesisHash(), + FromCatchpoint: false, + CatchpointEnabled: catchpointEnabled, + DbPathPrefix: dbPathPrefix, + BlockDb: bdbs, } var err0 error - mgr, err0 = runMigrations(ctx, tx, tp, log, accountDBVersion) + mgr, err0 = store.RunMigrations(ctx, tx, tp, log, store.AccountDBVersion) if err0 != nil { return err0 } - lastBalancesRound, err := accountsRound(tx) + lastBalancesRound, err := arw.AccountsRound() if err != nil { return err } // Check for blocks DB and tracker DB un-sync if lastBalancesRound > lastestBlockRound { log.Warnf("trackerDBInitialize: resetting accounts DB (on round %v, but blocks DB's latest is %v)", lastBalancesRound, lastestBlockRound) - err0 = accountsReset(ctx, tx) + err0 = arw.AccountsReset(ctx) if err0 != nil { return err0 } - mgr, err0 = runMigrations(ctx, tx, tp, log, accountDBVersion) + mgr, err0 = store.RunMigrations(ctx, tx, tp, log, store.AccountDBVersion) if err0 != nil { return err0 } @@ -115,495 +77,3 @@ func trackerDBInitialize(l ledgerForTracker, catchpointEnabled bool, dbPathPrefi return } - -// runMigrations initializes the accounts DB if needed and return current 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 runMigrations(ctx context.Context, tx *sql.Tx, params trackerDBParams, log logging.Logger, targetVersion int32) (mgr trackerDBInitParams, err error) { - // check current database version. - dbVersion, err := db.GetUserVersion(ctx, tx) - if err != nil { - return trackerDBInitParams{}, fmt.Errorf("trackerDBInitialize unable to read database schema version : %v", err) - } - - tu := trackerDBSchemaInitializer{ - trackerDBParams: params, - schemaVersion: dbVersion, - log: log, - } - - // if database version is greater than supported by current binary, write a warning. This would keep the existing - // fallback behavior where we could use an older binary iff the schema happen to be backward compatible. - if tu.version() > targetVersion { - tu.log.Warnf("trackerDBInitialize database schema version is %d, but migration target version is %d", tu.version(), targetVersion) - } - - if tu.version() < targetVersion { - tu.log.Infof("trackerDBInitialize upgrading database schema from version %d to version %d", tu.version(), targetVersion) - // newDatabase is determined during the tables creations. If we're filling the database with accounts, - // then we set this variable to true, allowing some of the upgrades to be skipped. - for tu.version() < targetVersion { - tu.log.Infof("trackerDBInitialize performing upgrade from version %d", tu.version()) - // perform the initialization/upgrade - switch tu.version() { - case 0: - err = tu.upgradeDatabaseSchema0(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 0 : %v", err) - return - } - case 1: - err = tu.upgradeDatabaseSchema1(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 1 : %v", err) - return - } - case 2: - err = tu.upgradeDatabaseSchema2(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 2 : %v", err) - return - } - case 3: - err = tu.upgradeDatabaseSchema3(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 3 : %v", err) - return - } - case 4: - err = tu.upgradeDatabaseSchema4(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 4 : %v", err) - return - } - case 5: - err = tu.upgradeDatabaseSchema5(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 5 : %v", err) - return - } - case 6: - err = tu.upgradeDatabaseSchema6(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 6 : %v", err) - return - } - case 7: - err = tu.upgradeDatabaseSchema7(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 7 : %v", err) - return - } - case 8: - err = tu.upgradeDatabaseSchema8(ctx, tx) - if err != nil { - tu.log.Warnf("trackerDBInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 8 : %v", err) - return - } - default: - return trackerDBInitParams{}, fmt.Errorf("trackerDBInitialize unable to upgrade database from schema version %d", tu.schemaVersion) - } - } - tu.log.Infof("trackerDBInitialize database schema upgrade complete") - } - - return trackerDBInitParams{tu.schemaVersion, tu.vacuumOnStartup}, nil -} - -func (tu *trackerDBSchemaInitializer) setVersion(ctx context.Context, tx *sql.Tx, version int32) (err error) { - oldVersion := tu.schemaVersion - tu.schemaVersion = version - _, err = db.SetUserVersion(ctx, tx, tu.schemaVersion) - if err != nil { - return fmt.Errorf("trackerDBInitialize unable to update database schema version from %d to %d: %v", oldVersion, version, err) - } - return nil -} - -func (tu trackerDBSchemaInitializer) version() int32 { - return tu.schemaVersion -} - -// 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 (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema0(ctx context.Context, tx *sql.Tx) (err error) { - tu.log.Infof("upgradeDatabaseSchema0 initializing schema") - tu.newDatabase, err = accountsInit(tx, tu.initAccounts, config.Consensus[tu.initProto]) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema0 unable to initialize schema : %v", err) - } - return tu.setVersion(ctx, tx, 1) -} - -// upgradeDatabaseSchema1 upgrades the database schema from version 1 to version 2 -// -// The schema updated to version 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 procedure 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 trackerDBInitialize would (if needed) rebuild the merkle 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 (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema1(ctx context.Context, tx *sql.Tx) (err error) { - var modifiedAccounts uint - if tu.newDatabase { - goto schemaUpdateComplete - } - - // update accounts encoding. - tu.log.Infof("upgradeDatabaseSchema1 verifying accounts data encoding") - modifiedAccounts, err = reencodeAccounts(ctx, tx) - if err != nil { - return err - } - - if modifiedAccounts > 0 { - tu.log.Infof("upgradeDatabaseSchema1 reencoded %d accounts", modifiedAccounts) - - tu.log.Infof("upgradeDatabaseSchema1 resetting account hashes") - // reset the merkle trie - err = resetAccountHashes(ctx, tx) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema1 unable to reset account hashes : %v", err) - } - - tu.log.Infof("upgradeDatabaseSchema1 preparing queries") - tu.log.Infof("upgradeDatabaseSchema1 resetting prior catchpoints") - // delete the last catchpoint label if we have any. - err = writeCatchpointStateString(ctx, tx, catchpointStateLastCatchpoint, "") - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema1 unable to clear prior catchpoint : %v", err) - } - - tu.log.Infof("upgradeDatabaseSchema1 deleting stored catchpoints") - // delete catchpoints. - err = deleteStoredCatchpoints(ctx, tx, tu.trackerDBParams.dbPathPrefix) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema1 unable to delete stored catchpoints : %v", err) - } - } else { - tu.log.Infof("upgradeDatabaseSchema1 found that no accounts needed to be reencoded") - } - -schemaUpdateComplete: - return tu.setVersion(ctx, tx, 2) -} - -// upgradeDatabaseSchema2 upgrades the database schema from version 2 to version 3 -// -// This upgrade only enables the database vacuuming which will take place once the upgrade process is complete. -// If the user has already specified the OptimizeAccountsDatabaseOnStartup flag in the configuration file, this -// step becomes a no-op. -// -func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema2(ctx context.Context, tx *sql.Tx) (err error) { - if !tu.newDatabase { - tu.vacuumOnStartup = true - } - - // update version - return tu.setVersion(ctx, tx, 3) -} - -// upgradeDatabaseSchema3 upgrades the database schema from version 3 to version 4, -// adding the normalizedonlinebalance column to the accountbase table. -func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema3(ctx context.Context, tx *sql.Tx) (err error) { - err = accountsAddNormalizedBalance(tx, config.Consensus[tu.initProto]) - if err != nil { - return err - } - - // update version - return tu.setVersion(ctx, tx, 4) -} - -// upgradeDatabaseSchema4 does not change the schema but migrates data: -// remove empty AccountData entries from accountbase table -func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema4(ctx context.Context, tx *sql.Tx) (err error) { - var numDeleted int64 - var addresses []basics.Address - - if tu.newDatabase { - goto done - } - - numDeleted, addresses, err = removeEmptyAccountData(tx, tu.catchpointEnabled) - if err != nil { - return err - } - - if tu.catchpointEnabled && len(addresses) > 0 { - mc, err := MakeMerkleCommitter(tx, false) - if err != nil { - // at this point record deleted and DB is pruned for account data - // if hash deletion fails just log it and do not abort startup - tu.log.Errorf("upgradeDatabaseSchema4: failed to create merkle committer: %v", err) - goto done - } - trie, err := merkletrie.MakeTrie(mc, TrieMemoryConfig) - if err != nil { - tu.log.Errorf("upgradeDatabaseSchema4: failed to create merkle trie: %v", err) - goto done - } - - var totalHashesDeleted int - for _, addr := range addresses { - hash := accountHashBuilder(addr, basics.AccountData{}, []byte{0x80}) - deleted, err := trie.Delete(hash) - if err != nil { - tu.log.Errorf("upgradeDatabaseSchema4: failed to delete hash '%s' from merkle trie for account %v: %v", hex.EncodeToString(hash), addr, err) - } else { - if !deleted { - tu.log.Warnf("upgradeDatabaseSchema4: failed to delete hash '%s' from merkle trie for account %v", hex.EncodeToString(hash), addr) - } else { - totalHashesDeleted++ - } - } - } - - if _, err = trie.Commit(); err != nil { - tu.log.Errorf("upgradeDatabaseSchema4: failed to commit changes to merkle trie: %v", err) - } - - tu.log.Infof("upgradeDatabaseSchema4: deleted %d hashes", totalHashesDeleted) - } - -done: - tu.log.Infof("upgradeDatabaseSchema4: deleted %d rows", numDeleted) - - return tu.setVersion(ctx, tx, 5) -} - -// upgradeDatabaseSchema5 upgrades the database schema from version 5 to version 6, -// adding the resources table and clearing empty catchpoint directories. -func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema5(ctx context.Context, tx *sql.Tx) (err error) { - err = accountsCreateResourceTable(ctx, tx) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema5 unable to create resources table : %v", err) - } - - err = removeEmptyDirsOnSchemaUpgrade(tu.trackerDBParams.dbPathPrefix) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema5 unable to clear empty catchpoint directories : %v", err) - } - - var lastProgressInfoMsg time.Time - const progressLoggingInterval = 5 * time.Second - migrationProcessLog := func(processed, total uint64) { - if time.Since(lastProgressInfoMsg) < progressLoggingInterval { - return - } - lastProgressInfoMsg = time.Now() - tu.log.Infof("upgradeDatabaseSchema5 upgraded %d out of %d accounts [ %3.1f%% ]", processed, total, float64(processed)*100.0/float64(total)) - } - - err = performResourceTableMigration(ctx, tx, migrationProcessLog) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema5 unable to complete data migration : %v", err) - } - - // reset the merkle trie - err = resetAccountHashes(ctx, tx) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema5 unable to reset account hashes : %v", err) - } - - // update version - return tu.setVersion(ctx, tx, 6) -} - -func (tu *trackerDBSchemaInitializer) deleteUnfinishedCatchpoint(ctx context.Context, tx *sql.Tx) error { - // Delete an unfinished catchpoint if there is one. - round, err := readCatchpointStateUint64(ctx, tx, catchpointStateWritingCatchpoint) - if err != nil { - return err - } - if round == 0 { - return nil - } - - relCatchpointFilePath := filepath.Join( - CatchpointDirName, - makeCatchpointFilePath(basics.Round(round))) - err = removeSingleCatchpointFileFromDisk(tu.dbPathPrefix, relCatchpointFilePath) - if err != nil { - return err - } - - return writeCatchpointStateUint64(ctx, tx, catchpointStateWritingCatchpoint, 0) -} - -// upgradeDatabaseSchema6 upgrades the database schema from version 6 to version 7, -// adding a new onlineaccounts table -// TODO: onlineaccounts: upgrade as needed after switching to the final table version -func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema6(ctx context.Context, tx *sql.Tx) (err error) { - err = accountsCreateOnlineAccountsTable(ctx, tx) - if err != nil { - return err - } - - err = accountsCreateTxTailTable(ctx, tx) - if err != nil { - return err - } - - err = accountsCreateOnlineRoundParamsTable(ctx, tx) - if err != nil { - return err - } - - var lastProgressInfoMsg time.Time - const progressLoggingInterval = 5 * time.Second - - migrationProcessLog := func(processed, total uint64) { - if time.Since(lastProgressInfoMsg) < progressLoggingInterval { - return - } - lastProgressInfoMsg = time.Now() - tu.log.Infof("upgradeDatabaseSchema6 upgraded %d out of %d accounts [ %3.1f%% ]", processed, total, float64(processed)*100.0/float64(total)) - } - err = performOnlineAccountsTableMigration(ctx, tx, migrationProcessLog, tu.log) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema6 unable to complete online account data migration : %w", err) - } - - if !tu.newDatabase { - err = performTxTailTableMigration(ctx, tx, tu.blockDb.Rdb) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema6 unable to complete transaction tail data migration : %w", err) - } - } - - err = performOnlineRoundParamsTailMigration(ctx, tx, tu.blockDb.Rdb, tu.newDatabase, tu.initProto) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema6 unable to complete online round params data migration : %w", err) - } - - err = tu.deleteUnfinishedCatchpoint(ctx, tx) - if err != nil { - return err - } - err = accountsCreateCatchpointFirstStageInfoTable(ctx, tx) - if err != nil { - return err - } - err = accountsCreateUnfinishedCatchpointsTable(ctx, tx) - if err != nil { - return err - } - - // update version - return tu.setVersion(ctx, tx, 7) -} - -// upgradeDatabaseSchema7 upgrades the database schema from version 7 to version 8. -// adding the kvstore table for box feature support. -func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema7(ctx context.Context, tx *sql.Tx) (err error) { - err = accountsCreateBoxTable(ctx, tx) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema7 unable to create kvstore through createTables : %v", err) - } - return tu.setVersion(ctx, tx, 8) -} - -// upgradeDatabaseSchema8 upgrades the database schema from version 8 to version 9, -// forcing a rebuild of the accounthashes table on betanet nodes. Otherwise it has no effect. -func (tu *trackerDBSchemaInitializer) upgradeDatabaseSchema8(ctx context.Context, tx *sql.Tx) (err error) { - betanetGenesisHash, _ := crypto.DigestFromString("TBMBVTC7W24RJNNUZCF7LWZD2NMESGZEQSMPG5XQD7JY4O7JKVWQ") - if tu.genesisHash == betanetGenesisHash && !tu.fromCatchpoint { - // reset hash round to 0, forcing catchpointTracker.initializeHashes to rebuild accounthashes - err = updateAccountsHashRound(ctx, tx, 0) - if err != nil { - return fmt.Errorf("upgradeDatabaseSchema8 unable to reset acctrounds table 'hashbase' round : %v", err) - } - } - return tu.setVersion(ctx, tx, 9) -} - -// isDirEmpty returns if a given directory is empty or not. -func isDirEmpty(path string) (bool, error) { - dir, err := os.Open(path) - if err != nil { - return false, err - } - defer dir.Close() - _, err = dir.Readdirnames(1) - if err != io.EOF { - return false, err - } - return true, nil -} - -// getEmptyDirs returns a slice of paths for empty directories which are located in PathToScan arg -func getEmptyDirs(PathToScan string) ([]string, error) { - var emptyDir []string - err := filepath.Walk(PathToScan, func(path string, f os.FileInfo, errIn error) error { - if errIn != nil { - return errIn - } - if !f.IsDir() { - return nil - } - isEmpty, err := isDirEmpty(path) - if err != nil { - if os.IsNotExist(err) { - return filepath.SkipDir - } - return err - } - if isEmpty { - emptyDir = append(emptyDir, path) - } - return nil - }) - return emptyDir, err -} - -func removeEmptyDirsOnSchemaUpgrade(dbDirectory string) (err error) { - catchpointRootDir := filepath.Join(dbDirectory, CatchpointDirName) - if _, err := os.Stat(catchpointRootDir); os.IsNotExist(err) { - return nil - } - for { - emptyDirs, err := getEmptyDirs(catchpointRootDir) - if err != nil { - return err - } - // There are no empty dirs - if len(emptyDirs) == 0 { - break - } - // only left with the root dir - if len(emptyDirs) == 1 && emptyDirs[0] == catchpointRootDir { - break - } - for _, emptyDirPath := range emptyDirs { - os.Remove(emptyDirPath) - } - } - return nil -} diff --git a/ledger/txtail.go b/ledger/txtail.go index 4fa2ab0a4b..b262b3ce3e 100644 --- a/ledger/txtail.go +++ b/ledger/txtail.go @@ -29,6 +29,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" "github.com/algorand/go-algorand/logging" ) @@ -93,12 +94,13 @@ func (t *txTail) loadFromDisk(l ledgerForTracker, dbRound basics.Round) error { rdb := l.trackerDB().Rdb t.log = l.trackerLog() - var roundData []*txTailRound + var roundData []*store.TxTailRound var roundTailHashes []crypto.Digest var baseRound basics.Round if dbRound > 0 { err := rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { - roundData, roundTailHashes, baseRound, err = loadTxTail(ctx, tx, dbRound) + arw := store.NewAccountsSQLReaderWriter(tx) + roundData, roundTailHashes, baseRound, err = arw.LoadTxTail(ctx, dbRound) return }) if err != nil { @@ -192,7 +194,7 @@ func (t *txTail) newBlock(blk bookkeeping.Block, delta ledgercore.StateDelta) { return } - var tail txTailRound + var tail store.TxTailRound tail.TxnIDs = make([]transactions.Txid, len(delta.Txids)) tail.LastValid = make([]basics.Round, len(delta.Txids)) tail.Hdr = blk.BlockHeader @@ -202,14 +204,14 @@ func (t *txTail) newBlock(blk bookkeeping.Block, delta ledgercore.StateDelta) { tail.TxnIDs[txnInc.Intra] = txid tail.LastValid[txnInc.Intra] = txnInc.LastValid if blk.Payset[txnInc.Intra].Txn.Lease != [32]byte{} { - tail.Leases = append(tail.Leases, txTailRoundLease{ + tail.Leases = append(tail.Leases, store.TxTailRoundLease{ Sender: blk.Payset[txnInc.Intra].Txn.Sender, Lease: blk.Payset[txnInc.Intra].Txn.Lease, TxnIdx: txnInc.Intra, }) } } - encodedTail, tailHash := tail.encode() + encodedTail, tailHash := tail.Encode() t.tailMu.Lock() defer t.tailMu.Unlock() @@ -269,11 +271,13 @@ func (t *txTail) prepareCommit(dcc *deferredCommitContext) (err error) { } func (t *txTail) commitRound(ctx context.Context, tx *sql.Tx, dcc *deferredCommitContext) error { + arw := store.NewAccountsSQLReaderWriter(tx) + // determine the round to remove data // the formula is similar to the committedUpTo: rnd + 1 - retain size forgetBeforeRound := (dcc.newBase + 1).SubSaturate(basics.Round(dcc.txTailRetainSize)) baseRound := dcc.oldBase + 1 - if err := txtailNewRound(ctx, tx, baseRound, dcc.txTailDeltas, forgetBeforeRound); err != nil { + if err := arw.TxtailNewRound(ctx, baseRound, dcc.txTailDeltas, forgetBeforeRound); err != nil { return fmt.Errorf("txTail: unable to persist new round %d : %w", baseRound, err) } return nil @@ -339,13 +343,13 @@ func (t *txTail) checkDup(proto config.ConsensusParams, current basics.Round, fi for rnd := firstChecked; rnd <= lastChecked; rnd++ { expires, ok := t.recent[rnd].txleases[txl] if ok && current <= expires { - return ledgercore.MakeLeaseInLedgerError(txid, txl) + return ledgercore.MakeLeaseInLedgerError(txid, txl, false) } } } if _, confirmed := t.lastValid[lastValid][txid]; confirmed { - return &ledgercore.TransactionInLedgerError{Txid: txid} + return &ledgercore.TransactionInLedgerError{Txid: txid, InBlockEvaluator: false} } return nil } diff --git a/ledger/txtail_test.go b/ledger/txtail_test.go index d32ee7568f..1f0d06a293 100644 --- a/ledger/txtail_test.go +++ b/ledger/txtail_test.go @@ -30,6 +30,8 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/ledger/store" + storetesting "github.com/algorand/go-algorand/ledger/store/testing" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" @@ -147,31 +149,31 @@ func (t *txTailTestLedger) Block(r basics.Round) (bookkeeping.Block, error) { func (t *txTailTestLedger) initialize(ts *testing.T, protoVersion protocol.ConsensusVersion) error { // create a corresponding blockdb. inMemory := true - t.blockDBs, _ = dbOpenTest(ts, inMemory) - t.trackerDBs, _ = dbOpenTest(ts, inMemory) + t.blockDBs, _ = storetesting.DbOpenTest(ts, inMemory) + t.trackerDBs, _ = storetesting.DbOpenTest(ts, inMemory) t.protoVersion = protoVersion tx, err := t.trackerDBs.Wdb.Handle.Begin() require.NoError(ts, err) + arw := store.NewAccountsSQLReaderWriter(tx) + accts := ledgertesting.RandomAccounts(20, true) proto := config.Consensus[protoVersion] - newDB := accountsInitTest(ts, tx, accts, protoVersion) + newDB := store.AccountsInitTest(ts, tx, accts, protoVersion) require.True(ts, newDB) - _, err = accountsInit(tx, accts, proto) - require.NoError(ts, err) roundData := make([][]byte, 0, proto.MaxTxnLife) startRound := t.Latest() - basics.Round(proto.MaxTxnLife) + 1 for i := startRound; i <= t.Latest(); i++ { blk, err := t.Block(i) require.NoError(ts, err) - tail, err := txTailRoundFromBlock(blk) + tail, err := store.TxTailRoundFromBlock(blk) require.NoError(ts, err) - encoded, _ := tail.encode() + encoded, _ := tail.Encode() roundData = append(roundData, encoded) } - err = txtailNewRound(context.Background(), tx, startRound, roundData, 0) + err = arw.TxtailNewRound(context.Background(), startRound, roundData, 0) require.NoError(ts, err) tx.Commit() return nil @@ -216,7 +218,7 @@ func TestTxTailLoadFromDisk(t *testing.T) { txn.Txn.FirstValid, txn.Txn.LastValid, txn.Txn.ID(), txl) if r >= ledger.Latest()-testTxTailValidityRange { - require.Equal(t, ledgercore.MakeLeaseInLedgerError(txn.Txn.ID(), txl), dupResult) + require.Equal(t, ledgercore.MakeLeaseInLedgerError(txn.Txn.ID(), txl, false), dupResult) } else { require.Equal(t, &errTxTailMissingRound{round: txn.Txn.LastValid}, dupResult) } diff --git a/libgoal/libgoal.go b/libgoal/libgoal.go index 962db20e7d..c764890794 100644 --- a/libgoal/libgoal.go +++ b/libgoal/libgoal.go @@ -1021,6 +1021,16 @@ func (c *Client) VerifyParticipationKey(timeout time.Duration, participationID s } } +// RemoveParticipationKey removes a participation key by its id +func (c *Client) RemoveParticipationKey(participationID string) error { + algod, err := c.ensureAlgodClient() + if err != nil { + return nil + } + + return algod.RemoveParticipationKeyByID(participationID) +} + // AddParticipationKey takes a participation key file and sends it to the node. // The key will be loaded into the system when the function returns successfully. func (c *Client) AddParticipationKey(keyfile string) (resp model.PostParticipationResponse, err error) { diff --git a/node/node.go b/node/node.go index c1617f2ba2..e87aad9338 100644 --- a/node/node.go +++ b/node/node.go @@ -230,7 +230,16 @@ func MakeFull(log logging.Logger, rootDir string, cfg config.Local, phonebookAdd blockListeners = append(blockListeners, &accountListener) } node.ledger.RegisterBlockListeners(blockListeners) - node.txHandler = data.MakeTxHandler(node.transactionPool, node.ledger, node.net, node.genesisID, node.genesisHash, node.lowPriorityCryptoVerificationPool) + txHandlerOpts := data.TxHandlerOpts{ + TxPool: node.transactionPool, + ExecutionPool: node.lowPriorityCryptoVerificationPool, + Ledger: node.ledger, + Net: node.net, + GenesisID: node.genesisID, + GenesisHash: node.genesisHash, + Config: cfg, + } + node.txHandler = data.MakeTxHandler(txHandlerOpts) // Indexer setup if cfg.IsIndexerActive && cfg.Archival { @@ -1384,3 +1393,21 @@ func (node *AlgorandFullNode) IsParticipating() bool { round := node.ledger.Latest() + 1 return node.accountManager.HasLiveKeys(round, round+10) } + +// SetSyncRound sets the minimum sync round on the catchup service +func (node *AlgorandFullNode) SetSyncRound(rnd uint64) error { + // Calculate the first round for which we want to disable catchup from the network. + // This is based on the size of the cache used in the ledger. + disableSyncRound := rnd + node.Config().MaxAcctLookback + return node.catchupService.SetDisableSyncRound(disableSyncRound) +} + +// GetSyncRound retrieves the sync round, removes cache offset used during SetSyncRound +func (node *AlgorandFullNode) GetSyncRound() uint64 { + return node.catchupService.GetDisableSyncRound() - node.Config().MaxAcctLookback +} + +// UnsetSyncRound removes the sync round constraint on the catchup service +func (node *AlgorandFullNode) UnsetSyncRound() { + node.catchupService.UnsetDisableSyncRound() +} diff --git a/node/node_test.go b/node/node_test.go index dcf2bb6a20..647e77c94a 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -29,7 +29,6 @@ import ( "github.com/stretchr/testify/require" "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" @@ -95,7 +94,7 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP rootDirectory := t.TempDir() rootDirs = append(rootDirs, rootDirectory) - defaultConfig.NetAddress = "127.0.0.1:0" + defaultConfig.NetAddress = neighbors[i] defaultConfig.SaveToDisk(rootDirectory) // Save empty phonebook - we'll add peers after they've been assigned listening ports @@ -141,6 +140,10 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP short := root.Address() genesis[short] = data } + genesis[poolAddr] = basics.AccountData{ + Status: basics.Online, + MicroAlgos: basics.MicroAlgos{Raw: uint64(100000)}, + } bootstrap := bookkeeping.MakeGenesisBalances(genesis, sinkAddr, poolAddr) @@ -158,16 +161,18 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP cfg, err := config.LoadConfigFromDisk(rootDirectory) require.NoError(t, err) cfg.Archival = true - _, err = data.LoadLedger(logging.Base().With("name", nodeID), ledgerFilenamePrefix, inMem, g.Proto, bootstrap, "", crypto.Digest{}, nil, cfg) + _, err = data.LoadLedger(logging.Base().With("name", nodeID), ledgerFilenamePrefix, inMem, g.Proto, bootstrap, g.ID(), g.Hash(), nil, cfg) require.NoError(t, err) } for i := range nodes { + var nodeNeighbors []string + nodeNeighbors = append(nodeNeighbors, neighbors[:i]...) + nodeNeighbors = append(nodeNeighbors, neighbors[i+1:]...) rootDirectory := rootDirs[i] cfg, err := config.LoadConfigFromDisk(rootDirectory) require.NoError(t, err) - - node, err := MakeFull(logging.Base().With("source", t.Name()+strconv.Itoa(i)), rootDirectory, cfg, []string{}, g) + node, err := MakeFull(logging.Base().With("source", t.Name()+strconv.Itoa(i)), rootDirectory, cfg, nodeNeighbors, g) nodes[i] = node require.NoError(t, err) } @@ -178,7 +183,7 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP func TestSyncingFullNode(t *testing.T) { partitiontest.PartitionTest(t) - t.Skip("This is failing randomly again - PLEASE FIX!") + t.Skip("Flaky in nightly test environment") backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) defer backlogPool.Shutdown() @@ -236,7 +241,9 @@ func TestSyncingFullNode(t *testing.T) { func TestInitialSync(t *testing.T) { partitiontest.PartitionTest(t) - t.Skip("flaky TestInitialSync ") + if testing.Short() { + t.Skip("Test takes ~25 seconds.") + } backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) defer backlogPool.Shutdown() @@ -270,7 +277,7 @@ func TestInitialSync(t *testing.T) { func TestSimpleUpgrade(t *testing.T) { partitiontest.PartitionTest(t) - t.Skip("Randomly failing: node_test.go:~330 : no block notification for account. Re-enable after agreement bug-fix pass") + t.Skip("Flaky in nightly test environment.") backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) defer backlogPool.Shutdown() @@ -286,6 +293,7 @@ func TestSimpleUpgrade(t *testing.T) { configurableConsensus := make(config.ConsensusProtocols) testParams0 := config.Consensus[protocol.ConsensusCurrentVersion] + testParams0.MinUpgradeWaitRounds = 0 testParams0.SupportGenesisHash = false testParams0.UpgradeVoteRounds = 2 testParams0.UpgradeThreshold = 1 @@ -299,6 +307,7 @@ func TestSimpleUpgrade(t *testing.T) { configurableConsensus[consensusTest0] = testParams0 testParams1 := config.Consensus[protocol.ConsensusCurrentVersion] + testParams1.MinUpgradeWaitRounds = 0 testParams1.SupportGenesisHash = false testParams1.UpgradeVoteRounds = 10 testParams1.UpgradeThreshold = 8 @@ -389,13 +398,7 @@ func startAndConnectNodes(nodes []*AlgorandFullNode, delayStartFirstNode bool) { } func connectPeers(nodes []*AlgorandFullNode) { - neighbors := make([]string, 0) - for _, node := range nodes { - neighbors = append(neighbors, node.config.NetAddress) - } - for _, node := range nodes { - // node.ExtendPeerList(neighbors...) node.net.RequestConnectOutgoing(false, nil) } } @@ -410,11 +413,10 @@ func delayStartNode(node *AlgorandFullNode, peers []*AlgorandFullNode, delay tim }() wg.Wait() - // node0Addr := node.config.NetAddress for _, peer := range peers { - // peer.ExtendPeerList(node0Addr) peer.net.RequestConnectOutgoing(false, nil) } + node.net.RequestConnectOutgoing(false, nil) } func TestStatusReport_TimeSinceLastRound(t *testing.T) { diff --git a/protocol/codec.go b/protocol/codec.go index e74b4b3e8e..3bd72eb698 100644 --- a/protocol/codec.go +++ b/protocol/codec.go @@ -275,6 +275,16 @@ func (d *MsgpDecoderBytes) Decode(objptr msgp.Unmarshaler) error { return nil } +// Consumed returns number of bytes consumed so far. +func (d *MsgpDecoderBytes) Consumed() int { + return d.pos +} + +// Remaining returns number of bytes remained in the input buffer. +func (d *MsgpDecoderBytes) Remaining() int { + return len(d.b) - d.pos +} + // encodingPool holds temporary byte slice buffers used for encoding messages. var encodingPool = sync.Pool{ New: func() interface{} { diff --git a/scripts/travis/codegen_verification.sh b/scripts/travis/codegen_verification.sh index 8ba594d7b1..9ac11f1724 100755 --- a/scripts/travis/codegen_verification.sh +++ b/scripts/travis/codegen_verification.sh @@ -31,10 +31,10 @@ echo "Running check_license..." ./scripts/check_license.sh echo "Rebuild swagger.json files" -make rebuild_swagger +make rebuild_kmd_swagger -echo "Regenerate config files" -go generate ./config +echo "Regenerate for stringer et el." +make generate echo "Running fixcheck" GOPATH=$(go env GOPATH) diff --git a/test/e2e-go/features/participation/deletePartKeys_test.go b/test/e2e-go/features/participation/deletePartKeys_test.go new file mode 100644 index 0000000000..5c7755ca74 --- /dev/null +++ b/test/e2e-go/features/participation/deletePartKeys_test.go @@ -0,0 +1,53 @@ +// Copyright (C) 2019-2022 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 participation + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/framework/fixtures" + "github.com/algorand/go-algorand/test/partitiontest" +) + +// TestDeletePartKey tests that the deletepartkey subcommand works +func TestDeletePartKey(t *testing.T) { + partitiontest.PartitionTest(t) + + // Start devmode network and initialize things for the test. + var fixture fixtures.RestClientFixture + fixture.SetupNoStart(t, filepath.Join("nettemplates", "DevModeOneWallet.json")) + fixture.Start() + defer fixture.Shutdown() + sClient := fixture.GetLibGoalClientForNamedNode("Node") + + partKeyResponse, err := sClient.GetParticipationKeys() + require.NoError(t, err) + numberOfPartKeys := len(partKeyResponse) + require.True(t, numberOfPartKeys > 0) + participationID := partKeyResponse[0].Id + err = sClient.RemoveParticipationKey(participationID) + require.NoError(t, err) + + newPartKeyResponse, err := sClient.GetParticipationKeys() + require.NoError(t, err) + newNumberOfPartKeys := len(newPartKeyResponse) + require.True(t, newNumberOfPartKeys < numberOfPartKeys) + +} diff --git a/test/framework/fixtures/restClientFixture.go b/test/framework/fixtures/restClientFixture.go index 9239895cde..5150e5c28e 100644 --- a/test/framework/fixtures/restClientFixture.go +++ b/test/framework/fixtures/restClientFixture.go @@ -30,7 +30,6 @@ import ( "github.com/algorand/go-algorand/daemon/algod/api/client" v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" - v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" "github.com/algorand/go-algorand/libgoal" "github.com/algorand/go-algorand/nodecontrol" @@ -417,13 +416,3 @@ func (f *RestClientFixture) AssertValidTxid(txid string) { } require.True(f.t, allLettersOrNumbers, "txid should be all letters") } - -// AccountListContainsAddress searches the passed account list for the passed account address -func (f *RestClientFixture) AccountListContainsAddress(searchList []v1.Account, address string) bool { - for _, item := range searchList { - if item.Address == address { - return true - } - } - return false -} diff --git a/test/scripts/e2e_client_runner.py b/test/scripts/e2e_client_runner.py index 53a0e484ef..7da46e2a81 100755 --- a/test/scripts/e2e_client_runner.py +++ b/test/scripts/e2e_client_runner.py @@ -58,7 +58,7 @@ def openalgod(algodata): algodnet = open(algodnetpath, 'rt').read().strip() algodtokenpath = os.path.join(algodata,'algod.token') algodtoken = open(algodtokenpath, 'rt').read().strip() - algod = algosdk.algod.AlgodClient(algodtoken, 'http://' + algodnet) + algod = algosdk.v2client.algod.AlgodClient(algodtoken, 'http://' + algodnet) return algod def read_script_for_timeout(fname): @@ -112,9 +112,9 @@ def _script_thread_inner(runset, scriptname, timeout): # send one million Algos to the test wallet's account params = algod.suggested_params() - round = params['lastRound'] + round = params.first max_init_wait_rounds = 5 - txn = algosdk.transaction.PaymentTxn(sender=maxpubaddr, fee=params['minFee'], first=round, last=round+max_init_wait_rounds, gh=params['genesishashb64'], receiver=addr, amt=1000000000000, flat_fee=True) + txn = algosdk.transaction.PaymentTxn(sender=maxpubaddr, fee=params.min_fee, first=round, last=round+max_init_wait_rounds, gh=params.gh, receiver=addr, amt=1000000000000, flat_fee=True) stxn = kmd.sign_transaction(pubw, '', txn) txid = algod.send_transaction(stxn) ptxinfo = None @@ -123,7 +123,7 @@ def _script_thread_inner(runset, scriptname, timeout): if txinfo.get('round'): break status = algod.status_after_block(round_num=round) - round = status['lastRound'] + round = status['last-round'] if ptxinfo is not None: sys.stderr.write('failed to initialize temporary test wallet account for test ({}) for {} rounds.\n'.format(scriptname, max_init_wait_rounds)) diff --git a/test/scripts/e2e_subs/e2e-app-abi-method.sh b/test/scripts/e2e_subs/e2e-app-abi-method.sh index 46cc5406ab..ac5d4f5925 100755 --- a/test/scripts/e2e_subs/e2e-app-abi-method.sh +++ b/test/scripts/e2e_subs/e2e-app-abi-method.sh @@ -79,7 +79,7 @@ if [[ $RES != *"${EXPECTED}"* ]]; then false fi -# Foreign reference test +# Foreign reference test during non-creation call. A creation test is further down. RES=$(${gcmd} app method --method "referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9]" --arg KGTOR3F3Q74JP4LB5M3SOCSJ4BOPOKZ2GPSLMLLGCWYWRXZJNN4LYQJXXU --arg $APPID --arg $ACCOUNT --arg 10 --arg KGTOR3F3Q74JP4LB5M3SOCSJ4BOPOKZ2GPSLMLLGCWYWRXZJNN4LYQJXXU --arg 11 --arg 10 --arg 20 --arg 21 --app-account 2R5LMPTYLVMWYEG4RPI26PJAM7ARTGUB7LZSONQPGLUWTPOP6LQCJTQZVE --foreign-app 21 --foreign-asset 10 --app-id $APPID --from $ACCOUNT 2>&1 || true) EXPECTED="method referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9] succeeded with output: [2,0,2,0,2,1,0,1,0]" if [[ $RES != *"${EXPECTED}"* ]]; then @@ -110,3 +110,11 @@ if [[ $RES != *"${EXPECTED}"* ]]; then date '+app-abi-method-test FAIL the method call to delete()void should not fail %Y%m%d_%H%M%S' false fi + +# Foreign reference test during creation +RES=$(${gcmd} app method --create --approval-prog ${DIR}/tealprogs/app-abi-method-example.teal --clear-prog ${TEMPDIR}/simple-v2.teal --global-byteslices 0 --global-ints 0 --local-byteslices 1 --local-ints 0 --extra-pages 0 --on-completion deleteapplication --method "referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9]" --arg KGTOR3F3Q74JP4LB5M3SOCSJ4BOPOKZ2GPSLMLLGCWYWRXZJNN4LYQJXXU --arg 0 --arg $ACCOUNT --arg 10 --arg KGTOR3F3Q74JP4LB5M3SOCSJ4BOPOKZ2GPSLMLLGCWYWRXZJNN4LYQJXXU --arg 11 --arg 10 --arg 20 --arg 21 --app-account 2R5LMPTYLVMWYEG4RPI26PJAM7ARTGUB7LZSONQPGLUWTPOP6LQCJTQZVE --foreign-app 21 --foreign-asset 10 --from $ACCOUNT 2>&1 || true) +EXPECTED="method referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9] succeeded with output: [2,0,2,0,2,1,0,1,0]" +if [[ $RES != *"${EXPECTED}"* ]]; then + date '+app-abi-method-test FAIL the creation method call to referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9] should not fail %Y%m%d_%H%M%S' + false +fi diff --git a/test/scripts/e2e_subs/tealprogs/app-abi-method-example.teal b/test/scripts/e2e_subs/tealprogs/app-abi-method-example.teal index 83c4f0fdd8..a6e68e76e7 100644 --- a/test/scripts/e2e_subs/tealprogs/app-abi-method-example.teal +++ b/test/scripts/e2e_subs/tealprogs/app-abi-method-example.teal @@ -1,154 +1,313 @@ -// generated from https://gist.github.com/jasonpaulos/99e4f8a75f2fc2ec9b8073c064530359/b4d37519ccc67383042b6c0fb8b7b26a2e538738 +// generated from https://gist.github.com/jasonpaulos/99e4f8a75f2fc2ec9b8073c064530359/bf36d82afaea0e5974fa172415153e77c3b5d42a #pragma version 5 -txn ApplicationID +txn NumAppArgs int 0 == -bnz main_l18 -txn OnCompletion -int UpdateApplication +bnz main_l20 +txna ApplicationArgs 0 +method "create(uint64)uint64" == +bnz main_l19 txna ApplicationArgs 0 -byte 0xa0e81872 +method "update()void" == -&& -bnz main_l17 -txn OnCompletion -int OptIn +bnz main_l18 +txna ApplicationArgs 0 +method "optIn(string)string" == +bnz main_l17 txna ApplicationArgs 0 -byte 0xcfa68e36 +method "closeOut()string" == -&& bnz main_l16 -txn OnCompletion -int CloseOut -== txna ApplicationArgs 0 -byte 0xa9f42b3d +method "delete()void" == -&& bnz main_l15 -txn OnCompletion -int DeleteApplication -== txna ApplicationArgs 0 -byte 0x24378d3c +method "add(uint64,uint64)uint64" == -&& bnz main_l14 -txn OnCompletion -int NoOp -== txna ApplicationArgs 0 -byte 0xfe6bdf69 +method "empty()void" == -&& bnz main_l13 -txn OnCompletion -int NoOp -== txna ApplicationArgs 0 -byte 0xa88c26a5 +method "payment(pay,uint64)bool" == -&& bnz main_l12 -txn OnCompletion -int NoOp -== txna ApplicationArgs 0 -byte 0x3e3b3d28 +method "referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9]" == -&& bnz main_l11 +err +main_l11: txn OnCompletion int NoOp == -txna ApplicationArgs 0 -byte 0x0df0050f +txn OnCompletion +int DeleteApplication == -&& -bnz main_l10 +txn ApplicationID int 0 -return -main_l10: +== +&& +|| +assert txna ApplicationArgs 1 +int 0 +getbyte +store 13 txna ApplicationArgs 2 +int 0 +getbyte +store 14 txna ApplicationArgs 3 +int 0 +getbyte +store 15 txna ApplicationArgs 4 +int 0 +getbyte +store 16 txna ApplicationArgs 5 +int 0 +getbyte +store 17 txna ApplicationArgs 6 +int 0 +getbyte +store 18 txna ApplicationArgs 7 +int 0 +getbyte +store 19 txna ApplicationArgs 8 +int 0 +getbyte +store 20 txna ApplicationArgs 9 -callsub sub8 +int 0 +getbyte +store 21 +load 13 +load 14 +load 15 +load 16 +load 17 +load 18 +load 19 +load 20 +load 21 +callsub referenceTest_8 +store 22 +byte 0x151f7c75 +load 22 +concat +log int 1 return -main_l11: +main_l12: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert txna ApplicationArgs 1 -callsub sub7 +btoi +store 10 +txn GroupIndex int 1 -return -main_l12: -callsub sub6 +- +store 9 +load 9 +gtxns TypeEnum +int pay +== +assert +load 9 +load 10 +callsub payment_7 +store 11 +byte 0x151f7c75 +byte 0x00 +int 0 +load 11 +setbit +concat +log int 1 return main_l13: -txna ApplicationArgs 1 -txna ApplicationArgs 2 -callsub sub5 +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +callsub empty_6 int 1 return main_l14: -callsub sub4 +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 6 +txna ApplicationArgs 2 +btoi +store 7 +load 6 +load 7 +callsub add_5 +store 8 +byte 0x151f7c75 +load 8 +itob +concat +log int 1 return main_l15: -callsub sub3 +txn OnCompletion +int DeleteApplication +== +assert +callsub delete_4 int 1 return main_l16: -txna ApplicationArgs 1 -callsub sub2 +txn OnCompletion +int CloseOut +== +txn ApplicationID +int 0 +!= +&& +assert +callsub closeOut_3 +store 4 +byte 0x151f7c75 +load 4 +concat +log int 1 return main_l17: -callsub sub1 +txn OnCompletion +int OptIn +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +callsub optIn_2 +store 1 +byte 0x151f7c75 +load 1 +concat +log int 1 return main_l18: -txn NumAppArgs +txn OnCompletion +int UpdateApplication +== +txn ApplicationID int 0 -> -bnz main_l20 -main_l19: +!= +&& +assert +callsub update_1 int 1 return -main_l20: -txna ApplicationArgs 0 -byte 0x43464101 +main_l19: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +== +&& +txn OnCompletion +int OptIn +== +txn ApplicationID +int 0 == +&& +|| +txn OnCompletion +int UpdateApplication +== +txn ApplicationID +int 0 +== +&& +|| +txn OnCompletion +int DeleteApplication +== +txn ApplicationID +int 0 +== +&& +|| assert txna ApplicationArgs 1 -callsub sub0 -b main_l19 -sub0: // create +btoi +callsub create_0 store 0 byte 0x151f7c75 load 0 -btoi -int 2 -* itob concat log +int 1 +return +main_l20: +txn OnCompletion +int NoOp +== +bnz main_l22 +err +main_l22: +txn ApplicationID +int 0 +== +assert +int 1 +return + +// create +create_0: +int 2 +* retsub -sub1: // update + +// update +update_1: retsub -sub2: // optIn -store 1 + +// optIn +optIn_2: +store 2 int 0 byte "name" -load 1 +load 2 extract 2 0 app_local_put byte "hello " @@ -156,111 +315,169 @@ int 0 byte "name" app_local_get concat -store 2 -byte 0x151f7c75 -load 2 +store 3 +load 3 len itob -extract 6 2 -concat -load 2 +extract 6 0 +load 3 concat -log +store 3 +load 3 retsub -sub3: // closeOut + +// closeOut +closeOut_3: byte "goodbye " int 0 byte "name" app_local_get concat -store 3 -byte 0x151f7c75 -load 3 +store 5 +load 5 len itob -extract 6 2 -concat -load 3 +extract 6 0 +load 5 concat -log +store 5 +load 5 retsub -sub4: // deleteApp + +// delete +delete_4: txn Sender global CreatorAddress == assert retsub -sub5: // add -store 5 -store 4 -byte 0x151f7c75 -load 4 -btoi -load 5 -btoi + +// add +add_5: + -itob -concat -log retsub -sub6: // empty + +// empty +empty_6: byte "random inconsequential log" log retsub -sub7: // payment -store 6 -txn GroupIndex -int 1 -- -gtxns TypeEnum -int pay -== -assert -byte 0x151f7c75 -txn GroupIndex -int 1 -- + +// payment +payment_7: +store 12 gtxns Amount -load 6 -btoi +load 12 == -bnz sub7_l2 -byte 0x00 -b sub7_l3 -sub7_l2: -byte 0x80 -sub7_l3: -concat -log +! +! retsub -sub8: // referenceTest -store 15 -store 14 -store 13 -store 12 -store 11 -store 10 -store 9 -store 8 -store 7 -byte 0x151f7c75 -load 7 -concat -load 9 + +// referenceTest +referenceTest_8: +store 30 +store 29 +store 28 +store 27 +store 26 +store 25 +store 24 +store 23 +store 31 +load 31 +int 256 +< +assert +load 24 +store 32 +load 32 +int 256 +< +assert +load 26 +store 33 +load 33 +int 256 +< +assert +load 23 +store 34 +load 34 +int 256 +< +assert +load 29 +store 35 +load 35 +int 256 +< +assert +load 30 +store 36 +load 36 +int 256 +< +assert +load 25 +store 37 +load 37 +int 256 +< +assert +load 27 +store 38 +load 38 +int 256 +< +assert +load 28 +store 39 +load 39 +int 256 +< +assert +byte 0x00 +int 0 +load 31 +setbyte +byte 0x00 +int 0 +load 32 +setbyte concat -load 11 +byte 0x00 +int 0 +load 33 +setbyte concat -load 8 +byte 0x00 +int 0 +load 34 +setbyte concat -load 14 +byte 0x00 +int 0 +load 35 +setbyte concat -load 15 +byte 0x00 +int 0 +load 36 +setbyte concat -load 10 +byte 0x00 +int 0 +load 37 +setbyte concat -load 12 +byte 0x00 +int 0 +load 38 +setbyte concat -load 13 +byte 0x00 +int 0 +load 39 +setbyte concat -log retsub diff --git a/test/scripts/e2e_subs/tealprogs/quine.map b/test/scripts/e2e_subs/tealprogs/quine.map index a7584274e1..62ea5cc401 100644 --- a/test/scripts/e2e_subs/tealprogs/quine.map +++ b/test/scripts/e2e_subs/tealprogs/quine.map @@ -1 +1 @@ -{"version":3,"sources":["test/scripts/e2e_subs/tealprogs/quine.teal"],"names":[],"mapping":";AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA;AACA;;;AACA;;;AACA;AACA;;AACA;AACA;;;AACA;AACA;AACA;;AACA;;AACA;AACA;AACA","mappings":";AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA;AACA;;;AACA;;;AACA;AACA;;AACA;AACA;;;AACA;AACA;AACA;;AACA;;AACA;AACA;AACA"} \ No newline at end of file +{"version":3,"sources":["test/scripts/e2e_subs/tealprogs/quine.teal"],"names":[],"mappings":";AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA;AACA;;;AACA;;;AACA;AACA;;AACA;AACA;;;AACA;AACA;AACA;;AACA;;AACA;AACA;AACA"} \ No newline at end of file diff --git a/test/testdata/configs/config-v26.json b/test/testdata/configs/config-v26.json new file mode 100644 index 0000000000..31d693f90a --- /dev/null +++ b/test/testdata/configs/config-v26.json @@ -0,0 +1,109 @@ +{ + "Version": 26, + "AccountUpdatesStatsInterval": 5000000000, + "AccountsRebuildSynchronousMode": 1, + "AgreementIncomingBundlesQueueLength": 7, + "AgreementIncomingProposalsQueueLength": 25, + "AgreementIncomingVotesQueueLength": 10000, + "AnnounceParticipationKey": true, + "Archival": false, + "BaseLoggerDebugLevel": 4, + "BlockServiceCustomFallbackEndpoints": "", + "BroadcastConnectionsLimit": -1, + "CadaverSizeTarget": 0, + "CatchpointFileHistoryLength": 365, + "CatchpointInterval": 10000, + "CatchpointTracking": 0, + "CatchupBlockDownloadRetryAttempts": 1000, + "CatchupBlockValidateMode": 0, + "CatchupFailurePeerRefreshRate": 10, + "CatchupGossipBlockFetchTimeoutSec": 4, + "CatchupHTTPBlockFetchTimeoutSec": 4, + "CatchupLedgerDownloadRetryAttempts": 50, + "CatchupParallelBlocks": 16, + "ConnectionsRateLimitingCount": 60, + "ConnectionsRateLimitingWindowSeconds": 1, + "DNSBootstrapID": ".algorand.network", + "DNSSecurityFlags": 1, + "DeadlockDetection": 0, + "DeadlockDetectionThreshold": 30, + "DisableLocalhostConnectionRateLimit": true, + "DisableNetworking": false, + "DisableOutgoingConnectionThrottling": false, + "EnableAccountUpdatesStats": false, + "EnableAgreementReporting": false, + "EnableAgreementTimeMetrics": false, + "EnableAssembleStats": false, + "EnableBlockService": false, + "EnableBlockServiceFallbackToArchiver": true, + "EnableCatchupFromArchiveServers": false, + "EnableDeveloperAPI": false, + "EnableGossipBlockService": true, + "EnableIncomingMessageFilter": false, + "EnableLedgerService": false, + "EnableMetricReporting": false, + "EnableOutgoingNetworkMessageFiltering": true, + "EnablePingHandler": true, + "EnableProcessBlockStats": false, + "EnableProfiler": false, + "EnableRequestLogger": false, + "EnableRuntimeMetrics": false, + "EnableTopAccountsReporting": false, + "EnableUsageLog": false, + "EnableVerbosedTransactionSyncLogging": false, + "EndpointAddress": "127.0.0.1:0", + "FallbackDNSResolverAddress": "", + "ForceFetchTransactions": false, + "ForceRelayMessages": false, + "GossipFanout": 4, + "IncomingConnectionsLimit": 800, + "IncomingMessageFilterBucketCount": 5, + "IncomingMessageFilterBucketSize": 512, + "IsIndexerActive": false, + "LedgerSynchronousMode": 2, + "LogArchiveMaxAge": "", + "LogArchiveName": "node.archive.log", + "LogSizeLimit": 1073741824, + "MaxAcctLookback": 4, + "MaxAPIBoxPerApplication": 100000, + "MaxAPIResourcesPerAccount": 100000, + "MaxCatchpointDownloadDuration": 7200000000000, + "MaxConnectionsPerIP": 30, + "MinCatchpointFileDownloadBytesPerSecond": 20480, + "NetAddress": "", + "NetworkMessageTraceServer": "", + "NetworkProtocolVersion": "", + "NodeExporterListenAddress": ":9100", + "NodeExporterPath": "./node_exporter", + "OptimizeAccountsDatabaseOnStartup": false, + "OutgoingMessageFilterBucketCount": 3, + "OutgoingMessageFilterBucketSize": 128, + "ParticipationKeysRefreshInterval": 60000000000, + "PeerConnectionsUpdateInterval": 3600, + "PeerPingPeriodSeconds": 0, + "PriorityPeers": {}, + "ProposalAssemblyTime": 500000000, + "PublicAddress": "", + "ReconnectTime": 60000000000, + "ReservedFDs": 256, + "RestConnectionsHardLimit": 2048, + "RestConnectionsSoftLimit": 1024, + "RestReadTimeoutSeconds": 15, + "RestWriteTimeoutSeconds": 120, + "RunHosted": false, + "SuggestedFeeBlockHistory": 3, + "SuggestedFeeSlidingWindowSize": 50, + "TLSCertFile": "", + "TLSKeyFile": "", + "TelemetryToLog": true, + "TransactionSyncDataExchangeRate": 0, + "TransactionSyncSignificantMessageThreshold": 0, + "TxIncomingFilteringFlags": 1, + "TxPoolExponentialIncreaseFactor": 2, + "TxPoolSize": 75000, + "TxSyncIntervalSeconds": 60, + "TxSyncServeResponseSize": 1000000, + "TxSyncTimeoutSeconds": 30, + "UseXForwardedForAddressField": "", + "VerifiedTranscationsCacheSize": 150000 +} diff --git a/util/db/interfaces.go b/util/db/interfaces.go index d607c08d68..5d4abed852 100644 --- a/util/db/interfaces.go +++ b/util/db/interfaces.go @@ -31,6 +31,7 @@ import ( // be added here as needed. type Queryable interface { Prepare(query string) (*sql.Stmt, error) + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) Query(query string, args ...interface{}) (*sql.Rows, error) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) QueryRow(query string, args ...interface{}) *sql.Row diff --git a/util/db/versioning.go b/util/db/versioning.go index 5b61d776be..28f6b1dbd1 100644 --- a/util/db/versioning.go +++ b/util/db/versioning.go @@ -26,9 +26,9 @@ import ( // 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) { +func GetUserVersion(ctx context.Context, q Queryable) (userVersion int32, err error) { - err = tx.QueryRowContext(ctx, "PRAGMA user_version").Scan(&userVersion) + err = q.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 { @@ -39,15 +39,15 @@ func GetUserVersion(ctx context.Context, tx *sql.Tx) (userVersion int32, err err } // 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) +func SetUserVersion(ctx context.Context, e Executable, userVersion int32) (previousUserVersion int32, err error) { + previousUserVersion, err = GetUserVersion(ctx, e) if err != nil { return } if previousUserVersion == userVersion { return } - _, err = tx.ExecContext(ctx, fmt.Sprintf("PRAGMA user_version = %d", userVersion)) + _, err = e.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. diff --git a/util/metrics/counter.go b/util/metrics/counter.go index 06ea4b0c44..5c195b5f8a 100644 --- a/util/metrics/counter.go +++ b/util/metrics/counter.go @@ -111,6 +111,11 @@ func (counter *Counter) AddMicrosecondsSince(t time.Time, labels map[string]stri counter.AddUint64(uint64(time.Since(t).Microseconds()), labels) } +// GetUint64Value returns the value of the counter. +func (counter *Counter) GetUint64Value() (x uint64) { + return atomic.LoadUint64(&counter.intValue) +} + func (counter *Counter) fastAddUint64(x uint64) { if atomic.AddUint64(&counter.intValue, x) == x { // What we just added is the whole value, this diff --git a/util/metrics/counter_test.go b/util/metrics/counter_test.go index ec253f150a..72e1d3b1ad 100644 --- a/util/metrics/counter_test.go +++ b/util/metrics/counter_test.go @@ -198,3 +198,16 @@ testname{host="myhost"} 2.3 ` require.Equal(t, expected, sbOut.String()) } + +func TestGetValue(t *testing.T) { + partitiontest.PartitionTest(t) + + c := MakeCounter(MetricName{Name: "testname", Description: "testhelp"}) + c.Deregister(nil) + + require.Equal(t, uint64(0), c.GetUint64Value()) + c.Inc(nil) + require.Equal(t, uint64(1), c.GetUint64Value()) + c.Inc(nil) + require.Equal(t, uint64(2), c.GetUint64Value()) +} diff --git a/util/metrics/metrics.go b/util/metrics/metrics.go index d21e8b4c71..af3a21b36c 100644 --- a/util/metrics/metrics.go +++ b/util/metrics/metrics.go @@ -109,6 +109,14 @@ var ( TransactionMessagesBacklogErr = MetricName{Name: "algod_transaction_messages_backlog_err", Description: "Number of transaction messages with some validation error"} // TransactionMessagesRemember "Number of transaction messages remembered in TX handler" TransactionMessagesRemember = MetricName{Name: "algod_transaction_messages_remember", Description: "Number of transaction messages remembered in TX handler"} + // TransactionMessageTxGroupFull "Number of transaction messages with max txns allowed" + TransactionMessageTxGroupFull = MetricName{Name: "algod_transaction_messages_txgroup_full", Description: "Number of transaction messages with max txns allowed"} + // TransactionMessageTxGroupExcessive "Number of transaction messages with greater than max allowed txns" + TransactionMessageTxGroupExcessive = MetricName{Name: "algod_transaction_messages_txgroup_excessive", Description: "Number of transaction messages with greater than max allowed txns"} + // TransactionMessagesDupRawMsg "Number of dupe raw transaction messages dropped" + TransactionMessagesDupRawMsg = MetricName{Name: "algod_transaction_messages_dropped_dup_raw", Description: "Number of dupe raw transaction messages dropped"} + // TransactionMessagesDupCanonical "Number of transaction messages dropped after canonical re-encoding" + TransactionMessagesDupCanonical = MetricName{Name: "algod_transaction_messages_dropped_dup_canonical", Description: "Number of transaction messages dropped after canonical re-encoding"} // TransactionMessagesBacklogSize "Number of transaction messages in the TX handler backlog queue" TransactionMessagesBacklogSize = MetricName{Name: "algod_transaction_messages_backlog_size", Description: "Number of transaction messages in the TX handler backlog queue"}