diff --git a/.golangci.yml b/.golangci.yml index 551341b0f9..c6fdb978d6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -24,7 +24,8 @@ linters: - staticcheck - testifylint - unused - + - forbidigo + settings: dupword: comments-only: true @@ -101,6 +102,11 @@ linters: enable: - error-is-as disable-all: true + forbidigo: + forbid: + - p: ^(require|assert)\.Error$ + pkg: ^github\.com/stretchr/testify/(require|assert)$ + msg: use ErrorContains, ErrorIs, or ErrorAs instead exclusions: generated: lax @@ -147,6 +153,8 @@ linters: text: '^empty-block: this block is empty, you can remove it' - linters: revive text: '^superfluous-else: if block ends with .* so drop this else and outdent its block' + - linters: forbidigo + path-except: ^network/ issues: # Work our way back over time to be clean against all these @@ -164,7 +172,15 @@ severity: formatters: enable: + - gci - gofmt + settings: + gci: + sections: + - standard + - default + - prefix(github.com/algorand) + - prefix(github.com/algorand/go-algorand) exclusions: generated: lax paths: diff --git a/AGENTS.md b/AGENTS.md index 88625609f6..600e54d008 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,6 +154,11 @@ Ledger uses independent state machines that can rebuild from blockchain events, - Race detection enabled for concurrent code validation - Benchmark tests for performance-critical paths +### Paginated Resource Lookups (`ledger/acctupdates.go`) +The `lookupAssetResources`, `lookupApplicationResources`, and `lookupBoxResources` functions follow the same pattern: walk in-memory deltas backwards, query the DB, then merge the two result sets. These functions must stay closely aligned — a bug fix or structural change in one almost certainly requires the same change in the others. They are candidates for future consolidation into shared generic logic. + +Their corresponding `Ledger`-level wrappers in `ledger/ledger.go` (`LookupAssets`, `LookupApplications`, `LookupBoxes`) must each hold `trackerMu.RLock()`, matching every other method that accesses tracker state. + ### Code Organization - Interface-first design for testability and modularity - Dependency injection for component assembly diff --git a/Makefile b/Makefile index e9552dcafc..66aaa930e8 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ GOTAGSLIST := sqlite_unlock_notify sqlite_omit_load_extension GOTAGSLIST += ${GOTAGSCUSTOM} GOTESTCOMMAND := go tool -modfile=tool.mod gotestsum --format pkgname --jsonfile testresults.json -- +GOLINTCOMMAND := go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.1 -c .golangci.yml ifeq ($(OS_TYPE), darwin) # M1 Mac--homebrew install location in /opt/homebrew @@ -97,6 +98,7 @@ default: build fmt: go fmt ./... + $(GOLINTCOMMAND) fmt ./scripts/check_license.sh -i fix: build @@ -106,7 +108,7 @@ modernize: GOTOOLCHAIN=auto go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@v0.39.0 -any=false -bloop=false -rangeint=false -fmtappendf=false -waitgroup=false -stringsbuilder=false -omitzero=false -fix ./... lint: - go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.1 run -c .golangci.yml + $(GOLINTCOMMAND) run warninglint: custom-golangci-lint ./custom-golangci-lint run -c .golangci-warnings.yml diff --git a/agreement/actions.go b/agreement/actions.go index 7f1ee27351..c6b3c76996 100644 --- a/agreement/actions.go +++ b/agreement/actions.go @@ -464,34 +464,28 @@ func disconnectAction(e messageEvent, err *serializableError) action { return networkAction{T: disconnect, Err: err, h: e.Input.messageHandle} } -func broadcastAction(tag protocol.Tag, o interface{}) action { - a := networkAction{T: broadcast, Tag: tag} - // TODO would be good to have compiler check this (and related) type switch - // by specializing one method per type - switch tag { - case protocol.AgreementVoteTag: - a.UnauthenticatedVote = o.(unauthenticatedVote) - case protocol.VoteBundleTag: - a.UnauthenticatedBundle = o.(unauthenticatedBundle) - case protocol.ProposalPayloadTag: - a.CompoundMessage = o.(compoundMessage) - } - return a +// note, there is no broadcastVoteAction +// because when a participant makes a vote, it is processed as voteVerified / handleMessageEvent events +// and then processed like any other votes, resulting in a relayVoteAction instead of a broadcast + +func broadcastBundleAction(b unauthenticatedBundle) action { + return networkAction{T: broadcast, Tag: protocol.VoteBundleTag, UnauthenticatedBundle: b} } -func relayAction(e messageEvent, tag protocol.Tag, o interface{}) action { - a := networkAction{T: relay, h: e.Input.messageHandle, Tag: tag} - // TODO would be good to have compiler check this (and related) type switch - // by specializing one method per type - switch tag { - case protocol.AgreementVoteTag: - a.UnauthenticatedVote = o.(unauthenticatedVote) - case protocol.VoteBundleTag: - a.UnauthenticatedBundle = o.(unauthenticatedBundle) - case protocol.ProposalPayloadTag: - a.CompoundMessage = o.(compoundMessage) - } - return a +func broadcastCompoundAction(msg compoundMessage) action { + return networkAction{T: broadcast, Tag: protocol.ProposalPayloadTag, CompoundMessage: msg} +} + +func relayVoteAction(e messageEvent, v unauthenticatedVote) action { + return networkAction{T: relay, h: e.Input.messageHandle, Tag: protocol.AgreementVoteTag, UnauthenticatedVote: v} +} + +func relayBundleAction(e messageEvent, b unauthenticatedBundle) action { + return networkAction{T: relay, h: e.Input.messageHandle, Tag: protocol.VoteBundleTag, UnauthenticatedBundle: b} +} + +func relayCompoundAction(e messageEvent, msg compoundMessage) action { + return networkAction{T: relay, h: e.Input.messageHandle, Tag: protocol.ProposalPayloadTag, CompoundMessage: msg} } func verifyVoteAction(e messageEvent, r round, p period, taskIndex uint64) action { diff --git a/agreement/agreementtest/simulate_test.go b/agreement/agreementtest/simulate_test.go index 5c61a91af7..2c7d891cf2 100644 --- a/agreement/agreementtest/simulate_test.go +++ b/agreement/agreementtest/simulate_test.go @@ -26,9 +26,10 @@ import ( "testing" "time" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" diff --git a/agreement/common_test.go b/agreement/common_test.go index 6081b2dde8..7d53727aa6 100644 --- a/agreement/common_test.go +++ b/agreement/common_test.go @@ -23,9 +23,10 @@ import ( "math/rand" "testing" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" diff --git a/agreement/credentialArrivalHistory_test.go b/agreement/credentialArrivalHistory_test.go index 915b7f1079..30a9bbb556 100644 --- a/agreement/credentialArrivalHistory_test.go +++ b/agreement/credentialArrivalHistory_test.go @@ -20,8 +20,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestCredentialHistoryStore(t *testing.T) { diff --git a/agreement/cryptoVerifier_test.go b/agreement/cryptoVerifier_test.go index 8880adcf55..1d6206cc84 100644 --- a/agreement/cryptoVerifier_test.go +++ b/agreement/cryptoVerifier_test.go @@ -24,10 +24,11 @@ import ( "testing" "time" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/account" diff --git a/agreement/dynamicFilterTimeoutParams_test.go b/agreement/dynamicFilterTimeoutParams_test.go index 0a5af5e8dd..7b658aecda 100644 --- a/agreement/dynamicFilterTimeoutParams_test.go +++ b/agreement/dynamicFilterTimeoutParams_test.go @@ -20,8 +20,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestSampleIndexIsValid(t *testing.T) { diff --git a/agreement/events_test.go b/agreement/events_test.go index 8bdaf93004..d829f9c9fd 100644 --- a/agreement/events_test.go +++ b/agreement/events_test.go @@ -20,9 +20,10 @@ import ( "encoding/base64" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TestSerializableErrorBackwardCompatibility ensures Err field of type serializableError can be diff --git a/agreement/fuzzer/dropMessageFilter_test.go b/agreement/fuzzer/dropMessageFilter_test.go index 832d54dc8e..71c77914b5 100644 --- a/agreement/fuzzer/dropMessageFilter_test.go +++ b/agreement/fuzzer/dropMessageFilter_test.go @@ -18,6 +18,7 @@ package fuzzer import ( "encoding/json" + "github.com/algorand/go-algorand/protocol" ) diff --git a/agreement/fuzzer/ledger_test.go b/agreement/fuzzer/ledger_test.go index 45a3ae743e..476115f1cf 100644 --- a/agreement/fuzzer/ledger_test.go +++ b/agreement/fuzzer/ledger_test.go @@ -22,6 +22,8 @@ import ( "maps" "math/rand" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -30,7 +32,6 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/committee" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-deadlock" ) const randseed = 0 diff --git a/agreement/fuzzer/messageDelayFilter_test.go b/agreement/fuzzer/messageDelayFilter_test.go index b1c24c1621..aefe7a1bf6 100644 --- a/agreement/fuzzer/messageDelayFilter_test.go +++ b/agreement/fuzzer/messageDelayFilter_test.go @@ -19,6 +19,7 @@ package fuzzer import ( "container/heap" "encoding/json" + "github.com/algorand/go-deadlock" "github.com/algorand/go-algorand/protocol" diff --git a/agreement/fuzzer/messageDuplicationFilter_test.go b/agreement/fuzzer/messageDuplicationFilter_test.go index 00f4a65a14..22760f0fb1 100644 --- a/agreement/fuzzer/messageDuplicationFilter_test.go +++ b/agreement/fuzzer/messageDuplicationFilter_test.go @@ -20,8 +20,9 @@ import ( "container/heap" "encoding/json" - "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/protocol" ) // Duplicate message with delay diff --git a/agreement/fuzzer/messagePriorityQueue_test.go b/agreement/fuzzer/messagePriorityQueue_test.go index a0f65026c1..823c1f8a42 100644 --- a/agreement/fuzzer/messagePriorityQueue_test.go +++ b/agreement/fuzzer/messagePriorityQueue_test.go @@ -18,6 +18,7 @@ package fuzzer import ( "container/heap" + "github.com/algorand/go-algorand/protocol" ) diff --git a/agreement/fuzzer/messageReflectionFilter_test.go b/agreement/fuzzer/messageReflectionFilter_test.go index bf7e0c5428..ad4b3d6139 100644 --- a/agreement/fuzzer/messageReflectionFilter_test.go +++ b/agreement/fuzzer/messageReflectionFilter_test.go @@ -20,8 +20,9 @@ import ( "container/heap" "encoding/json" - "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/protocol" ) // Simulate a 2 way mirror where messages are passed through and also reflected back to sender with a delay diff --git a/agreement/fuzzer/tests_test.go b/agreement/fuzzer/tests_test.go index 098dfa7df2..ac1d401e37 100644 --- a/agreement/fuzzer/tests_test.go +++ b/agreement/fuzzer/tests_test.go @@ -24,7 +24,6 @@ import ( "math" "math/rand" "os" - //ossignal "os/signal" "path/filepath" //"runtime/pprof" @@ -34,9 +33,9 @@ import ( "github.com/stretchr/testify/require" - "github.com/algorand/go-algorand/logging" "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/logging" //"github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" ) diff --git a/agreement/player.go b/agreement/player.go index d6019d95c6..d7440bb60b 100644 --- a/agreement/player.go +++ b/agreement/player.go @@ -520,7 +520,7 @@ func (p *player) partitionPolicy(r routerHandle) (actions []action) { // TODO do we want to authenticate our own bundles? b := bundleResponse.Event.Bundle r.t.logBundleBroadcast(*p, b) - a0 := broadcastAction(protocol.VoteBundleTag, b) + a0 := broadcastBundleAction(b) actions = append(actions, a0) } @@ -550,7 +550,7 @@ func (p *player) partitionPolicy(r routerHandle) (actions []action) { if resStaged.Committable { transmit := compoundMessage{Proposal: resStaged.Payload.u()} r.t.logProposalRepropagate(resStaged.Proposal, bundleRound, bundlePeriod) - a1 := broadcastAction(protocol.ProposalPayloadTag, transmit) + a1 := broadcastCompoundAction(transmit) actions = append(actions, a1) } else { // even if there is no staged value, there may be a pinned value @@ -558,7 +558,7 @@ func (p *player) partitionPolicy(r routerHandle) (actions []action) { if resPinned.PayloadOK { transmit := compoundMessage{Proposal: resPinned.Payload.u()} r.t.logProposalRepropagate(resPinned.Proposal, bundleRound, bundlePeriod) - a1 := broadcastAction(protocol.ProposalPayloadTag, transmit) + a1 := broadcastCompoundAction(transmit) actions = append(actions, a1) } } @@ -628,7 +628,7 @@ func (p *player) handleMessageEvent(r routerHandle, e messageEvent) (actions []a // Dynamic filter timeout feature enabled, and current message // updated the best credential arrival time v := e.Input.Vote - return append(actions, relayAction(e, protocol.AgreementVoteTag, v.u())) + return append(actions, relayVoteAction(e, v.u())) case NoLateCredentialTrackingImpact: // Dynamic filter timeout feature enabled, but current message // may not update the best credential arrival time, so we should @@ -649,14 +649,14 @@ func (p *player) handleMessageEvent(r routerHandle, e messageEvent) (actions []a return append(actions, verifyVoteAction(e, uv.R.Round, uv.R.Period, seq)) } v := e.Input.Vote - a := relayAction(e, protocol.AgreementVoteTag, v.u()) + a := relayVoteAction(e, v.u()) ep := ef.(proposalAcceptedEvent) if ep.PayloadOk { transmit := compoundMessage{ Proposal: ep.Payload.u(), Vote: v.u(), } - a = broadcastAction(protocol.ProposalPayloadTag, transmit) + a = broadcastCompoundAction(transmit) } return append(actions, a) } @@ -677,7 +677,7 @@ func (p *player) handleMessageEvent(r routerHandle, e messageEvent) (actions []a uv := ef.(payloadProcessedEvent).Vote.u() // relay proposal if it has been pipelined - ra := relayAction(e, protocol.ProposalPayloadTag, compoundMessage{Proposal: up, Vote: uv}) + ra := relayCompoundAction(e, compoundMessage{Proposal: up, Vote: uv}) if ep.Round == p.Round { vpa := verifyPayloadAction(e, ep.Round, ep.Period, ep.Pinned) @@ -698,7 +698,7 @@ func (p *player) handleMessageEvent(r routerHandle, e messageEvent) (actions []a } up := e.Input.UnauthenticatedProposal - a := relayAction(e, protocol.ProposalPayloadTag, compoundMessage{Proposal: up, Vote: uv}) + a := relayCompoundAction(e, compoundMessage{Proposal: up, Vote: uv}) actions = append(actions, a) } @@ -740,7 +740,7 @@ func (p *player) handleMessageEvent(r routerHandle, e messageEvent) (actions []a return append(actions, verifyVoteAction(e, uv.R.Round, uv.R.Period, 0)) } // else e.t() == voteVerified v := e.Input.Vote - actions = append(actions, relayAction(e, protocol.AgreementVoteTag, v.u())) + actions = append(actions, relayVoteAction(e, v.u())) a1 := p.handle(r, ef) return append(actions, a1...) @@ -758,7 +758,7 @@ func (p *player) handleMessageEvent(r routerHandle, e messageEvent) (actions []a ub := e.Input.UnauthenticatedBundle return append(actions, verifyBundleAction(e, ub.Round, ub.Period, ub.Step)) } - a0 := relayAction(e, protocol.VoteBundleTag, ef.(thresholdEvent).Bundle) + a0 := relayBundleAction(e, ef.(thresholdEvent).Bundle) a1 := p.handle(r, ef) return append(append(actions, a0), a1...) } diff --git a/agreement/proposalManager_test.go b/agreement/proposalManager_test.go index 5a5c1d0387..561d0978cc 100644 --- a/agreement/proposalManager_test.go +++ b/agreement/proposalManager_test.go @@ -19,9 +19,10 @@ package agreement import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // Creates a proposal manager, and returns it in automata and white box form, along diff --git a/agreement/proposalTracker_test.go b/agreement/proposalTracker_test.go index 9cd9f07f70..5cfa4a3fe5 100644 --- a/agreement/proposalTracker_test.go +++ b/agreement/proposalTracker_test.go @@ -21,9 +21,10 @@ import ( "sort" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func sortedVoteGen(t *testing.T) (votes []vote) { diff --git a/agreement/service_test.go b/agreement/service_test.go index 27f35fba0f..4486ae8315 100644 --- a/agreement/service_test.go +++ b/agreement/service_test.go @@ -29,9 +29,10 @@ import ( "testing" "time" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/account" diff --git a/agreement/sort_test.go b/agreement/sort_test.go index 87ac1594ff..83c7abb657 100644 --- a/agreement/sort_test.go +++ b/agreement/sort_test.go @@ -19,10 +19,11 @@ package agreement import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestSortProposalValueLess(t *testing.T) { diff --git a/buildnumber.dat b/buildnumber.dat index d00491fd7e..573541ac97 100644 --- a/buildnumber.dat +++ b/buildnumber.dat @@ -1 +1 @@ -1 +0 diff --git a/catchup/classBasedPeerSelector.go b/catchup/classBasedPeerSelector.go index 3596f7c7f0..5506e45adc 100644 --- a/catchup/classBasedPeerSelector.go +++ b/catchup/classBasedPeerSelector.go @@ -20,8 +20,9 @@ import ( "errors" "time" - "github.com/algorand/go-algorand/network" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/network" ) // classBasedPeerSelector is a rankPooledPeerSelector that tracks and ranks classes of peers based on their response behavior. diff --git a/catchup/classBasedPeerSelector_test.go b/catchup/classBasedPeerSelector_test.go index ff1470f0c6..ce7ce89f06 100644 --- a/catchup/classBasedPeerSelector_test.go +++ b/catchup/classBasedPeerSelector_test.go @@ -20,9 +20,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // Use to mock the wrapped peer selectors where warranted diff --git a/catchup/fetcher_test.go b/catchup/fetcher_test.go index 8c5cd2aae1..43ad7bc06e 100644 --- a/catchup/fetcher_test.go +++ b/catchup/fetcher_test.go @@ -256,6 +256,11 @@ func (p *testUnicastPeer) RoutingAddr() []byte { } func (p *testUnicastPeer) Request(ctx context.Context, tag protocol.Tag, topics network.Topics) (resp *network.Response, e error) { + select { + case <-ctx.Done(): + return resp, ctx.Err() + default: + } responseChannel := make(chan *network.Response, 1) p.responseChannels[0] = responseChannel diff --git a/catchup/service_test.go b/catchup/service_test.go index e7993225ca..a8ac63eca7 100644 --- a/catchup/service_test.go +++ b/catchup/service_test.go @@ -26,10 +26,11 @@ import ( "testing" "time" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" diff --git a/cmd/algocfg/main.go b/cmd/algocfg/main.go index f503514703..5e2be37ad7 100644 --- a/cmd/algocfg/main.go +++ b/cmd/algocfg/main.go @@ -20,8 +20,9 @@ import ( "fmt" "os" - "github.com/algorand/go-algorand/cmd/util/datadir" "github.com/spf13/cobra" + + "github.com/algorand/go-algorand/cmd/util/datadir" ) func init() { diff --git a/cmd/algocfg/profileCommand_test.go b/cmd/algocfg/profileCommand_test.go index e9914279b7..4d53588c4e 100644 --- a/cmd/algocfg/profileCommand_test.go +++ b/cmd/algocfg/profileCommand_test.go @@ -19,10 +19,9 @@ package main import ( "testing" - "github.com/algorand/go-algorand/config" - "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/test/partitiontest" ) diff --git a/cmd/algod/main.go b/cmd/algod/main.go index 4b1a3a2ed8..a96b6a5473 100644 --- a/cmd/algod/main.go +++ b/cmd/algod/main.go @@ -29,6 +29,10 @@ import ( "strings" "time" + "github.com/gofrs/flock" + + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod" @@ -41,9 +45,6 @@ import ( "github.com/algorand/go-algorand/util" "github.com/algorand/go-algorand/util/metrics" "github.com/algorand/go-algorand/util/tokens" - "github.com/gofrs/flock" - - "github.com/algorand/go-deadlock" ) var dataDirectory = flag.String("d", "", "Root Algorand daemon data path") diff --git a/cmd/algofix/deadlock_test.go b/cmd/algofix/deadlock_test.go index 2e13c4b7bc..475d14bc69 100644 --- a/cmd/algofix/deadlock_test.go +++ b/cmd/algofix/deadlock_test.go @@ -25,8 +25,9 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) const deadlockSimpleSrc = `package main diff --git a/cmd/algoh/blockWatcher_test.go b/cmd/algoh/blockWatcher_test.go index 8d3d2662ac..c271abef6f 100644 --- a/cmd/algoh/blockWatcher_test.go +++ b/cmd/algoh/blockWatcher_test.go @@ -22,10 +22,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func bw(client Client) *blockWatcher { diff --git a/cmd/algoh/blockstats_test.go b/cmd/algoh/blockstats_test.go index cf848e96f4..b67128bcfc 100644 --- a/cmd/algoh/blockstats_test.go +++ b/cmd/algoh/blockstats_test.go @@ -20,13 +20,14 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) type event struct { diff --git a/cmd/algokey/keyreg.go b/cmd/algokey/keyreg.go index 262dc43653..48358daf01 100644 --- a/cmd/algokey/keyreg.go +++ b/cmd/algokey/keyreg.go @@ -94,7 +94,7 @@ func init() { "mainnet": mustConvertB64ToDigest("wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8="), "testnet": mustConvertB64ToDigest("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="), "betanet": mustConvertB64ToDigest("mFgazF+2uRS1tMiL9dsj01hJGySEmPN28B/TjjvpVW0="), - "devnet": mustConvertB64ToDigest("sC3P7e2SdbqKJK0tbiCdK9tdSpbe6XeCGKdoNzmlj0E="), + "devnet": mustConvertB64ToDigest("sjkznd5fmOPzTzMi6BAHa2Ir9DyOxu5H7NH3ratQG1w="), } validNetworkList = slices.Collect(maps.Keys(validNetworks)) } diff --git a/cmd/catchpointdump/bench_report.go b/cmd/catchpointdump/bench_report.go index 970486e926..483c779cf4 100644 --- a/cmd/catchpointdump/bench_report.go +++ b/cmd/catchpointdump/bench_report.go @@ -24,9 +24,10 @@ import ( "runtime" "time" - "github.com/algorand/go-algorand/util" "github.com/google/uuid" "github.com/klauspost/cpuid/v2" + + "github.com/algorand/go-algorand/util" ) type benchStage struct { diff --git a/cmd/catchpointdump/file.go b/cmd/catchpointdump/file.go index 50c68fd440..75b750a846 100644 --- a/cmd/catchpointdump/file.go +++ b/cmd/catchpointdump/file.go @@ -33,6 +33,7 @@ import ( "github.com/spf13/cobra" "github.com/algorand/avm-abi/apps" + cmdutil "github.com/algorand/go-algorand/cmd/util" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" diff --git a/cmd/catchupsrv/download_test.go b/cmd/catchupsrv/download_test.go index bf80605300..5a90a2b6ed 100644 --- a/cmd/catchupsrv/download_test.go +++ b/cmd/catchupsrv/download_test.go @@ -19,8 +19,9 @@ package main import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestBlockToPath(t *testing.T) { diff --git a/cmd/catchupsrv/main.go b/cmd/catchupsrv/main.go index 6bfdae0f9f..b2b06d12e2 100644 --- a/cmd/catchupsrv/main.go +++ b/cmd/catchupsrv/main.go @@ -26,9 +26,10 @@ import ( "path" "strconv" - "github.com/algorand/websocket" "github.com/gorilla/mux" + "github.com/algorand/websocket" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" diff --git a/cmd/catchupsrv/tarblocks.go b/cmd/catchupsrv/tarblocks.go index a74b445806..97206b7ade 100644 --- a/cmd/catchupsrv/tarblocks.go +++ b/cmd/catchupsrv/tarblocks.go @@ -26,8 +26,9 @@ import ( "strconv" "strings" - "github.com/algorand/go-algorand/logging" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/logging" ) type tarBlockSet struct { diff --git a/cmd/dispenser/server.go b/cmd/dispenser/server.go index e6f314a334..a87ee089f8 100644 --- a/cmd/dispenser/server.go +++ b/cmd/dispenser/server.go @@ -18,12 +18,10 @@ package main import ( _ "embed" - "html" - - // "bytes" "encoding/json" "flag" "fmt" + "html" "io" "log" "net/http" diff --git a/cmd/goal/application.go b/cmd/goal/application.go index 01614a49a1..45c8c7c671 100644 --- a/cmd/goal/application.go +++ b/cmd/goal/application.go @@ -32,6 +32,7 @@ import ( "github.com/algorand/avm-abi/abi" "github.com/algorand/avm-abi/apps" + "github.com/algorand/go-algorand/cmd/util/datadir" "github.com/algorand/go-algorand/crypto" apiclient "github.com/algorand/go-algorand/daemon/algod/api/client" diff --git a/cmd/goal/application_test.go b/cmd/goal/application_test.go index bb5d7e1bf7..c3375a01c0 100644 --- a/cmd/goal/application_test.go +++ b/cmd/goal/application_test.go @@ -21,9 +21,10 @@ import ( "slices" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestParseMethodArgJSONtoByteSlice(t *testing.T) { diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index 23c4488b15..4a93692d57 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -27,6 +27,8 @@ import ( "strings" "time" + "github.com/spf13/cobra" + "github.com/algorand/go-algorand/cmd/util/datadir" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -41,8 +43,6 @@ import ( "github.com/algorand/go-algorand/libgoal" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util" - - "github.com/spf13/cobra" ) var ( diff --git a/cmd/goal/clerk_test.go b/cmd/goal/clerk_test.go index c590bdb46c..52306f6d77 100644 --- a/cmd/goal/clerk_test.go +++ b/cmd/goal/clerk_test.go @@ -20,8 +20,9 @@ import ( "path/filepath" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func abs(t *testing.T, path string) string { diff --git a/cmd/goal/commands_test.go b/cmd/goal/commands_test.go index 2cebf09eda..238c820561 100644 --- a/cmd/goal/commands_test.go +++ b/cmd/goal/commands_test.go @@ -20,9 +20,10 @@ import ( "os" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/cmd/util/datadir" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestEnsureDataDirReturnsWhenDataDirIsProvided(t *testing.T) { // nolint:paralleltest // Sets shared OS environment variable. diff --git a/cmd/goal/formatting_test.go b/cmd/goal/formatting_test.go index bb87249dc8..faba1aedef 100644 --- a/cmd/goal/formatting_test.go +++ b/cmd/goal/formatting_test.go @@ -19,9 +19,10 @@ package main import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestUnicodePrintable(t *testing.T) { diff --git a/cmd/goal/node_test.go b/cmd/goal/node_test.go index 5ab6b3b53a..0873e52412 100644 --- a/cmd/goal/node_test.go +++ b/cmd/goal/node_test.go @@ -22,10 +22,11 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/ledger/ledgercore" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/test/partitiontest" ) var isNum = regexp.MustCompile(`^[0-9]+$`) diff --git a/cmd/goal/p2pid.go b/cmd/goal/p2pid.go index 2ccdfdc571..8201e44827 100644 --- a/cmd/goal/p2pid.go +++ b/cmd/goal/p2pid.go @@ -23,12 +23,13 @@ import ( "os" "path" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/spf13/cobra" + "github.com/algorand/go-algorand/cmd/util/datadir" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/util" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/spf13/cobra" ) var p2pID = &cobra.Command{ diff --git a/cmd/goal/tealsign.go b/cmd/goal/tealsign.go index 74b718ece2..a7b6f40a13 100644 --- a/cmd/goal/tealsign.go +++ b/cmd/goal/tealsign.go @@ -21,13 +21,13 @@ import ( "encoding/base64" "os" + "github.com/spf13/cobra" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/protocol" - - "github.com/spf13/cobra" ) var ( diff --git a/cmd/netgoal/commands.go b/cmd/netgoal/commands.go index c87f060816..172b1e9cd1 100644 --- a/cmd/netgoal/commands.go +++ b/cmd/netgoal/commands.go @@ -20,10 +20,10 @@ import ( "fmt" "os" - "github.com/algorand/go-deadlock" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" + + "github.com/algorand/go-deadlock" ) var log *logrus.Logger diff --git a/cmd/tealdbg/cdtdbg.go b/cmd/tealdbg/cdtdbg.go index ea38efff7f..297864888c 100644 --- a/cmd/tealdbg/cdtdbg.go +++ b/cmd/tealdbg/cdtdbg.go @@ -23,9 +23,10 @@ import ( "net/http" "time" - "github.com/algorand/go-deadlock" "github.com/gorilla/mux" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/cmd/tealdbg/cdt" ) diff --git a/cmd/tealdbg/dryrunRequest.go b/cmd/tealdbg/dryrunRequest.go index e89e8cfc3d..bf1d4e6a05 100644 --- a/cmd/tealdbg/dryrunRequest.go +++ b/cmd/tealdbg/dryrunRequest.go @@ -17,13 +17,13 @@ package main import ( + "fmt" "log" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/protocol" - v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/protocol" ) // ddrFromParams converts serialized DryrunRequest to v2.DryrunRequest @@ -69,8 +69,12 @@ func balanceRecordsFromDdr(ddr *v2.DryrunRequest) (records []basics.BalanceRecor return } // deserialize app params and update account data + if a.Params == nil { + err = fmt.Errorf("application %d has no params", a.Id) + return + } var params basics.AppParams - params, err = v2.ApplicationParamsToAppParams(&a.Params) + params, err = v2.ApplicationParamsToAppParams(a.Params) if err != nil { return } diff --git a/cmd/tealdbg/local_test.go b/cmd/tealdbg/local_test.go index c781e1d3c0..1e8ac5d566 100644 --- a/cmd/tealdbg/local_test.go +++ b/cmd/tealdbg/local_test.go @@ -27,6 +27,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" "github.com/algorand/go-algorand/data/basics" @@ -35,8 +38,6 @@ import ( "github.com/algorand/go-algorand/ledger/apply" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var txnSample string = `{ @@ -1060,7 +1061,7 @@ func TestLocalBalanceAdapterIndexer(t *testing.T) { case strings.HasPrefix(r.URL.Path, accountPath): w.WriteHeader(200) if r.URL.Path[len(accountPath):] == brs.Addr.String() { - account, err := v2.AccountDataToAccount(brs.Addr.String(), &brs.AccountData, 100, &config.ConsensusParams{MinBalance: 100000}, basics.MicroAlgos{Raw: 0}) + account, err := v2.AccountDataToAccount(brs.Addr.String(), &brs.AccountData, 100, &config.ConsensusParams{MinBalance: 100000}, basics.MicroAlgos{Raw: 0}, v2.AccountDataToAccountOptions{}) a.NoError(err) accountResponse := AccountIndexerResponse{Account: account, CurrentRound: 100} response, err := json.Marshal(accountResponse) diff --git a/cmd/tealdbg/server.go b/cmd/tealdbg/server.go index 216c746e24..64131ad30e 100644 --- a/cmd/tealdbg/server.go +++ b/cmd/tealdbg/server.go @@ -25,9 +25,11 @@ import ( "strings" "time" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/websocket" "github.com/gorilla/mux" + + "github.com/algorand/websocket" + + "github.com/algorand/go-algorand/data/basics" ) const ( diff --git a/cmd/tealdbg/server_test.go b/cmd/tealdbg/server_test.go index 04c03725de..b0106429d8 100644 --- a/cmd/tealdbg/server_test.go +++ b/cmd/tealdbg/server_test.go @@ -22,9 +22,10 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/gorilla/mux" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) type mockFactory struct{} diff --git a/cmd/tealdbg/webdbg.go b/cmd/tealdbg/webdbg.go index 9d4dde8b44..e15e4a3220 100644 --- a/cmd/tealdbg/webdbg.go +++ b/cmd/tealdbg/webdbg.go @@ -25,10 +25,12 @@ import ( "log" "net/http" - "github.com/algorand/go-algorand/protocol" + "github.com/gorilla/mux" + "github.com/algorand/go-deadlock" "github.com/algorand/websocket" - "github.com/gorilla/mux" + + "github.com/algorand/go-algorand/protocol" ) // WebPageFrontend is web page debugging frontend diff --git a/cmd/tealdbg/webdbg_test.go b/cmd/tealdbg/webdbg_test.go index cb36742821..2a229a70a4 100644 --- a/cmd/tealdbg/webdbg_test.go +++ b/cmd/tealdbg/webdbg_test.go @@ -23,9 +23,10 @@ import ( "net/http/httptest" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/gorilla/mux" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestWebPageFrontendHandlers(t *testing.T) { diff --git a/config/consensus_test.go b/config/consensus_test.go index 53af3a9aaa..296cb41b1e 100644 --- a/config/consensus_test.go +++ b/config/consensus_test.go @@ -19,8 +19,9 @@ package config import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestConsensusParams(t *testing.T) { diff --git a/config/dnsbootstrap_test.go b/config/dnsbootstrap_test.go index 43caa73134..885ccf6565 100644 --- a/config/dnsbootstrap_test.go +++ b/config/dnsbootstrap_test.go @@ -17,14 +17,15 @@ package config import ( - "github.com/algorand/go-algorand/protocol" - "pgregory.net/rapid" "strings" "testing" + "github.com/stretchr/testify/assert" + "pgregory.net/rapid" + "github.com/algorand/go-algorand/internal/rapidgen" + "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/assert" ) /** diff --git a/config/version.go b/config/version.go index 45955101fa..0a5c3a7040 100644 --- a/config/version.go +++ b/config/version.go @@ -33,7 +33,7 @@ const VersionMajor = 4 // 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 = 5 +const VersionMinor = 6 // Version is the type holding our full version information. type Version struct { diff --git a/config/version_test.go b/config/version_test.go index bf289d65de..e491993d87 100644 --- a/config/version_test.go +++ b/config/version_test.go @@ -20,9 +20,10 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/s3" - "github.com/stretchr/testify/require" ) func TestAlgodVsUpdatedVersions(t *testing.T) { diff --git a/crypto/batchverifier.go b/crypto/batchverifier.go index a4d5d6a32f..74053f9fea 100644 --- a/crypto/batchverifier.go +++ b/crypto/batchverifier.go @@ -39,6 +39,7 @@ package crypto // size_t num, // int *valid_p); import "C" + import ( "errors" "unsafe" diff --git a/crypto/batchverifier_bench_test.go b/crypto/batchverifier_bench_test.go index 0fc983a831..7ba4d49ef6 100644 --- a/crypto/batchverifier_bench_test.go +++ b/crypto/batchverifier_bench_test.go @@ -21,8 +21,9 @@ import ( "io" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func randSignedMsg(t testing.TB, r io.Reader) (SignatureVerifier, Hashable, Signature) { diff --git a/crypto/encoding_test.go b/crypto/encoding_test.go index 1a72fcad2d..6761f0be17 100644 --- a/crypto/encoding_test.go +++ b/crypto/encoding_test.go @@ -20,8 +20,6 @@ import ( "testing" "github.com/algorand/go-algorand/test/partitiontest" - // "github.com/stretchr/testify/require" - // "github.com/algorand/go-algorand/protocol" ) func TestEmptyEncoding(t *testing.T) { diff --git a/crypto/falconWrapper_test.go b/crypto/falconWrapper_test.go index a105ed8845..b30c9f8881 100644 --- a/crypto/falconWrapper_test.go +++ b/crypto/falconWrapper_test.go @@ -19,9 +19,11 @@ package crypto import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/falcon" + "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestSignAndVerifyFalcon(t *testing.T) { diff --git a/crypto/gobatchverifier_test.go b/crypto/gobatchverifier_test.go index a096ec5fe7..1049ed78a7 100644 --- a/crypto/gobatchverifier_test.go +++ b/crypto/gobatchverifier_test.go @@ -31,10 +31,11 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" ) // ensure internal ed25519 types match the expected []byte lengths used by ed25519consensus package diff --git a/crypto/hashes.go b/crypto/hashes.go index 039c38d239..2b0be41ca6 100644 --- a/crypto/hashes.go +++ b/crypto/hashes.go @@ -23,8 +23,9 @@ import ( "fmt" "hash" - "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-sumhash" + + "github.com/algorand/go-algorand/protocol" ) // HashType represents different hash functions diff --git a/crypto/hashes_test.go b/crypto/hashes_test.go index 0f748322e9..3a29780cc4 100644 --- a/crypto/hashes_test.go +++ b/crypto/hashes_test.go @@ -19,9 +19,9 @@ package crypto import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestHashFactoryCreatingNewHashes(t *testing.T) { diff --git a/crypto/merklearray/proof.go b/crypto/merklearray/proof.go index 5ea9fabf1d..9311298d03 100644 --- a/crypto/merklearray/proof.go +++ b/crypto/merklearray/proof.go @@ -19,8 +19,9 @@ package merklearray import ( "fmt" - "github.com/algorand/go-algorand/crypto" "github.com/algorand/msgp/msgp" + + "github.com/algorand/go-algorand/crypto" ) // Proof is used to convince a verifier about membership of leaves: h0,h1...hn diff --git a/crypto/merkletrie/bitset_test.go b/crypto/merkletrie/bitset_test.go index f65d7a818b..1d0d6231a9 100644 --- a/crypto/merkletrie/bitset_test.go +++ b/crypto/merkletrie/bitset_test.go @@ -20,8 +20,9 @@ import ( "math/bits" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestBitSet(t *testing.T) { diff --git a/crypto/onetimesig.go b/crypto/onetimesig.go index 6aee8ae2da..14f3f4e292 100644 --- a/crypto/onetimesig.go +++ b/crypto/onetimesig.go @@ -20,9 +20,10 @@ import ( "encoding/binary" "fmt" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-deadlock" ) // A OneTimeSignature is a cryptographic signature that is produced a limited diff --git a/crypto/secp256k1/scalar_mult_cgo.go b/crypto/secp256k1/scalar_mult_cgo.go index d25046fd53..444f6e62bc 100644 --- a/crypto/secp256k1/scalar_mult_cgo.go +++ b/crypto/secp256k1/scalar_mult_cgo.go @@ -6,11 +6,6 @@ package secp256k1 -import ( - "math/big" - "unsafe" -) - /* #include "libsecp256k1/include/secp256k1.h" @@ -20,6 +15,11 @@ extern int secp256k1_ext_scalar_mul(const secp256k1_context* ctx, const unsigned */ import "C" +import ( + "math/big" + "unsafe" +) + // ScalarMult func func (BitCurve *BitCurve) ScalarMult(Bx, By *big.Int, scalar []byte) (*big.Int, *big.Int) { // Ensure scalar is exactly 32 bytes. We pad always, even if diff --git a/crypto/stateproof/weights_test.go b/crypto/stateproof/weights_test.go index 5620b66fb3..c571f36dfd 100644 --- a/crypto/stateproof/weights_test.go +++ b/crypto/stateproof/weights_test.go @@ -21,10 +21,11 @@ import ( "math" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config/bounds" "github.com/algorand/go-algorand/crypto/merklearray" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestMaxNumberOfRevealsInVerify(t *testing.T) { diff --git a/crypto/util_test.go b/crypto/util_test.go index 7c5184ae4e..3214264331 100644 --- a/crypto/util_test.go +++ b/crypto/util_test.go @@ -20,9 +20,10 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestEncodeDecode(t *testing.T) { diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index b775d6fe41..6f566f007c 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -190,11 +190,15 @@ }, { "name": "exclude", - "description": "When set to `all` will exclude asset holdings, application local state, created asset parameters, any created application parameters. Defaults to `none`.", + "description": "Exclude additional items from the account. Use `all` to exclude asset holdings, application local state, created asset parameters, and created application parameters. Use `created-apps-params` to exclude only the parameters of created applications (returns only application IDs). Use `created-assets-params` to exclude only the parameters of created assets (returns only asset IDs). Multiple values can be comma-separated (e.g., `created-apps-params,created-assets-params`). Note: `all` and `none` cannot be combined with other values. Defaults to `none`.", "in": "query", "required": false, - "type": "string", - "enum": ["all", "none"] + "type": "array", + "items": { + "type": "string", + "enum": ["all", "none", "created-apps-params", "created-assets-params"] + }, + "collectionFormat": "csv" }, { "$ref": "#/parameters/format" @@ -278,7 +282,7 @@ "/v2/accounts/{address}/assets": { "get": { "description": "Lookup an account's asset holdings.", - "tags": ["public", "experimental"], + "tags": ["public", "nonparticipating"], "produces": ["application/json"], "schemes": ["http"], "summary": "Get a list of assets held by an account, inclusive of asset params.", @@ -322,6 +326,56 @@ } } }, + "/v2/accounts/{address}/applications": { + "get": { + "description": "Lookup an account's application holdings (local state and params if the account is the creator).", + "tags": ["public", "nonparticipating"], + "produces": ["application/json"], + "schemes": ["http"], + "summary": "Get a list of applications held by an account.", + "operationId": "AccountApplicationsInformation", + "parameters": [ + { + "$ref": "#/parameters/address" + }, + { + "$ref": "#/parameters/limit" + }, + { + "$ref": "#/parameters/next" + }, + { + "$ref": "#/parameters/include" + } + ], + "responses": { + "200": { + "$ref": "#/responses/AccountApplicationsInformationResponse" + }, + "400": { + "description": "Malformed address", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "401": { + "description": "Invalid API Token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Unknown Error" + } + } + } + }, "/v2/accounts/{address}/applications/{application-id}": { "get": { "description": "Given a specific account public key and application ID, this call returns the account's application local state and global state (AppLocalState and AppParams, if either exists). Global state will only be returned if the provided address is the application's creator.", @@ -2653,6 +2707,35 @@ } } }, + "AccountApplicationResource": { + "description": "AccountApplicationResource describes the account's application resource (local state and params if the account is the creator) for a specific application ID.", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "description": "The application ID.", + "type": "integer", + "x-go-type": "basics.AppIndex" + }, + "created-at-round": { + "description": "Round when the account opted into or created the application.", + "type": "integer", + "x-go-type": "basics.Round" + }, + "deleted": { + "description": "Whether the application has been deleted.", + "type": "boolean" + }, + "app-local-state": { + "description": "\\[appl\\] the application local data stored in this account.\n\nThe raw account uses `AppLocalState` for this type.", + "$ref": "#/definitions/ApplicationLocalState" + }, + "params": { + "description": "\\[appp\\] parameters of the application created by this account including app global data.\n\nThe raw account uses `AppParams` for this type.\nOnly present if the account is the creator and `include=params` is specified.", + "$ref": "#/definitions/ApplicationParams" + } + } + }, "AccountParticipation": { "description": "AccountParticipation describes the parameters used by this account in consensus protocol.", "type": "object", @@ -2699,7 +2782,7 @@ "Asset": { "description": "Specifies both the unique identifier and the parameters for an asset", "type": "object", - "required": ["index", "params"], + "required": ["index"], "properties": { "index": { "description": "unique asset identifier", @@ -3067,7 +3150,7 @@ "Application": { "description": "Application index and its parameters", "type": "object", - "required": ["id", "params"], + "required": ["id"], "properties": { "id": { "description": "\\[appidx\\] application index.", @@ -4191,6 +4274,18 @@ "type": "string", "name": "tx-type", "in": "query" + }, + "include": { + "name": "include", + "description": "Include additional items in the response. Use `params` to include full application parameters (global state, schema, etc.). Multiple values can be comma-separated. Defaults to returning only application IDs and local state.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string", + "enum": ["params"] + }, + "collectionFormat": "csv" } }, "responses": { @@ -4313,6 +4408,30 @@ } } }, + "AccountApplicationsInformationResponse": { + "description": "AccountApplicationsInformationResponse contains a list of application resources for an account.", + "schema": { + "type": "object", + "required": ["round"], + "properties": { + "round": { + "description": "The round for which this information is relevant.", + "type": "integer", + "x-go-type": "basics.Round" + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter. The next token is the next application ID to use as the pagination cursor.", + "type": "string" + }, + "application-resources": { + "type": "array", + "items": { + "$ref": "#/definitions/AccountApplicationResource" + } + } + } + } + }, "AccountApplicationResponse": { "description": "AccountApplicationResponse describes the account's application local state and global state (AppLocalState and AppParams, if either exists) for a specific application ID. Global state will only be returned if the provided address is the application's creator.", "schema": { @@ -4676,7 +4795,7 @@ "schema": { "description": "Supply represents the current supply of MicroAlgos in the system", "type": "object", - "required": ["online-money", "current_round", "total-money"], + "required": ["online-money", "current_round", "total-money", "online-stake"], "properties": { "current_round": { "description": "Round", @@ -4684,7 +4803,12 @@ "x-go-type": "basics.Round" }, "online-money": { - "description": "OnlineMoney", + "description": "Total stake held by accounts with status Online at current_round, including those whose participation keys have expired but have not yet been marked offline.", + "type": "integer", + "x-go-type": "uint64" + }, + "online-stake": { + "description": "Online stake used by agreement to vote for current_round, excluding accounts whose participation keys have expired.", "type": "integer", "x-go-type": "uint64" }, diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index 7331a93cbd..8fe08aa12c 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -62,6 +62,22 @@ "type": "string" } }, + "include": { + "description": "Include additional items in the response. Use `params` to include full application parameters (global state, schema, etc.). Multiple values can be comma-separated. Defaults to returning only application IDs and local state.", + "explode": false, + "in": "query", + "name": "include", + "schema": { + "items": { + "enum": [ + "params" + ], + "type": "string" + }, + "type": "array" + }, + "style": "form" + }, "limit": { "description": "Maximum number of results to return.", "in": "query", @@ -171,6 +187,36 @@ }, "description": "AccountApplicationResponse describes the account's application local state and global state (AppLocalState and AppParams, if either exists) for a specific application ID. Global state will only be returned if the provided address is the application's creator." }, + "AccountApplicationsInformationResponse": { + "content": { + "application/json": { + "schema": { + "properties": { + "application-resources": { + "items": { + "$ref": "#/components/schemas/AccountApplicationResource" + }, + "type": "array" + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter. The next token is the next application ID to use as the pagination cursor.", + "type": "string" + }, + "round": { + "description": "The round for which this information is relevant.", + "type": "integer", + "x-go-type": "basics.Round" + } + }, + "required": [ + "round" + ], + "type": "object" + } + } + }, + "description": "AccountApplicationsInformationResponse contains a list of application resources for an account." + }, "AccountAssetResponse": { "content": { "application/json": { @@ -874,7 +920,12 @@ "x-go-type": "basics.Round" }, "online-money": { - "description": "OnlineMoney", + "description": "Total stake held by accounts with status Online at current_round, including those whose participation keys have expired but have not yet been marked offline.", + "type": "integer", + "x-go-type": "uint64" + }, + "online-stake": { + "description": "Online stake used by agreement to vote for current_round, excluding accounts whose participation keys have expired.", "type": "integer", "x-go-type": "uint64" }, @@ -887,6 +938,7 @@ "required": [ "current_round", "online-money", + "online-stake", "total-money" ], "type": "object" @@ -1148,6 +1200,35 @@ ], "type": "object" }, + "AccountApplicationResource": { + "description": "AccountApplicationResource describes the account's application resource (local state and params if the account is the creator) for a specific application ID.", + "properties": { + "app-local-state": { + "$ref": "#/components/schemas/ApplicationLocalState" + }, + "created-at-round": { + "description": "Round when the account opted into or created the application.", + "type": "integer", + "x-go-type": "basics.Round" + }, + "deleted": { + "description": "Whether the application has been deleted.", + "type": "boolean" + }, + "id": { + "description": "The application ID.", + "type": "integer", + "x-go-type": "basics.AppIndex" + }, + "params": { + "$ref": "#/components/schemas/ApplicationParams" + } + }, + "required": [ + "id" + ], + "type": "object" + }, "AccountAssetHolding": { "description": "AccountAssetHolding describes the account's asset holding and asset parameters (if either exist) for a specific asset ID.", "properties": { @@ -1267,8 +1348,7 @@ } }, "required": [ - "id", - "params" + "id" ], "type": "object" }, @@ -1483,8 +1563,7 @@ } }, "required": [ - "index", - "params" + "index" ], "type": "object" }, @@ -3107,16 +3186,23 @@ "x-go-type": "basics.Address" }, { - "description": "When set to `all` will exclude asset holdings, application local state, created asset parameters, any created application parameters. Defaults to `none`.", + "description": "Exclude additional items from the account. Use `all` to exclude asset holdings, application local state, created asset parameters, and created application parameters. Use `created-apps-params` to exclude only the parameters of created applications (returns only application IDs). Use `created-assets-params` to exclude only the parameters of created assets (returns only asset IDs). Multiple values can be comma-separated (e.g., `created-apps-params,created-assets-params`). Note: `all` and `none` cannot be combined with other values. Defaults to `none`.", + "explode": false, "in": "query", "name": "exclude", "schema": { - "enum": [ - "all", - "none" - ], - "type": "string" - } + "items": { + "enum": [ + "all", + "none", + "created-apps-params", + "created-assets-params" + ], + "type": "string" + }, + "type": "array" + }, + "style": "form" }, { "description": "Configures whether the response object is JSON or MessagePack encoded. If not provided, defaults to JSON.", @@ -3204,6 +3290,131 @@ ] } }, + "/v2/accounts/{address}/applications": { + "get": { + "description": "Lookup an account's application holdings (local state and params if the account is the creator).", + "operationId": "AccountApplicationsInformation", + "parameters": [ + { + "description": "An account public key.", + "in": "path", + "name": "address", + "required": true, + "schema": { + "pattern": "[A-Z0-9]{58}", + "type": "string", + "x-go-type": "basics.Address" + }, + "x-go-type": "basics.Address" + }, + { + "description": "Maximum number of results to return.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "x-go-type": "uint64" + }, + "x-go-type": "uint64" + }, + { + "description": "The next page of results. Use the next token provided by the previous results.", + "in": "query", + "name": "next", + "schema": { + "type": "string" + } + }, + { + "description": "Include additional items in the response. Use `params` to include full application parameters (global state, schema, etc.). Multiple values can be comma-separated. Defaults to returning only application IDs and local state.", + "explode": false, + "in": "query", + "name": "include", + "schema": { + "items": { + "enum": [ + "params" + ], + "type": "string" + }, + "type": "array" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "application-resources": { + "items": { + "$ref": "#/components/schemas/AccountApplicationResource" + }, + "type": "array" + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter. The next token is the next application ID to use as the pagination cursor.", + "type": "string" + }, + "round": { + "description": "The round for which this information is relevant.", + "type": "integer", + "x-go-type": "basics.Round" + } + }, + "required": [ + "round" + ], + "type": "object" + } + } + }, + "description": "AccountApplicationsInformationResponse contains a list of application resources for an account." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Malformed address" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid API Token" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Error" + }, + "default": { + "content": {}, + "description": "Unknown Error" + } + }, + "summary": "Get a list of applications held by an account.", + "tags": [ + "public", + "nonparticipating" + ] + } + }, "/v2/accounts/{address}/applications/{application-id}": { "get": { "description": "Given a specific account public key and application ID, this call returns the account's application local state and global state (AppLocalState and AppParams, if either exists). Global state will only be returned if the provided address is the application's creator.", @@ -3456,7 +3667,7 @@ "summary": "Get a list of assets held by an account, inclusive of asset params.", "tags": [ "public", - "experimental" + "nonparticipating" ] } }, @@ -5521,7 +5732,12 @@ "x-go-type": "basics.Round" }, "online-money": { - "description": "OnlineMoney", + "description": "Total stake held by accounts with status Online at current_round, including those whose participation keys have expired but have not yet been marked offline.", + "type": "integer", + "x-go-type": "uint64" + }, + "online-stake": { + "description": "Online stake used by agreement to vote for current_round, excluding accounts whose participation keys have expired.", "type": "integer", "x-go-type": "uint64" }, @@ -5534,6 +5750,7 @@ "required": [ "current_round", "online-money", + "online-stake", "total-money" ], "type": "object" diff --git a/daemon/algod/api/server/lib/middlewares/auth_test.go b/daemon/algod/api/server/lib/middlewares/auth_test.go index 8ef7900376..937fec4e57 100644 --- a/daemon/algod/api/server/lib/middlewares/auth_test.go +++ b/daemon/algod/api/server/lib/middlewares/auth_test.go @@ -21,9 +21,10 @@ import ( "net/http" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/labstack/echo/v4" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) var errSuccess = errors.New("unexpected success") diff --git a/daemon/algod/api/server/lib/middlewares/cors_test.go b/daemon/algod/api/server/lib/middlewares/cors_test.go index 05ac524960..edad86f15f 100644 --- a/daemon/algod/api/server/lib/middlewares/cors_test.go +++ b/daemon/algod/api/server/lib/middlewares/cors_test.go @@ -21,9 +21,10 @@ import ( "net/http/httptest" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestMakeCORS(t *testing.T) { diff --git a/daemon/algod/api/server/router.go b/daemon/algod/api/server/router.go index acfc946dbb..e0b9adf0ed 100644 --- a/daemon/algod/api/server/router.go +++ b/daemon/algod/api/server/router.go @@ -22,10 +22,9 @@ import ( "net" "net/http" - "golang.org/x/sync/semaphore" - "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "golang.org/x/sync/semaphore" "github.com/algorand/go-algorand/daemon/algod/api/server/common" "github.com/algorand/go-algorand/daemon/algod/api/server/lib" diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go index 7089b19bf9..3de8a01d66 100644 --- a/daemon/algod/api/server/v2/account.go +++ b/daemon/algod/api/server/v2/account.go @@ -39,11 +39,18 @@ func AssetHolding(ah basics.AssetHolding, ai basics.AssetIndex) model.AssetHoldi } } +// AccountDataToAccountOptions specifies options for converting AccountData to Account +type AccountDataToAccountOptions struct { + ExcludeCreatedAppsParams bool + ExcludeCreatedAssetsParams bool +} + // AccountDataToAccount converts basics.AccountData to v2.model.Account func AccountDataToAccount( address string, record *basics.AccountData, lastRound basics.Round, consensus *config.ConsensusParams, amountWithoutPendingRewards basics.MicroAlgos, + opts AccountDataToAccountOptions, ) (model.Account, error) { assets := make([]model.AssetHolding, 0, len(record.Assets)) @@ -60,7 +67,14 @@ func AccountDataToAccount( createdAssets := make([]model.Asset, 0, len(record.AssetParams)) for idx, params := range record.AssetParams { - asset := AssetParamsToAsset(address, idx, ¶ms) + var asset model.Asset + if opts.ExcludeCreatedAssetsParams { + asset = model.Asset{ + Index: idx, + } + } else { + asset = AssetParamsToAsset(address, idx, ¶ms) + } createdAssets = append(createdAssets, asset) } sort.Slice(createdAssets, func(i, j int) bool { @@ -84,7 +98,14 @@ func AccountDataToAccount( createdApps := make([]model.Application, 0, len(record.AppParams)) for appIdx, appParams := range record.AppParams { - app := AppParamsToApplication(address, appIdx, &appParams) + var app model.Application + if opts.ExcludeCreatedAppsParams { + app = model.Application{ + Id: appIdx, + } + } else { + app = AppParamsToApplication(address, appIdx, &appParams) + } createdApps = append(createdApps, app) } sort.Slice(createdApps, func(i, j int) bool { @@ -224,6 +245,9 @@ func AccountToAccountData(a *model.Account) (basics.AccountData, error) { if a.CreatedAssets != nil && len(*a.CreatedAssets) > 0 { assetParams = make(map[basics.AssetIndex]basics.AssetParams, len(*a.CreatedAssets)) for _, ca := range *a.CreatedAssets { + if ca.Params == nil { + continue + } var metadataHash [32]byte if ca.Params.MetadataHash != nil { copy(metadataHash[:], *ca.Params.MetadataHash) @@ -293,11 +317,13 @@ func AccountToAccountData(a *model.Account) (basics.AccountData, error) { if a.CreatedApps != nil && len(*a.CreatedApps) > 0 { appParams = make(map[basics.AppIndex]basics.AppParams, len(*a.CreatedApps)) for _, params := range *a.CreatedApps { - ap, err := ApplicationParamsToAppParams(¶ms.Params) - if err != nil { - return basics.AccountData{}, err + if params.Params != nil { + ap, err := ApplicationParamsToAppParams(params.Params) + if err != nil { + return basics.AccountData{}, err + } + appParams[params.Id] = ap } - appParams[params.Id] = ap } } @@ -408,7 +434,7 @@ func AppParamsToApplication(creator string, appIdx basics.AppIndex, appParams *b extraProgramPages := uint64(appParams.ExtraProgramPages) app := model.Application{ Id: appIdx, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: creator, ApprovalProgram: appParams.ApprovalProgram, ClearStateProgram: appParams.ClearStateProgram, @@ -468,6 +494,6 @@ func AssetParamsToAsset(creator string, idx basics.AssetIndex, params *basics.As return model.Asset{ Index: idx, - Params: assetParams, + Params: &assetParams, } } diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go index 173a7cbf80..8528cadcaf 100644 --- a/daemon/algod/api/server/v2/account_test.go +++ b/daemon/algod/api/server/v2/account_test.go @@ -106,7 +106,7 @@ func TestAccount(t *testing.T) { b := a.WithUpdatedRewards(proto.RewardUnit, 100) addr := basics.Address{}.String() - conv, err := AccountDataToAccount(addr, &b, round, &proto, a.MicroAlgos) + conv, err := AccountDataToAccount(addr, &b, round, &proto, a.MicroAlgos, AccountDataToAccountOptions{}) require.NoError(t, err) require.Equal(t, addr, conv.Address) require.Equal(t, b.MicroAlgos.Raw, conv.Amount) @@ -206,7 +206,7 @@ func TestAccount(t *testing.T) { // convert the same account a few more times to make sure we always // produce the same model.Account for i := 0; i < 10; i++ { - anotherConv, err := AccountDataToAccount(addr, &b, round, &proto, a.MicroAlgos) + anotherConv, err := AccountDataToAccount(addr, &b, round, &proto, a.MicroAlgos, AccountDataToAccountOptions{}) require.NoError(t, err) require.Equal(t, protocol.EncodeJSON(conv), protocol.EncodeJSON(anotherConv)) @@ -223,7 +223,7 @@ func TestAccountRandomRoundTrip(t *testing.T) { for addr, acct := range accts { round := basics.Round(2) proto := config.Consensus[protocol.ConsensusFuture] - conv, err := AccountDataToAccount(addr.String(), &acct, round, &proto, acct.MicroAlgos) + conv, err := AccountDataToAccount(addr.String(), &acct, round, &proto, acct.MicroAlgos, AccountDataToAccountOptions{}) require.NoError(t, err) c, err := AccountToAccountData(&conv) require.NoError(t, err) diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index c940709296..504cfac480 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -27,10 +27,9 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/ledger/apply" "github.com/algorand/go-algorand/ledger/ledgercore" - - "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/protocol" ) @@ -101,6 +100,9 @@ func (dr *DryrunRequest) ExpandSources() error { case "approv", "clearp": for ai, app := range dr.Apps { if app.Id == s.AppIndex { + if dr.Apps[ai].Params == nil { + dr.Apps[ai].Params = &model.ApplicationParams{} + } switch s.FieldName { case "approv": dr.Apps[ai].Params.ApprovalProgram = ops.Program @@ -116,6 +118,19 @@ func (dr *DryrunRequest) ExpandSources() error { return nil } +// ValidateApps ensures all applications have params set. +// This should be called after ExpandSources to ensure that either: +// 1. The caller provided params explicitly, or +// 2. Sources populated the params +func (dr *DryrunRequest) ValidateApps() error { + for _, app := range dr.Apps { + if app.Params == nil { + return fmt.Errorf("application %d does not have params set", app.Id) + } + } + return nil +} + type dryrunDebugReceiver struct { disassembly string lines []string @@ -229,6 +244,9 @@ func (dl *dryrunLedger) init() error { } for i, app := range dl.dr.Apps { var addr basics.Address + if app.Params == nil { + continue + } if app.Params.Creator != "" { var err error addr, err = basics.UnmarshalChecksumAddress(app.Params.Creator) @@ -275,20 +293,22 @@ func (dl *dryrunLedger) lookup(rnd basics.Round, addr basics.Address) (basics.Ac appi, ok := dl.accountApps[addr] if ok { app := dl.dr.Apps[appi] - params, err := ApplicationParamsToAppParams(&app.Params) - if err != nil { - return basics.AccountData{}, 0, err - } - if out.AppParams == nil { - out.AppParams = make(map[basics.AppIndex]basics.AppParams) - out.AppParams[app.Id] = params - } else { - ap, ok := out.AppParams[app.Id] - if ok { - MergeAppParams(&ap, ¶ms) - out.AppParams[app.Id] = ap - } else { + if app.Params != nil { + params, err := ApplicationParamsToAppParams(app.Params) + if err != nil { + return basics.AccountData{}, 0, err + } + if out.AppParams == nil { + out.AppParams = make(map[basics.AppIndex]basics.AppParams) out.AppParams[app.Id] = params + } else { + ap, ok := out.AppParams[app.Id] + if ok { + MergeAppParams(&ap, ¶ms) + out.AppParams[app.Id] = ap + } else { + out.AppParams[app.Id] = params + } } } } @@ -424,6 +444,13 @@ func doDryrunRequest(dr *DryrunRequest, response *model.DryrunResponse) { return } + // Validate that all apps have params after Sources expansion + err = dr.ValidateApps() + if err != nil { + response.Error = err.Error() + return + } + dl := dryrunLedger{dr: dr} err = dl.init() if err != nil { @@ -525,8 +552,8 @@ func doDryrunRequest(dr *DryrunRequest, response *model.DryrunResponse) { var app basics.AppParams ok := false for _, appt := range dr.Apps { - if appt.Id == appIdx { - app, err = ApplicationParamsToAppParams(&appt.Params) + if appt.Id == appIdx && appt.Params != nil { + app, err = ApplicationParamsToAppParams(appt.Params) if err != nil { response.Error = err.Error() return diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go index f4f1d044b8..afbf897dd0 100644 --- a/daemon/algod/api/server/v2/dryrun_test.go +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -174,6 +174,26 @@ int not_an_int`, require.Contains(t, response.Error, "5:4: unable to parse \"not_an_int\" as integer") } +func TestDryrunAppWithoutParams(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // Test that an app without params and without Sources fails validation + dr := DryrunRequest{ + Apps: []model.Application{ + { + Id: 1007, + // No Params, no Sources - should fail validation + }, + }, + } + var response model.DryrunResponse + + doDryrunRequest(&dr, &response) + require.NotEmpty(t, response.Error) + require.Contains(t, response.Error, "application 1007 does not have params set") +} + func TestDryrunLogicSig(t *testing.T) { partitiontest.PartitionTest(t) // {"txns":[{"lsig":{"l":"AiABASI="},"txn":{}}]} @@ -497,7 +517,7 @@ func TestDryrunGlobal1(t *testing.T) { dr.Apps = []model.Application{ { Id: 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: globalTestProgram, GlobalState: &gkv, GlobalStateSchema: &model.ApplicationStateSchema{ @@ -547,7 +567,7 @@ func TestDryrunGlobal2(t *testing.T) { dr.Apps = []model.Application{ { Id: 1234, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: globalTestProgram, GlobalState: &gkv, }, @@ -594,7 +614,7 @@ func TestDryrunLocal1(t *testing.T) { dr.Apps = []model.Application{ { Id: 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: localStateCheckProg, LocalStateSchema: &model.ApplicationStateSchema{ NumByteSlice: 10, @@ -668,7 +688,7 @@ func TestDryrunLocal1A(t *testing.T) { dr.Apps = []model.Application{ { Id: 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ LocalStateSchema: &model.ApplicationStateSchema{ NumByteSlice: 10, NumUint: 10, @@ -746,7 +766,7 @@ func TestDryrunLocalCheck(t *testing.T) { dr.Apps = []model.Application{ { Id: 1234, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: localStateCheckProg, }, }, @@ -806,7 +826,7 @@ func TestDryrunMultipleTxns(t *testing.T) { dr.Apps = []model.Application{ { Id: 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: globalTestProgram, GlobalState: &gkv, GlobalStateSchema: &model.ApplicationStateSchema{ @@ -851,7 +871,7 @@ func TestDryrunEncodeDecode(t *testing.T) { gdr.Apps = []model.Application{ { Id: 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: localStateCheckProg, }, }, @@ -961,7 +981,7 @@ func TestDryrunMakeLedger(t *testing.T) { dr.Apps = []model.Application{ { Id: 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: sender.String(), ApprovalProgram: localStateCheckProg, }, @@ -1156,7 +1176,7 @@ int 1`) Apps: []model.Application{ { Id: appIdx, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: creator.String(), ApprovalProgram: approval, ClearStateProgram: clst, @@ -1247,7 +1267,7 @@ return Apps: []model.Application{ { Id: appIdx, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: creator.String(), ApprovalProgram: approval, ClearStateProgram: clst, @@ -1256,7 +1276,7 @@ return }, { Id: appIdx + 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: creator.String(), ApprovalProgram: approv, ClearStateProgram: clst, @@ -1384,7 +1404,7 @@ int 1`) Apps: []model.Application{ { Id: appIdx, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: creator.String(), ApprovalProgram: app1, ClearStateProgram: clst, @@ -1393,7 +1413,7 @@ int 1`) }, { Id: appIdx + 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: creator.String(), ApprovalProgram: app2, ClearStateProgram: clst, @@ -1402,7 +1422,7 @@ int 1`) }, { Id: appIdx + 2, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: creator.String(), ApprovalProgram: app3, ClearStateProgram: clst, @@ -1484,7 +1504,7 @@ int 1` }.SignedTxn()}, Apps: []model.Application{{ Id: appIdx, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: sender.String(), ApprovalProgram: approval, ClearStateProgram: clst, @@ -1546,7 +1566,7 @@ int 0 Apps: []model.Application{ { Id: appIdx, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ Creator: creator.String(), ApprovalProgram: approval, ClearStateProgram: clst, @@ -1611,7 +1631,7 @@ int 1 }.SignedTxn()}, Apps: []model.Application{{ Id: appIdx, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: paySender.Program, ClearStateProgram: clst, }, @@ -1687,7 +1707,7 @@ int 1`) ApplicationID: appIdx}.SignedTxn()) apps = append(apps, model.Application{ Id: appIdx, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: approvalOps.Program, ClearStateProgram: clst, }, @@ -1806,7 +1826,7 @@ int %d`, expectedUint, i)) dr.Apps = []model.Application{ { Id: 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: ops.Program, GlobalStateSchema: &model.ApplicationStateSchema{ NumByteSlice: 1, @@ -1861,7 +1881,7 @@ func TestDryrunEarlyExit(t *testing.T) { } dr.Apps = []model.Application{{ Id: 1, - Params: model.ApplicationParams{ + Params: &model.ApplicationParams{ ApprovalProgram: ops.Program, }, }} diff --git a/daemon/algod/api/server/v2/generated/data/routes.go b/daemon/algod/api/server/v2/generated/data/routes.go index fdd7e23287..f3ec27a816 100644 --- a/daemon/algod/api/server/v2/generated/data/routes.go +++ b/daemon/algod/api/server/v2/generated/data/routes.go @@ -115,226 +115,232 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "jp5OH4/u/l8AAAD//2Q3BRCzDQEA", + "H4sIAAAAAAAC/+y9e5PbtpIo/lVQ2q3y4ydpxo6Tc+Jfndo7ifOYjRO7MpOcu+vxTSCyJeEMBfAAoEaK", + "r7/7LTQeBElQojSyk2ydfxKPiEej0Wg0+vlulIlVKThwrUbP341KKukKNEj8i+a5BIX/zEFlkpWaCT56", + "PrrghGaZqLgmZTUrWEZuYTsdjUfMfC2pXo7GI05XMHoeBhmPJPyzYhLy0XMtKxiPVLaEFbXTag3S9H1z", + "Mfnv88nnb999+tf3o/FIb0szhtKS8cVoPNpMFmLifpxRxTI1vXDjv9/3lZZlwTJqljBheXpRdRPCcuCa", + "zRnIvoU1x9u1vhXjbFWtRs/Pw5IY17AA2bOmsrzkOWz6FhV9pkqB7l2P+ThgJX6Mk67BDLpzFY0GGdXZ", + "shSM68RKCH4l9nNyCVH3XYuYC7miut0+Ij+kvSfjJ+fv/y2Q4pPxp5+kiZEWCyEpzydh3C/DuOTKtnt/", + "QEP/tY2ALwWfs0UlQZG7JeglSKKXQCSoUnAFRMz+AZkmTJH/vHr1AxGSfA9K0QW8ptktAZ6JHPIpuZwT", + "LjQppVizHPIxyWFOq0IrogX2DPTxzwrktsaugyvGJHBDC29G/1CCj8ajlVqUNLsdvW2j6f17M2RWVDl0", + "13VpPxCa58z8RAvCNKwUYbyxwCn5SQH5FbmT+tVA64Yk86ooGse25mDk4aIQM1oQpamGMbGwjwnobPpo", + "Sr6vCs3KAsiaFhUoklFOZkAysVrRiQIzjjZIexHhSIKuJGd8QQQvto15L18oQnlOCpH5KQ02YVMWwix9", + "TgsFaex69MToRTTEeLZrT+A3/EClpFvzt9Lbwu+a+btgK5Ygqu/pxhxowqvVDCQRc4Pu5kr76MGOGMO7", + "kyNUjOvPnrXZQP3rim664F3LimdmCyIAtaRc0cy0QChzpsqCbpGyV3Tzt/OxA1wRWhSkBJ6bzdIbrvqW", + "YuY+2UI4bBKIvl4CMV9ISRcQ4dlStfZftbgFHg4nmW3xUylhzUSlQqeedeDUiYVEx1CKiqfuCYIfHJp7", + "rgjb95T3w4844vvd3xRbuE9tqK/Y4npbApmzAg/7PyqlAwFXCrd9CUSVkJmrLydmGIN8xRac6krC8xv+", + "2PxFJuRKU55TmZtfVvYnZA9XbGF+KuxPL8WCZVds0bMDAdYUm1TYbWX/Z8ZLc0q9SV7lL4W4rcp4QVl8", + "FgytXL7ooww7Zj9ppO+niyC24f64sa43ly/6brTdPfQmbGQPkL24K6lpeAtbCQZams3xf5s5khady99G", + "VrozvXU5T6HWkL+7TJCtXljx9aLm4D+6z+ZrJrgGK4lEPP4M77rn72LBVYoSpGZ2UFqWE+T/E+T/5qd/", + "lzAfPR/921ktZ5/Z7uosmvyl6XWFnYwsJMEwvgktywPGeG1viP6DbviQPepzIcndkmVLopfM3LZ2E1Hs", + "NZymgDXlejo66CS/j7nDGwdEvRVWRrFb0WJAvXtBbMMZKKR99+Z4oBo3b3Tj4g0c3/rk4UVZ1sjF7xdl", + "aVE1JmxOgKE4BRumtHqEmKH1IWve8FPyTTz2HSsKKwjMwN07kJsxLd92fNy9fwxicQ31iA8UwZ0Wcmp2", + "rYsGdVlvzGnIMzxYJChRycx+CMLGTkpL7RKOkZJBzHU0wRutS4c/KbAkWNIF4zjU2Mi1nKzorWHclAvc", + "FENOoILAaonVXpN3TC/rqzMIfVNy3bxOHdbxl+ZmGvmhUkCobVHDQrJKKiGno4So9ac/WSmSIoaeKDNC", + "EymY0uaWjHEVaMWejvD2b1CtecydgkbxKboUhRHc9pKkafytaxvzTfP7oM5/ep4Zo72fW6IawCEVeaD9", + "JX4rtVhhlxNiD8MDL9p9j+ODZpQeDmg+nZr3xXR1ONNrEdofhdv9D2JRfXueZE7YmCyhwGdSmiMdRTQD", + "aGHHIgLMd5KWlszdF/v6YJzQWqeBsN5T/hwoGiZhjnWdNd4RqqOZ+V6Gm4TEaimbMHxRiOz2W6qWJzj8", + "Mz9W91jgNGQJNAdJllQtE2eqRdv1aEPo2zREmiWzaKppWOJLsVAnWGIhDuFqZfklLQozdZebtVaLAw86", + "yEVBTGMCK6a1uQCsDm/B1sAt65mSr2i2NLIFyWhRjGtlpignBayhIEISxjnIMdFLquvDjyP75z2eI6+i", + "I9FqnCIUpUAJcyFRvSKBrCheTiuv84v7BOaq6AraQqK5LEWlDYzRe/vyhV8drIEjTwpDI/hhjaimigef", + "mrndJ5yZC7s4KgG1s04bmHd1oDHQpnV91fJ6CiFz1A5TbX5jkmRC2iHs5e8mN/8AKuvOljoflhImbghJ", + "1yAVLczqWot6FMj3VKdzz8nMqabRyXRUmNZDWM6B/VAoBJnQyb0qnb7ZfDYCjqGkmnoYyiko04T9wDvb", + "oMrOZBoYvqUFWVllOylpdnsQlF/Wk6fZzKCT95XV77stdIsIO3S9Ybk61TbhYH171TwhVlPp2dEepXVq", + "7XauIQi4FiWx7KMFguUUOJpFiNic/Fr7QmxSMH0hNp0rTWzgJDthxhnM7L8QmxcOMiH3Yx7HHoJ0s0BO", + "V6DwdmvYTs0stX3rYibkcdJEx55ZW+0INaNGwtS4hSRsWpUTdzYTNjXboDUQCUrR3UJAe/gUxhpYuNL0", + "A2BBmVFPgYXmQKfGgliVrIATkP4yKcTNqIJPnpKrby8+ffL0l6effmZIspRiIemKzLYaFHnotNMEjWOP", + "kg8nlC7So3/2zFtRm+OmxrHKkhUtu0NZ66x9GNtmxLTrYq2JZlx1AHAQRwRztVm0kx9tv/fj0QuYVYsr", + "0No8gl9LMT85N+zMkIIOG70upREsVNOS7aSls9w0OYONlvSsxJbAc2uvN+tgyrwBV7OTEFXfxuf1LDlx", + "GEUz7e5Dceg21dNs462SW1mdQvMBUgqZvIJLKbTIRDExch4TCd3Fa9eCuBZ+u8r27xZackcVMXOj2bbi", + "eY+KQm/48PvLDn294TVudt5gdr2J1bl5h+xLE/n1K6QEOdEbTpA6G5qTuRQrQkmOHVHW+Aa0lb/YCq40", + "XZWv5vPT6EgFDpRQ8bAVKDMTsS2M9KMgEzxXe7U53obdQqabagjO2tjyFljdD5VD09WWZ6hGOsVZ7td+", + "OQM1UVueRaowA2MB+aJBqx9U5dWHKQvFA5WA1GDqJX5GO9YLKDT9WsjrWtz9RoqqPDk7b885dDnULcZZ", + "ynLT12uUGV8U0JDUFwb2aWqNv8uCvgxKB7sGhB6J9SVbLHX0vnwtxQe4Q5OzpADFD1a5VJg+XRXTDyI3", + "zEdX6gSiZz1YzREN3cZ8kM5EpQklXOSAm1+ptFDa4+pnDmpWSQlcx3Iu6jOYIjMw1JXRyqy2KokWqful", + "7jihmT2hE0SN6nHOCQ5GtpWdbknXQGghgeZbMgPgRMzMomvfHFwkVaQ0srMT65xIPJTfNoAtpchAKcgn", + "Tp+9F17fzt4/egfycDW4ijALUYLMqfwwK7hd7wX+FrYT53z38Luf1aM/yiK00LTYswXYJrURbfVddyn3", + "gGkXEbchiknZagvtSTAitmE6BWjoQ/b9sde7/W0wO0TwgRC4Bol+YB/0aPlJPgBRBvg/8MH6IEuoyokR", + "A3vVD0ZyNfvNKRdeNtwzQ5igoEpP9l0pplFDb2KWGnHx1C2CA/fIky+p0igGEsZz1N/aqxDnsbKlmWJ0", + "oCskTtn7GjOT/uwfYt1pM3O9c1Wp8CpTVVkKqSFPLQ9t1r1z/QCbMJeYR2OHp591n9k3ch8Co/EdHp0i", + "AP+gOlionc27uzj0OjDiy/ZQLDfgq3G0C8Yr3ypCfOyJ3wMjU/UeWHJjqkVvMyEKoNz6bYuyNBxKTyoe", + "+vVh8Mq2vtA/1W27JGnNQFZSyQUoNDG59g7yO4t0676+pIo4OLx/Aiq8rGNnF2ZzrCeK8Qwmu84LPoJN", + "q/jgHHXcq3IhaQ6THAq6TXhb2M/Efj6QMPzYSCC1/kBomMzQmpimkfpMeC/p42YVOJVKCd4Ev5DMnHPz", + "jKpJzfU+ftIccNoU33TE+iDMgmAk6cCPh8iy9JQYEe/+tdCGrBzR4WrcrXTPtfRgL8z6QRCI405qRUB7", + "9v8C5eYOAthJ59+C6lt4PfWplt2j/se7vXFhtq6y1m2TvCJ6+fIextjHg3psEa+p1CxjJT5Xv4PtyV/v", + "7QmSvhIkB01ZATmJPtiXfBn3J9Z5vj3mca/5QerWLvgdfWtiOd4zqwn8LWxRbfLaxuFE2qpTqCMSo5oL", + "l3KCgPpYD/PiiZvAhma62Dq/3y25AwlEVTPrtdI1oWlRTuIB0oGW/TM6g3zSHL7TQ+AKh4qWl/I8tK+t", + "3fBdt55cDXS4V1YpRJHQf7ZPfAcZSQgGuQuRUphdZ7QotkSHYC9PSQ0g3QWB3hhBnnmgGmjGFZD/EhVG", + "8hnKrjQEIU1IlHxQWDYzGHEzzOlcVWsMQQErsK95/PL4cXvhjx+7PWeKzOHOutxwbNhGx+PHqIp7LZRu", + "HK4TaLvNcbtMXDpoq8QwReeE2+Ip+53c3MhDdvJ1a/Bg4DRnSilHuGb592YArZO5GbL2mEaGOfjhuIPM", + "d02XsM66cd+v2KoqqD6FoRLWtJiINUjJctjLyd3ETPCv1rR4Fbq9H49gA5mh0QwmGYYWDxwLrk0fG408", + "wsBeZg6wDXcaChBc2l5XttOel3btt8xWK8gZ1VBsSSkhAxvbaaRUFZY6JTbQJ1tSvsAXkBTVwrk623GQ", + "4VfKasJkxTtDHCqK6Q2foAlDJYMr0WzpQ7SNEAbUvGzb9g/7WLujARR7GQ26tKPtaduDkibT8aj34W/w", + "va4f/hZvzTjzY42JDfkwQloNzUDrGeLTyEpdJMbbaA6fIYYPY6Wph05B2Z04cgqvP/b5hV9VZVlsTyAk", + "2YGIhFKCwistVgMq+1XMyfcsk+KiWIhw56mt0rDqGm9s1196juuPx7yABS8Yh8lKcNj2iS9K01uoXf69", + "chZdaK2dibzCYQjVpAHk2KmULbcQZifwv11Z1apBYVPiM21WOb2oERy2oK1+dEXlLeREzOdmsulwRahb", + "JK4j4Y9qYberROZkVrmQgIKI4VTmJRe/lv3iYOMXVyNlyPoOAN3KNLu253v8dhxbaFJUixpaeGvCMoRd", + "3PcA4HFs89W2FVl9LeSpPBjsgIPfawO8Ava6zLgpj/VdoEWRMPdb1U6HQ6txcLhnklClRMZQCL/M1dh5", + "9lsPARsy0EL/6xB2dgLm2B63ZdeOQtyskQSKklCSFQxNKIIrLatM33CKWtRoqQlHTK946Ve5f+mbpHX8", + "CRW8G+qGU3TCDbrVpNPVHBJs52sAr3lX1WIBSrcer3OAG+5aMU4qzjTOtTLHZWLPSwkSvSGntuWKbsnc", + "0IQW5DeQAhlp4zm3qpQmSrOicEZ2Mw0R8xtONSmAKk2+Z/x6g8N5Hx1/ZDnoOyFvAxYO4GML4KCYmqS9", + "SL+xXzFgx+Fk6YJ3MI7Ffvbe5HWynpFZeyOL0P95+B/P31xM/ptOfjuffP7/nb199+z9o8edH5++/9vf", + "/m/zp0/e/+3Rf/x7avs87Kn0EA7yyxdO/3H5Ah+5UQxOG/Y/grFrxfgkSZSxs1aLFslDTGDkCO5RU6eq", + "l3DD9YbjbUkLllN9QvJp31qdA22PWIvKGhvXUpF6BBz41LwHqyIJTtXirx9EVm5PsNOZKd7yVvyG44zq", + "5AC6gVNwtedMuSw/+Oara3LmCEE9QGJxQ0fJRhKvQxcd2vCgMrsUB83d8Bv+Aub41hb8+Q3PqaZn9jSd", + "VQrkF7SgPIPpQpDnPuD0BdX0hneuod6MflHAeJTSL8Up6Cq9lpubN7RYiJubtx0fj65s5aaKuag7Z10V", + "pJ9yYuQGUemJS+s0kXBHZcrO5JP+uEhz7L0TDiuTiMoqCH3aKDf+dCiUZana6V+6KCrLwqAoIlXlMpiY", + "bSVKixCUZ5i5i2s2NPCDcA47kt55dUKlQJFfV7R8w7h+SyY31fn5JxjeWCc9+dXxQEO32xIGKxV609O0", + "dQm4cCuXo8P+pKSLlD3q5uaNBloihaDAscJXfFEQ7NZM4+aiLHCoegEhzvuALbGQHRwzjcu9sr18nsX0", + "ovATbmozLv1eOxhlHDh6A/dkLaCVXk4MR0iuSplj4PfKJ2+gC3PleO8MxRb4AFBLUZklA8mWkN26VIOw", + "KvV23OjunYjcXewZDlP45HWBl3Nm8OfS8VVlTp0gQ/m2nfRK2UATHPRHuIXttbDdpwPTNUbpQaOkS6rv", + "6CLtRndtM1GKS2YBnc2PFBC0LH2CIoxp9WTxPNCF79N/tF+7NIj3PtYpomjkUOlDBJUJRFji70HBEQs1", + "492L9FPLYzwDrtkaJlCwBZsVCTb9967NyMNqqFJCBmztI6bDgIqwOTGvo5m9jt2LSVK+QJ2UuYiFogUG", + "REyTThQoHS6BSj0DqnfqwnmcwsNDhwL5HQako9JkbJYAG7PfTKMShMOdeeDh21sElZOu1PQoVzW7JsiP", + "BNV3rwPQp8c8IhzCExku/X0f9iS8F5zvX0ydCLL9jjq3hRR3ZjcNgMLn0sXkOdE9VSm6gKHXUUMjNzDd", + "SMO6hoPsk36S8o6Yt8WajowxcBG2+8TgJckdwHwx7AG1mC33UT+3Nc86i82rKJfqrECBOlJlGtKhsqHO", + "5IvDgE2zMZC8FlY9YE2sxUd/SZU/+g2V8pHS4u+TpmdXRs3LyLOR6m6+TH9Nt1n72OpzZkAENz18Xk2f", + "TNNn0ByND8qGOR658JHU3gmOUnQOBSwsTpwNwNFZnfuq3k0DxyurtieTlJNkpIyMJBM3B5iH2GPijQyD", + "R0idgghs9FrAgckPIj7sfHEIkNzl7qJ+bLy7or8hHYhpIx2MlCxKc+uzHotg5lkKbaZbVm33cRyGMD4m", + "hpOuaeGsF7oxSCd7I759Wrkand/Mo7430cCD5taI0slBq7TyzDHriwVvv4z0q+CgNczEZmKjzpNPq9lm", + "Zs5EMhYEY+BTh9fm0nygyExsrI3I3HA2eOBg6Poh84BFLjYbppDKsV+f2GjBOwyQ3YJ8ipoVkp7TqwWy", + "65NkjwOmR5zuI7uHUXrCE4HUUmDWdRmcRmevnqUpbXUlkfq6HYd80SEEMMVq+g5ncid7MNpVno5HO1KV", + "9qngEm0HJZ/1uTHJw3YaWpuw3fuWRbwaWYXNergv32xXf3fqDMO9Sn+r8A85jzz8nv9pYd4Wnh5biW0P", + "FkrMJZUMwfh7FGMR48YIY8iPXc/0e4r1CFhdLB9alMNl4z88OXPrELJ8JwHHCpxeyo0vm4+SAbRLlfdJ", + "2mo7D0RonLO1zc8aQOzA6uv2KyyJ1qZXYxOvEdZSd6qRVLrW2i7aFBSAqqxJ42E4uU25WdzcvFGAQu+V", + "7xYp6nH3KN8+ilxlJSyY0lBbx7wH3Mc3XiKzmpRSiHn/6nQp52Z9PwoRJGXLTrFjY5kffQUY1zJnUukJ", + "mhaTSzCNvlaoCv7aNE2/5Jp+OUxZW+XBPBMhuoXtJGdFlSZlB9J3LwxEPwTRS1UzlPQYt66IMyyuk/Te", + "P8C4jvDYqI+dCHppEfSSfgz8DDtYpqmBSRrKa07/JzliLV64i7MkaDlFTN0N7UXpDl4bJdroMtroFo78", + "hqa7jJadc5n7sfe6avp0H31SsB0puZYoXWo6ulgsFpD7NJAuYtymxHPJNgvBF3WiUfP7jtyiU2JTfGKG", + "zh3JPV3sCvRFrjQKlKHkslcaQsjr0FtMTIqTLIDbtE5HCEtFEnFx1Ay2iFT7H5e3d2JqknEF161Ygtrh", + "3+5h2GzcngJo7vQCCvz6dh/a7nY51I37IhIa+aN3HzAcECmOaRUJMB2i6eHctCxZvmlZru2of0T5ue7Y", + "DDbYU/TvgbkUsb0z0p3hY+tsJjaWRTl9EB4JmrkMJHkl0QraiCDovtuCgmTgkr/7+UoLSRfgLNkTC9K9", + "hsDlHIKG6FWriGY2aCJn8znEFlx1jPWxAVzHTpcPoOceyuuaeYNOZCdZHkxb9Qr2IzRNTwlK6fMVuu7a", + "0f17I9IJhzumVYfuQGN4MsnId7Cd/EyLyjyAmFS1T7UzbDdv8wNoYr36DrY48l5XZQPYnl1BTcePgBSa", + "0viETyqqHPBANeoI+UInTZ3GwJ26SO/SibbGFYXqPxr1xdRQSe1Vz5zo2NSuXQbSIXt1lfaWMmcLmtvS", + "JvR9WzREARS9POKpGHodHXO3hew7e70igRae8HGxo/fj0f38lLosLIy4Zydehxs5uQvoRWz9VhrOigdu", + "CC1LKda0mDj/rj5ZQ4q1kzWwuXcH+8jPqvSpuP7q4uVrB/778SgrgMpJ0HD0rgrblX+aVVkF9e5ryJZo", + "CDpg1lCN12n0Yw+wOyzH0FKidaq21f5+0UF1HmHzdITDXr7pXBPtEne4KEIZPBRrTwrroNh0SqRrygrv", + "sOChHWodsssdpsVP8ol4gHs7N0YmhXuPpdhvMEHXaNHjWKgCft3N6FypmcElBshabFsst2nj+y9+PHzz", + "e4Nubm7erD04tXHSeh2G2h0Jx1R1ZNhAhwGmGUh9APewbUT+K0y5nH4DcpeQGbm1896kJxdOvxaycXu6", + "8Ouk9+eHk1rNC8fiMe3hcu1cWjqy6pRYufbXxa+GYT1+HFPc48dj8mvhPkQA4u8z9zs+7h4/TnpZJNWO", + "ho+iVpHTFTwKQUa9G/FxVSIc7obJMBfrVRDcRT8ZBgq1bpwe3XcOe3eSOXzm7hdr10sitHui4k236I6B", + "GXKCrvrCp0MkwcpWy1ZE8HayEAznN6SF96ErNWSdVrpHiFcrdOKYqIJlaQ86PkMOya1/vGlMsPFghwwz", + "R8V6gjR4xaLRTTN1lP9AayHRrEmEq2TK8hq/M+FYQMXZPysgLDcPyzkDiVdAS2Lw7zMctSP1p3WdbmBr", + "xqyHHyrhm26H6q92mCstkL2o6rX6vgiWSL/+VB28A2OG4hk7PH9HvI8jJH9rYnDo0rnf7yWonW/OYBhO", + "KoKcJdpzTWf07X+suSLTdg9fDNlgpiZzKX6DtMiAdspEaiFvYGdoA/gNeMovoc2/gveNX288+z4CGa7n", + "6COVe+s1/KJDVc9jbu40ezhsow9UYET73a/CUOnyB24T+h7NsfNWMxith4fhgY1CK9DvxruMUm5PqM27", + "04jeTJ/zONj6zI5fn3MHcydAvaB3M5oqxGbergamaPsbzq1aEN/Zb5AKqWPs7CSKBwptmU1GWoKsDVjd", + "VO5HvkPttINfoPWDEykufmqOrbtMoURimIrfUY6+uNjPckDXW4F1BTG97oTEBMQq7YebQ8ZWScX8zc2b", + "POt6T+ZsYWbyJazn2vlIuYGIzXKMVJQzVRZ0G3IlOdRczsn5uD6zfjdytmb4EMMWT2yLGVV4Lwe3jNDF", + "LA+4Xips/nRA82XFcwm5XiqLWCVI0BWgxBm8yWeg7wA4Ocd2Tz4nD9HpXrE1PEpfME5GGz1/8jn6Kto/", + "zlMiUg5zWhV6F5PPkct7D7Q0ZWNkgh3DsFU3atobbS4BfoP++2TH+bJdh5wubOmuoP2na0U5NQhJwbTa", + "A5Pti/uL3iQtvHBrKQKlpdgSptPzg6aGY/VkZDAM0YJBMrFaMe1z4SixMhTmWas/fn44WwXelWn0cPmP", + "GMZQJp72v8Mri656ooQxMuUHNPnHaB0TajNKF6yOYfIVtMmlz5yPdStr103EjZnLLB3FVAxpmpNSMq5R", + "g1Xp+eSv5tUuaWYY4rQP3Mnss2eJ+o/NEmn8MMA/Ot4lKJDrNOplD9l7Kcf1JQ+54JOV4Sj5ozotSnQq", + "e+Mt0j7yfa77PUPfW7o24056CbBqECCNuPm9SJHvGPCexBnWcxCFHryyj06rlUwTDK3MDv3040sniayE", + "TFXiqRmAk0okaMlgjTHa6U0yY95zL2QxaBfuA/3v62DnxdJIdPOnO/lYiCzciXdaSE1mJP2fv6/rd6Ch", + "3ca+t5SWQibUs07R+JE9Yw9TE7bt+dYjEb/1YG4w2nCULlZ6QqZsTFTo83u4nLVBsnve0JA++ZVI845H", + "Wf/xYwT68eOxE5V/fdr8bNn748fDvXbTakLzawI1x9017ey6pm9qq78QCaWdrzIcXNdcup+EYjV5l5kr", + "debGGJNmKdePL3ecJub3YE/o9AHyqMHPbdz8zvwVN7OOIuvnD83q1knyycP3KIyDki/EZigRta4tT09/", + "ABT1oGSgVhBX0qnenfTa2OtyFJGtGXUGhTAv1bhA32APmj/RLhjUjHfsRcWK/Ofa+Ny6mSTl2TLp1z4z", + "HX+xz4CoQaTByJaUcyiSve1r+Rf/qk68+/8heoZdMZ7+1C4Ub2FvQVqD1QTCT+nHN7hiujATxChqJrUL", + "aYKKhcgJzlNXVqpZ43SUQHy3DnU3TwYOu6q0c4zGBCSu4NGcFejSmzaDY8uJpLqHq0oMX5/XI8LayClW", + "LWFHB0koW+G1reiqLAAP4RokXWBXwaHVHbMe4shR2SSiSvMJW2ICJUF0JTkR83m0DOCaSSi2Y1JSpewg", + "52ZZsMG5R8+fnJ+fD7MtIr4GrN3i1S/8Vb24J2fYxH5xlQltQZeDwD8G+vc11R2y+V3icuWh/1mB0ikW", + "ix9sUgM0DJt73ZaGDmXMp+QbzPFnCL1RwgSVoj4DfDOvblUWguZjTFp//dXFS2JntX0kIOqwNPUCNYDN", + "I5I08gzPM+xzGPbkfxs+zu70U2bVSk9C0ehUNlLToq51zVqeWKgbjLEzJS+sWjb489hJCJY+kCvIoxrV", + "Vg2AxGH+oTXNlqjvnI52qpR7qpUNL7HuOWBtLopCb0NBP+TgZhmuyrotsj4mQi9B3jHMbU41rKGZ9DRk", + "DHYKeZ8EtblaWXFuCWd6gPQayvcdugseOCv6ereKJGStfbi37a/OhoPB+4cWo7+yuQSSoUOtyvYtdwdb", + "0mfjiwJNyffO2JFRLjjLsBhOSgTHdKbDzKoD6gal7Z1q5M5y4hgm6+mHJA8Oi70V9j3LvOpJwhB/Nftt", + "Ccf+qWHjipQuQCvHAyEfo4KKFeAMdIwrkCE3QSPdtJAJj69kiE7wHDmhe/x4hBkJe3StX5tvPzjdPOZd", + "umUcdW4Oqe4laA1shWJoZ+eEabIQoNxqm6Fp6o3pM73ecATh7fSlWLDsii1wDOuBiNkb0CO5O9SF9092", + "/sCm7ZemrautEn5ueNLZSf263yZZSJ2Eo6sR2fBe9KdcvnyEXITcMH482g5i3Bl2gPeyIUNYo8MflHif", + "d8gGpEw9PL8yT1ZLb9iC2ODhZOptxhNgvGTcG3zTueSy5F2CG4OnuaefyiTV9tExiONdAy16QnMwrt96", + "DNx3qHalGIMSXKOfo38brzfclbnpYSuhQf26oHxL/KEw1B0JJV/SIjjmW2GqqZc20pkTxqyPsA32deJd", + "mq0Ytj7x0cENdO2NRQ3dsVrTofdUX8beWZUvQE9onqeSrnyBXwl+9cGNsIGsCkUKQ6hrs+RBl9rcRJng", + "qlrtmMs3uOd0OVNUKVjNioTH7YvwEfKww5jMbbYlvpjL8J1xDvgHB6B7b/v8sDof3YD6lPRsaHqi2GIy", + "HBN4p9wfHfXUxxF63f+klO5jz/8QoeUtLhfvUYq/fWUujjjVfce1314tIRM9utEL/O5z6oVsyE2uhFdZ", + "pw4lemTg5iW2rAW8b5gEfE2LnqQPsdXG3q/WktGX+iHrzWxCtcsAqSmpecIQFUZ/Dj3reN2yDHXNm32u", + "1daz+kMaTxw+diK939L4XcOuaL3eaobSa088zuRXE8GhNj9XzqSrL6VFIbLBnMENc2E69ae7FquVqx6R", + "8Mpbr0Qen4XYmwsgzdisw3IiogIftslv+LRKfpF36dEa+pFANEMz/yEa3RLGNkjUg+eBsVPHE0UqW4dZ", + "8jUrsHjdf169+mHUv5HRDnS31KWfT6qw+zYmRM21yWMhGvjYwQMEL9L6b9WjUsf0VOnT4KqnJz98bRWE", + "Q0CyqZoOaf1y6OAdAlgIW1ktVXummyBnVG+HR35EDfX2Wo4SU0eKKtoVyxJvH6v0rJuQUCh5UOHkhow0", + "pEBaqhaXeyl4Day9aFxKPFugrFPbrMNAXwwRDjv4eD8eXeYHiU+pem4jO0qKwb5ki6X+ohDZ7bdAc5C2", + "Jk/qOWkr8qzAPEPVkpU2s6VQrK5XXpjBXDL8JQ43HRqRc70El5jGJyzojOUdqNeQaaxfX7uBSoDhfg5l", + "eokGAm9QxCa/gyuIBMih1MudwpJ17i71si5rDC7gjCkyA2e6WAMfEzaFaTtGLa/zUpEC6NwrYaUQekDd", + "b69tsWiMgU7RV6eG/G4xsJN2LsqqaEt9T4cXMroIMQE2vvKOqjp5VSulw+DQ8fkcMiwasTMD4N+XwKOU", + "cGOvukNY5lFCQBaiBLHsyUk12jWsu3Lx7QQ1quv2ISHtS85xC9sHijRoKFmxPATWHlNFAZFj7bi+MMee", + "HLhMBXpCBHk/eFfEoq5TdkwhjShB5pFgeBo311OdNPM4aLxEcwQYpuuBk/Zm5EPBtC/B4GubfDq6yvtf", + "yi9AU1Yo51RKQ8mGWJ9ELrvl4u9cyQfM9Rishb74Ayj/m88Ra2cp2K2r8oQIs7bZOypz3+IkmfrsvcnS", + "QM/DzKwOjOp6+Rzql2MjFLNCGAFo0hcY2oxUCi68D5T1ta4TqCHUc5AS8mATLISCiRY+zOqA/KMufHIH", + "9qyX+VF4a3n0HxApbFfUW4fkx7oYC5ZUpVh3hDrn8xgrRMKKGuhlVCAlrQbdt0Nf2u8+v4kvkblbvdqH", + "93Au9lfw96F35p5pYT4+XXPihIODuVcjKcoRmlnGOciJN+K2y6PwZqZOTO2cV5mr1R2dzaC9HpwCbQc3", + "Syo1s+4qW0+oKBnHLWzPrNrHpeUIOx4DbWVIC3qU07pFFCfVVasU3IuTgPf7ZhAthSgmPZbBy25Nl/Zh", + "uGUZ1piv6sgUIwU/aB4bMwl5iAap4DNyt9z6iiVlCRzyR1NCLriNDvTuI80qvq3J+QO9a/4NzppXtkqT", + "00BPb3g6zAqrJcl7cj8/zA6e18ebFBh+ec/57SBHzK43vM9H7g7LKjVrbU+Hqje6/h0tESoiPwtFSoC6", + "sobgL5ElJN5RBJOyRNmD0D+AEmdAJqoQKS/8YxLHmKHSmIonQ4A08AHP1RoKN3gSAc7Jbk+GWPfZ50AV", + "81Dz4z7JYF1+VcvEVZ9qpD1zmKXJGedCQjwj+pnaXNEhsg1TLeM/ZkxLKrfHpGxtoiqlhurF8l5vyeAo", + "WS+kdpbs4rAoxN0E2dokVChLqQNMO9W8tn2t37qfOeoziNwuqS/csiVLmpNMSAlZ3CMd4m2hWgkJk0Kg", + "F2bKsWOuzSNhhXGdnBRiQUSZiRxsMcE0BfXNVXFOUfaCyJUtiQJLO5gywPaJ6HjglOb2tebZCcpre2t9", + "+M2/Nn1s+oo6FZ9d9MS6CPTEF4ByyeAchmzjLrw2bRwmYmorZdMi8pxtkG5Apo78nGhZwZi4FlYgiUkI", + "Dz6VQFZMKQtKoKU7VhSYPYJtIoeG4A+URm2P7HyJftBrhg5vzUwiVqQuze0Y0q/EPOAqTsRG9FKKarGM", + "ShQEOP3TXVbuYR+P8pOq0CcRQ0TNFM/ISijtnsV2pHrJtQvow0xwLUVRNBV5Vs5fOKPv93RzkWX6pRC3", + "M5rdPsJHOBc6rDQf+5QKbd/deibZygc57KWgN3yC5KH2Z3q37dCr1dHzYN7Z4n4dw8M+TX4E5tv9zHW/", + "XeOiu7D2upp8Nv0WuuCEarFiWfq4/bm8X3t9VlPcK5lg0VbytllosBnygfgeC+5MyD27aAZOk6WIL4jj", + "Ec6tAzmR+SeK8e1xyRwcD+q5Q7t8xwlYk6xXDGwBgJDaRAi6krb8dyykBYYjFjZxCjqltAEdeOGg79/9", + "YDMjnBwoDfcCquONHAB8aDUYY5sI03o2z8TGf39UZ8o8Cvj3u6m8wTz6nCqvatKS1q3SJ7Lq4QjpYgg7", + "PRCvMQnGbKgfovJWwoGXfwRAv2diA4ZB/omHgjGnrMAafD33PurAxtFz3cVYRqP7mqiWk2e08tW0zdiV", + "BJdYyUr/smlOLKkhJRGadzXiPIcN2Bit30AKWwt7HJmzoLClslsaBVFOClhDw2HTZXuqUApla/B9VehM", + "coASLb5tRVvKEzGutNnSvri1TyJftiHYTapjLGLtTpE9upakZmjDJ/aYqKFHyUC0ZnlFG/hTh4ocTV2i", + "OcoJVHWeDxP/xBw6zU92BF80U134/ilRxmPi7TA+dDALSqNuFwPa65lcqb5Tz9OOyXEqs2AowtnyYNe2", + "JF7zDVXSO96v1eySfP0SG7hPTPAIsV9tIEOpxj2FIHePoR7LicuBhNTOAXL7YDBdEtr8JXDCRVQ3/I6q", + "8Iqpk7n6H+zE2Ihx99A+wkZf+w/ff2cJDkZUK9liusxvIOv76fh/l5O48yD2jpeiEQUulHeHasxTt3t2", + "YANRFTnhZj+N7I91tt0t5rj4mMwqP1BRiDtbCDx+or4Ab8+11OdNTE4sZ+Fa9n7SY5dnuK0FYVGEyIpu", + "iZD4P/Mg/WdFCzbfIp+x4IfCv2pJDQk5A7L1onB+12bi3eLV2APmFTHCT2XXzYaOGQ23NaNEQJuL3FeO", + "E2RFbyHeBnQQsfwz04ZxqmqGSg1zZbe2s4sFt3ifnmlF81gJgIlmtw3u4POcm97/fx22Gk/l8z+WBc18", + "2XdX/67JZ4wwFIhLL2G1O8y5y9c8CfhWEdFKnyYjP0KbeiDrSsX89BXqaoDdKaPfqVF2r2UcUlm6zjiy", + "I0B80FJOvQunieHsLCmuNrxvcXHx5Y+zO8kM0X3LGAL+H2hXGu4Vncg2X2Svfz3Y5GPsQiMRTwJWqwaf", + "ic1Ewlztc6SxevCZ2NQAq6C7ZTyTQJX1O7p85Z6tdQJkxs0z2nrtBrNqGCWHOeM1q2W8rHTiFYR5kPk2", + "QlhsTUC09tjm+mQMI4quafFqDVKyvG/jzOmx1YnjgkHeguL6JhQg4UbuDsBU/QLEeOpaPx83M9e/LXZo", + "fWeVpjynMo+bM04ykEZqIHd0q443VQWrwz5jFY1koWa2kMhshaRtASm2ztp8T0NSAJCe0KI0wBKETtoJ", + "K5BVDGnRY/jpwvCnsASt6GZSiAVG/fYcCJfnGk2H9gEpOCrRrXQ3bN1+HsV+g93TYAUSx4i0wFmHTLH7", + "3L/CrcRH6E+c6Z0n32o422HY1tPZHkyPVL6owzMssXTPYypy3iVmiqPnvajq05R42oNoE5Mu0R2tes8u", + "on+FS7sQq9CHF85sunCk4vOtXmGC+ga1IwADVB1XQDPnIdZVxHUUFRYpY5fd4EA9ndXu+3upBzxUpCh3", + "1pvTBgcdM84h1UZ35zOYlKKcZEN8W22RotwZGRykTRh76CMyIfSsO/jdqFC2q5ETrVG/69CCq731w/bZ", + "yspsl8qgT8nUw9GbBgwxR16GR9iq1jDWKqhixv5x7o3dTSVaYBKEEglZJVHJfEe3+4tQ9mSfv/r24tMn", + "T395+ulnxDQgOVuAqmsatIo41q6JjLe1Rh/XGbGzPJ3eBJ8txCLOWy992FvYFHfWLLdVdTLiTgnLQ7TT", + "iQsgFZzbrYx31F7hOHVYxB9ru1KLPPmOpVDw4fdMiqJI15QJclXC/JLarcgAY14gJUjFlDaMsGk/Zbp2", + "ylZLVC5i1vC1zQ0leAZe++yogOkeX67UQvp8epGfYS4GZ3MisCkLx6usnWjXutw7zer3UGhEd5sZkFKU", + "TrRnc5KCCGO2ZAVBr+7UpqhPj9x0A7O1DrspQnTO72nSu+DuJSzmZDe3b5YF12lObzYxIV74Q3kEafZZ", + "N/rzjBzDSWrDwB+GfyQSp5yMa4TlfghekXwf7IgKv+h4TYSkIYNA6ybISJAHAtATD90IWo2C7KLc5NLa", + "GNAa4c3PbfHj+9osvTcyBSHxHfaAF8cy1+1CMIUD53dO7P19QEq0lLd9lNBY/r7waM96w0USbZFTmmgN", + "yrIl0RULo4B49WWIM+95lXTC0aUQmpiXaVEkwtitHgfPVEw45kkg17T4+FzjayaVvkB8QP5jf+BWHLYc", + "I9miUp08IedLOgisKET5o0DFX2Ns/d/B7GzydnSzOMN/5w5ElRAtrLf3PFjAgZM7HNM6dj35jMxcuZ9S", + "QsZU26Hgzos0Id4WJJs7/1rY6Hbs773LBP0s9D2Ow9z7A5EfIiNb8BxwMNdH/XdmTj0cIHlaUqTaIZQE", + "/lK8Li7wvufauWdpmONSOUWJGw9M5dQtXT90ebgOvLwqBd11Dr71G7hNXPj12obmKhtcYebm5o2eDUko", + "lq4GY7pjjrOTlIW5f1GYj5LgzKLSjeEgSRJWLXLvy17T8peM8jQ0d9GI+z1145cW/WY0fBTMK27HCwVQ", + "MVbcs3UxHwcvBsFNt+fkhj8makn928L9+fTTz0bjEfBqZRZffx+NR+7r29RLLd8k40rrRDodH1FXTeCB", + "IiXdDglm35s6J4nfOlPQxxdplGaz9JvuW7Nn+HB1AQiXHFk9shd7g7r8Of9KALSTGFqHNZwYS5J1eqCw", + "FfsyBf3clxbfpn7vqfbR4r4VK/Y6yTUKsbwfjxY2SRlWJ/nF1ar7uNvuIejJF+iWfp80YBYxibU2Jo+m", + "ipK6DSjI4rolKmRg5HVWSaa3Vwb/Xu3OfrlNJYP6JqRncjm/ggXeyb5a3AL3PmZ1MqdKeen6G0ELlD6t", + "YwA3MqcopuQrWyHEXYt/ezD7C3zy12f5+SdP/jL76/mn5xk8+/Tz83P6+TP65PNPnsDTv3767ByezD/7", + "fPY0f/rs6ezZ02efffp59smzJ7Nnn33+lweG0g3IFlBf+ef56H9PLoqFmFy8vpxcG2BrnNCSfQdmb1DD", + "NscEhYjUDK9YWFFWjJ77n/6XvyinmVjVw/tfR64e5Gipdamen53d3d1N4y5nC8yBMtGiypZnfh7MZdl4", + "r7y+DHFB1vcPd7S2OeGmhvx+5tuPX11dk4vXl9OaYEbPR+fT8+kTzKdYAqclGz0ffYI/4elZ4r6fYRbt", + "M+WK8ZyF0NH34863srSlesynRUgDav5aAi2QRZo/VqAly/wnCTTfun+rO7pYgJxixJj9af30zL89zt65", + "vDLvd307i73RBjc8e9fI4pPvmcI7Xu1rcvbOJbbZM2Cj5rtziI06DAR0V7OzGRboG9oU4tX1LwXFEnX2", + "Dh/zvb+fuYs9/RH1LfZInnlppaelTTqS/thA4Tu9MQvZPZxpE42XUZ0tq/LsHf4DT1e0Ipvw+0xv+Bn6", + "p5y9ayDCfe4govl73T1ugXlqPXBiPlfoRrPr89k7+/9oItiUIJl5pGIuNPerTX95hvVwt92ft9x5UxSQ", + "yhn2E1dglXG+kNGWZ3XIbmA4l7lvfLXlmX9Ne4dtZCNPz8/t9M/wHyNXCLKVPuvMHfyRvfj36oQbKbaR", + "SbfMAQFeG5hsJGeE4cnHg+GSWydtw7Xt7fJ+PPr0Y2LhkhtBiBYEW9rpP/mImwByzTIg17AqhaSSFVvy", + "Ew9+5lE9/xQF3nJxxz3kRjSpVisqtyher8QaFHF1nSLiJBKMkGUfNSg11zSMdyM1fOTNqKxmBctGY5tQ", + "/S2KdTol4XgddXcmr5+vB2+eim/2nonhu9AUnHfk6xoE5/E5/uzMidzDna33ZNF2/rBQPEjt3ehfPOJf", + "POKEPEJXkvee3uhqw5SYULrQ/IxmS9jFKroXaXT3j0qRypVztYOPuAJqfWzkqslGaifn0fM33Rh2R82o", + "Ppj6R4+R6Os3iQwMyZ9r9OiI9nNwuby2uaX/29s/hFDwJeX+pDdowbpaUFkwkIE+KO9Wu/sXf/gfwx9s", + "FU9q93VMNBSFirmCFsgVrKbOJVXm1ltgIIdopMeuJfDGz2deK5J64TZbvmv82XyMqWWlc3EXzYL2RGtC", + "7z5NzMdKtf8+u6NMT+ZCuvzKdK5BdjtroMWZq+HX+rUujNP5gtV+oh/jAPnkr2fUvVFS35AL9nXsPKJT", + "X907saeRj8zwn2udXqwjQw4ctGNv3houp0CuPXOuVT7Pz84w0G8plD4bvR+/a6mD4o9vA2H5suajUrI1", + "1kl6a3iskGzBOC0mTmdS1ygdPZ2ej97/vwAAAP//pGd+BRwWAQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/algod/api/server/v2/generated/experimental/routes.go b/daemon/algod/api/server/v2/generated/experimental/routes.go index 41ab0d5c51..b197616e66 100644 --- a/daemon/algod/api/server/v2/generated/experimental/routes.go +++ b/daemon/algod/api/server/v2/generated/experimental/routes.go @@ -8,23 +8,17 @@ import ( "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/go-algorand/data/basics" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" - "github.com/oapi-codegen/runtime" ) // ServerInterface represents all server handlers. type ServerInterface interface { - // Get a list of assets held by an account, inclusive of asset params. - // (GET /v2/accounts/{address}/assets) - AccountAssetsInformation(ctx echo.Context, address basics.Address, params AccountAssetsInformationParams) error // Returns OK if experimental API is enabled. // (GET /v2/experimental) ExperimentalCheck(ctx echo.Context) error @@ -38,40 +32,6 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } -// AccountAssetsInformation converts echo context to params. -func (w *ServerInterfaceWrapper) AccountAssetsInformation(ctx echo.Context) error { - var err error - // ------------- Path parameter "address" ------------- - var address basics.Address - - err = runtime.BindStyledParameterWithOptions("simple", "address", ctx.Param("address"), &address, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter address: %s", err)) - } - - ctx.Set(Api_keyScopes, []string{}) - - // Parameter object where we will unmarshal all parameters from the context - var params AccountAssetsInformationParams - // ------------- Optional query parameter "limit" ------------- - - err = runtime.BindQueryParameter("form", true, false, "limit", ctx.QueryParams(), ¶ms.Limit) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) - } - - // ------------- Optional query parameter "next" ------------- - - err = runtime.BindQueryParameter("form", true, false, "next", ctx.QueryParams(), ¶ms.Next) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter next: %s", err)) - } - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.AccountAssetsInformation(ctx, address, params) - return err -} - // ExperimentalCheck converts echo context to params. func (w *ServerInterfaceWrapper) ExperimentalCheck(ctx echo.Context) error { var err error @@ -122,7 +82,6 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL Handler: si, } - router.GET(baseURL+"/v2/accounts/:address/assets", wrapper.AccountAssetsInformation, m...) router.GET(baseURL+"/v2/experimental", wrapper.ExperimentalCheck, m...) router.POST(baseURL+"/v2/transactions/async", wrapper.RawTransactionAsync, m...) @@ -131,231 +90,233 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "EAEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/algod/api/server/v2/generated/model/types.go b/daemon/algod/api/server/v2/generated/model/types.go index 53bb1417ae..65bc0b1bc0 100644 --- a/daemon/algod/api/server/v2/generated/model/types.go +++ b/daemon/algod/api/server/v2/generated/model/types.go @@ -53,8 +53,10 @@ const ( // Defines values for AccountInformationParamsExclude. const ( - AccountInformationParamsExcludeAll AccountInformationParamsExclude = "all" - AccountInformationParamsExcludeNone AccountInformationParamsExclude = "none" + AccountInformationParamsExcludeAll AccountInformationParamsExclude = "all" + AccountInformationParamsExcludeCreatedAppsParams AccountInformationParamsExclude = "created-apps-params" + AccountInformationParamsExcludeCreatedAssetsParams AccountInformationParamsExclude = "created-assets-params" + AccountInformationParamsExcludeNone AccountInformationParamsExclude = "none" ) // Defines values for AccountInformationParamsFormat. @@ -63,6 +65,11 @@ const ( AccountInformationParamsFormatMsgpack AccountInformationParamsFormat = "msgpack" ) +// Defines values for AccountApplicationsInformationParamsInclude. +const ( + AccountApplicationsInformationParamsIncludeParams AccountApplicationsInformationParamsInclude = "params" +) + // Defines values for AccountApplicationInformationParamsFormat. const ( AccountApplicationInformationParamsFormatJson AccountApplicationInformationParamsFormat = "json" @@ -244,6 +251,24 @@ type Account struct { // * lsig type AccountSigType string +// AccountApplicationResource AccountApplicationResource describes the account's application resource (local state and params if the account is the creator) for a specific application ID. +type AccountApplicationResource struct { + // AppLocalState Stores local state associated with an application. + AppLocalState *ApplicationLocalState `json:"app-local-state,omitempty"` + + // CreatedAtRound Round when the account opted into or created the application. + CreatedAtRound *basics.Round `json:"created-at-round,omitempty"` + + // Deleted Whether the application has been deleted. + Deleted *bool `json:"deleted,omitempty"` + + // Id The application ID. + Id basics.AppIndex `json:"id"` + + // Params Stores the global information associated with an application. + Params *ApplicationParams `json:"params,omitempty"` +} + // AccountAssetHolding AccountAssetHolding describes the account's asset holding and asset parameters (if either exist) for a specific asset ID. type AccountAssetHolding struct { // AssetHolding Describes an asset held by an account. @@ -308,7 +333,7 @@ type Application struct { Id basics.AppIndex `json:"id"` // Params Stores the global information associated with an application. - Params ApplicationParams `json:"params"` + Params *ApplicationParams `json:"params,omitempty"` } // ApplicationInitialStates An application's initial global/local/box states that were accessed during simulation. @@ -424,7 +449,7 @@ type Asset struct { // // Definition: // data/transactions/asset.go : AssetParams - Params AssetParams `json:"params"` + Params *AssetParams `json:"params,omitempty"` } // AssetHolding Describes an asset held by an account. @@ -1087,6 +1112,9 @@ type Catchpoint = string // Format defines model for format. type Format string +// Include defines model for include. +type Include = []string + // Limit defines model for limit. type Limit = uint64 @@ -1120,6 +1148,17 @@ type AccountApplicationResponse struct { Round basics.Round `json:"round"` } +// AccountApplicationsInformationResponse defines model for AccountApplicationsInformationResponse. +type AccountApplicationsInformationResponse struct { + ApplicationResources *[]AccountApplicationResource `json:"application-resources,omitempty"` + + // NextToken Used for pagination, when making another request provide this token with the next parameter. The next token is the next application ID to use as the pagination cursor. + NextToken *string `json:"next-token,omitempty"` + + // Round The round for which this information is relevant. + Round basics.Round `json:"round"` +} + // AccountAssetResponse defines model for AccountAssetResponse. type AccountAssetResponse struct { // AssetHolding Describes an asset held by an account. @@ -1397,9 +1436,12 @@ type SupplyResponse struct { // CurrentRound Round CurrentRound basics.Round `json:"current_round"` - // OnlineMoney OnlineMoney + // OnlineMoney Total stake held by accounts with status Online at current_round, including those whose participation keys have expired but have not yet been marked offline. OnlineMoney uint64 `json:"online-money"` + // OnlineStake Online stake used by agreement to vote for current_round, excluding accounts whose participation keys have expired. + OnlineStake uint64 `json:"online-stake"` + // TotalMoney TotalMoney TotalMoney uint64 `json:"total-money"` } @@ -1444,8 +1486,8 @@ type VersionsResponse = Version // AccountInformationParams defines parameters for AccountInformation. type AccountInformationParams struct { - // Exclude When set to `all` will exclude asset holdings, application local state, created asset parameters, any created application parameters. Defaults to `none`. - Exclude *AccountInformationParamsExclude `form:"exclude,omitempty" json:"exclude,omitempty"` + // Exclude Exclude additional items from the account. Use `all` to exclude asset holdings, application local state, created asset parameters, and created application parameters. Use `created-apps-params` to exclude only the parameters of created applications (returns only application IDs). Use `created-assets-params` to exclude only the parameters of created assets (returns only asset IDs). Multiple values can be comma-separated (e.g., `created-apps-params,created-assets-params`). Note: `all` and `none` cannot be combined with other values. Defaults to `none`. + Exclude *[]AccountInformationParamsExclude `form:"exclude,omitempty" json:"exclude,omitempty"` // Format Configures whether the response object is JSON or MessagePack encoded. If not provided, defaults to JSON. Format *AccountInformationParamsFormat `form:"format,omitempty" json:"format,omitempty"` @@ -1457,6 +1499,21 @@ type AccountInformationParamsExclude string // AccountInformationParamsFormat defines parameters for AccountInformation. type AccountInformationParamsFormat string +// AccountApplicationsInformationParams defines parameters for AccountApplicationsInformation. +type AccountApplicationsInformationParams struct { + // Limit Maximum number of results to return. + Limit *uint64 `form:"limit,omitempty" json:"limit,omitempty"` + + // Next The next page of results. Use the next token provided by the previous results. + Next *string `form:"next,omitempty" json:"next,omitempty"` + + // Include Include additional items in the response. Use `params` to include full application parameters (global state, schema, etc.). Multiple values can be comma-separated. Defaults to returning only application IDs and local state. + Include *[]AccountApplicationsInformationParamsInclude `form:"include,omitempty" json:"include,omitempty"` +} + +// AccountApplicationsInformationParamsInclude defines parameters for AccountApplicationsInformation. +type AccountApplicationsInformationParamsInclude string + // AccountApplicationInformationParams defines parameters for AccountApplicationInformation. type AccountApplicationInformationParams struct { // Format Configures whether the response object is JSON or MessagePack encoded. If not provided, defaults to JSON. 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 ce7f06a588..15eb973f1f 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go +++ b/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go @@ -184,231 +184,238 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "ju7+/wAAAP//IsjI8GQcAQA=", } // GetSwagger returns the content of the embedded swagger specification file 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 8ed5635845..53407e127c 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go +++ b/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go @@ -25,9 +25,15 @@ type ServerInterface interface { // Get account information. // (GET /v2/accounts/{address}) AccountInformation(ctx echo.Context, address basics.Address, params AccountInformationParams) error + // Get a list of applications held by an account. + // (GET /v2/accounts/{address}/applications) + AccountApplicationsInformation(ctx echo.Context, address basics.Address, params AccountApplicationsInformationParams) error // Get account information about a given app. // (GET /v2/accounts/{address}/applications/{application-id}) AccountApplicationInformation(ctx echo.Context, address basics.Address, applicationId basics.AppIndex, params AccountApplicationInformationParams) error + // Get a list of assets held by an account, inclusive of asset params. + // (GET /v2/accounts/{address}/assets) + AccountAssetsInformation(ctx echo.Context, address basics.Address, params AccountAssetsInformationParams) error // Get account information about a given asset. // (GET /v2/accounts/{address}/assets/{asset-id}) AccountAssetInformation(ctx echo.Context, address basics.Address, assetId basics.AssetIndex, params AccountAssetInformationParams) error @@ -127,7 +133,7 @@ func (w *ServerInterfaceWrapper) AccountInformation(ctx echo.Context) error { var params AccountInformationParams // ------------- Optional query parameter "exclude" ------------- - err = runtime.BindQueryParameter("form", true, false, "exclude", ctx.QueryParams(), ¶ms.Exclude) + err = runtime.BindQueryParameter("form", false, false, "exclude", ctx.QueryParams(), ¶ms.Exclude) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter exclude: %s", err)) } @@ -144,6 +150,47 @@ func (w *ServerInterfaceWrapper) AccountInformation(ctx echo.Context) error { return err } +// AccountApplicationsInformation converts echo context to params. +func (w *ServerInterfaceWrapper) AccountApplicationsInformation(ctx echo.Context) error { + var err error + // ------------- Path parameter "address" ------------- + var address basics.Address + + err = runtime.BindStyledParameterWithOptions("simple", "address", ctx.Param("address"), &address, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter address: %s", err)) + } + + ctx.Set(Api_keyScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params AccountApplicationsInformationParams + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", ctx.QueryParams(), ¶ms.Limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + // ------------- Optional query parameter "next" ------------- + + err = runtime.BindQueryParameter("form", true, false, "next", ctx.QueryParams(), ¶ms.Next) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter next: %s", err)) + } + + // ------------- Optional query parameter "include" ------------- + + err = runtime.BindQueryParameter("form", false, false, "include", ctx.QueryParams(), ¶ms.Include) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter include: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.AccountApplicationsInformation(ctx, address, params) + return err +} + // AccountApplicationInformation converts echo context to params. func (w *ServerInterfaceWrapper) AccountApplicationInformation(ctx echo.Context) error { var err error @@ -179,6 +226,40 @@ func (w *ServerInterfaceWrapper) AccountApplicationInformation(ctx echo.Context) return err } +// AccountAssetsInformation converts echo context to params. +func (w *ServerInterfaceWrapper) AccountAssetsInformation(ctx echo.Context) error { + var err error + // ------------- Path parameter "address" ------------- + var address basics.Address + + err = runtime.BindStyledParameterWithOptions("simple", "address", ctx.Param("address"), &address, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter address: %s", err)) + } + + ctx.Set(Api_keyScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params AccountAssetsInformationParams + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", ctx.QueryParams(), ¶ms.Limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + // ------------- Optional query parameter "next" ------------- + + err = runtime.BindQueryParameter("form", true, false, "next", ctx.QueryParams(), ¶ms.Next) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter next: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.AccountAssetsInformation(ctx, address, params) + return err +} + // AccountAssetInformation converts echo context to params. func (w *ServerInterfaceWrapper) AccountAssetInformation(ctx echo.Context) error { var err error @@ -722,7 +803,9 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL } router.GET(baseURL+"/v2/accounts/:address", wrapper.AccountInformation, m...) + router.GET(baseURL+"/v2/accounts/:address/applications", wrapper.AccountApplicationsInformation, m...) router.GET(baseURL+"/v2/accounts/:address/applications/:application-id", wrapper.AccountApplicationInformation, m...) + router.GET(baseURL+"/v2/accounts/:address/assets", wrapper.AccountAssetsInformation, m...) router.GET(baseURL+"/v2/accounts/:address/assets/:asset-id", wrapper.AccountAssetInformation, m...) router.GET(baseURL+"/v2/applications/:application-id", wrapper.GetApplicationByID, m...) router.GET(baseURL+"/v2/applications/:application-id/box", wrapper.GetApplicationBoxByName, m...) @@ -754,305 +837,322 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "9Gj6YPT+/wUAAP//psDgLSLlAQA=", } // GetSwagger returns the content of the embedded swagger specification file 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 6110d048f0..29fe0d6e4f 100644 --- a/daemon/algod/api/server/v2/generated/participating/private/routes.go +++ b/daemon/algod/api/server/v2/generated/participating/private/routes.go @@ -249,235 +249,242 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "Wp0bKwEA", } // GetSwagger returns the content of the embedded swagger specification file 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 2ba1687f18..296bfee771 100644 --- a/daemon/algod/api/server/v2/generated/participating/public/routes.go +++ b/daemon/algod/api/server/v2/generated/participating/public/routes.go @@ -178,238 +178,244 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "aTFxfqiT2lX28fRkdPN/AwAA//9KiHz6MCcBAA==", + "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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 348c973461..437aa136c2 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -27,6 +27,7 @@ import ( "net/http" "os" "runtime" + "slices" "strconv" "strings" "sync/atomic" @@ -82,6 +83,17 @@ const MaxAssetResults = 1000 // /v2/accounts/{address}/assets endpoint const DefaultAssetResults = uint64(1000) +// MaxApplicationResults sets a size limit for the number of applications returned in a single request to the +// /v2/accounts/{address}/applications endpoint when includeParams=true +const MaxApplicationResults = 1000 + +// MaxApplicationResultsWithoutParams sets a higher limit when params are excluded, since responses are ~100x smaller +const MaxApplicationResultsWithoutParams = MaxApplicationResults * 100 + +// DefaultApplicationResults sets a default size limit for the number of applications returned in a single request to the +// /v2/accounts/{address}/applications endpoint +const DefaultApplicationResults = uint64(1000) + const ( errInvalidLimit = "limit parameter must be a positive integer" errUnableToParseNext = "unable to parse next token" @@ -111,8 +123,10 @@ type LedgerForAPI interface { LookupAsset(rnd basics.Round, addr basics.Address, aidx basics.AssetIndex) (ledgercore.AssetResource, error) LookupAssets(addr basics.Address, assetIDGT basics.AssetIndex, limit uint64) ([]ledgercore.AssetResourceWithIDs, basics.Round, error) LookupApplication(rnd basics.Round, addr basics.Address, aidx basics.AppIndex) (ledgercore.AppResource, error) + LookupApplications(addr basics.Address, appIDGT basics.AppIndex, limit uint64, includeParams bool) ([]ledgercore.AppResourceWithIDs, basics.Round, error) BlockCert(rnd basics.Round) (blk bookkeeping.Block, cert agreement.Certificate, err error) LatestTotals() (basics.Round, ledgercore.AccountTotals, error) + OnlineCirculation(rnd basics.Round, voteRnd basics.Round) (basics.MicroAlgos, error) BlockHdr(rnd basics.Round) (blk bookkeeping.BlockHeader, err error) Wait(r basics.Round) chan struct{} WaitWithCancel(r basics.Round) (chan struct{}, func()) @@ -416,13 +430,36 @@ func (v2 *Handlers) AccountInformation(ctx echo.Context, address basics.Address, } // should we skip fetching apps and assets? - if params.Exclude != nil { - switch *params.Exclude { - case "all": - return v2.basicAccountInformation(ctx, address, handle, contentType) - case "none", "": - default: - return badRequest(ctx, err, errFailedToParseExclude, v2.Log) + var excludeCreatedAppsParams bool + var excludeCreatedAssetsParams bool + if params.Exclude != nil && len(*params.Exclude) > 0 { + excludeValues := *params.Exclude + + // Handle special cases that must be alone + if len(excludeValues) == 1 { + val := strings.TrimSpace(string(excludeValues[0])) + if val == "all" { + return v2.basicAccountInformation(ctx, address, handle, contentType) + } + if val == "none" || val == "" { + // Default behavior - include everything + excludeValues = nil + } + } + + // Parse exclusions + for _, excludeVal := range excludeValues { + val := strings.TrimSpace(string(excludeVal)) + switch val { + case "created-apps-params": + excludeCreatedAppsParams = true + case "created-assets-params": + excludeCreatedAssetsParams = true + case "all", "none": + return badRequest(ctx, fmt.Errorf("'%s' cannot be combined with other exclude values", val), errFailedToParseExclude, v2.Log) + default: + return badRequest(ctx, fmt.Errorf("invalid exclude value: %s", val), errFailedToParseExclude, v2.Log) + } } } @@ -470,7 +507,10 @@ func (v2 *Handlers) AccountInformation(ctx echo.Context, address basics.Address, return internalError(ctx, err, fmt.Sprintf("could not retrieve consensus information for last round (%d)", lastRound), v2.Log) } - account, err := AccountDataToAccount(address.String(), &record, lastRound, &consensus, amountWithoutPendingRewards) + account, err := AccountDataToAccount(address.String(), &record, lastRound, &consensus, amountWithoutPendingRewards, AccountDataToAccountOptions{ + ExcludeCreatedAppsParams: excludeCreatedAppsParams, + ExcludeCreatedAssetsParams: excludeCreatedAssetsParams, + }) if err != nil { return internalError(ctx, err, errInternalFailure, v2.Log) } @@ -586,7 +626,7 @@ func (v2 *Handlers) AccountAssetInformation(ctx echo.Context, address basics.Add if record.AssetParams != nil { asset := AssetParamsToAsset(address.String(), assetID, record.AssetParams) - response.CreatedAsset = &asset.Params + response.CreatedAsset = asset.Params } if record.AssetHolding != nil { @@ -634,7 +674,7 @@ func (v2 *Handlers) AccountApplicationInformation(ctx echo.Context, address basi if record.AppParams != nil { app := AppParamsToApplication(address.String(), applicationID, record.AppParams) - response.CreatedApp = &app.Params + response.CreatedApp = app.Params } if record.AppLocalState != nil { @@ -939,7 +979,20 @@ func (v2 *Handlers) GetTransactionProof(ctx echo.Context, round basics.Round, tx func (v2 *Handlers) GetSupply(ctx echo.Context) error { latest, totals, err := v2.Node.LedgerForAPI().LatestTotals() if err != nil { - err = fmt.Errorf("GetSupply(): round %d, failed: %v", latest, err) + err = fmt.Errorf("GetSupply(): round %d, LatestTotals failed: %v", latest, err) + return internalError(ctx, err, errInternalFailure, v2.Log) + } + + params, err := v2.Node.LedgerForAPI().ConsensusParams(latest) + if err != nil { + err = fmt.Errorf("GetSupply(): round %d, ConsensusParams failed: %v", latest, err) + return internalError(ctx, err, errInternalFailure, v2.Log) + } + + brnd := agreement.BalanceRound(latest, params) + onlineCirculation, err := v2.Node.LedgerForAPI().OnlineCirculation(brnd, latest) + if err != nil { + err = fmt.Errorf("GetSupply(): round %d, OnlineCirculation failed: %v", latest, err) return internalError(ctx, err, errInternalFailure, v2.Log) } @@ -947,6 +1000,7 @@ func (v2 *Handlers) GetSupply(ctx echo.Context) error { CurrentRound: latest, TotalMoney: totals.Participating().Raw, OnlineMoney: totals.Online.Money.Raw, + OnlineStake: onlineCirculation.Raw, } return ctx.JSON(http.StatusOK, supply) @@ -1136,10 +1190,6 @@ func (v2 *Handlers) RawTransactionAsync(ctx echo.Context) error { // AccountAssetsInformation looks up an account's asset holdings. // (GET /v2/accounts/{address}/assets) func (v2 *Handlers) AccountAssetsInformation(ctx echo.Context, address basics.Address, params model.AccountAssetsInformationParams) error { - if !v2.Node.Config().EnableExperimentalAPI { - return ctx.String(http.StatusNotFound, "/v2/accounts/{address}/assets was not enabled in the configuration file by setting the EnableExperimentalAPI to true") - } - var assetGreaterThan uint64 = 0 if params.Next != nil { agt, err0 := strconv.ParseUint(*params.Next, 10, 64) @@ -1207,7 +1257,7 @@ func (v2 *Handlers) AccountAssetsInformation(ctx echo.Context, address basics.Ad if !record.Creator.IsZero() { asset := AssetParamsToAsset(record.Creator.String(), record.AssetID, record.AssetParams) - aah.AssetParams = &asset.Params + aah.AssetParams = asset.Params } assetHoldings = append(assetHoldings, aah) @@ -1218,6 +1268,110 @@ func (v2 *Handlers) AccountAssetsInformation(ctx echo.Context, address basics.Ad return ctx.JSON(http.StatusOK, response) } +// AccountApplicationsInformation returns application resources for a specific address. +func (v2 *Handlers) AccountApplicationsInformation(ctx echo.Context, address basics.Address, params model.AccountApplicationsInformationParams) error { + var appGreaterThan uint64 = 0 + if params.Next != nil { + agt, err0 := strconv.ParseUint(*params.Next, 10, 64) + if err0 != nil { + return badRequest(ctx, err0, fmt.Sprintf("%s: %v", errUnableToParseNext, err0), v2.Log) + } + appGreaterThan = agt + } + + // Determine includeParams first, as it affects limit validation + includeParams := false + if params.Include != nil { + if slices.Contains(*params.Include, model.AccountApplicationsInformationParamsIncludeParams) { + includeParams = true + } + } + + // Choose appropriate max limit based on whether params are included + // When params are excluded, responses are ~100x smaller, so we can return more results + maxLimit := uint64(MaxApplicationResults) + if !includeParams { + maxLimit = uint64(MaxApplicationResultsWithoutParams) + } + + if params.Limit != nil { + if *params.Limit <= 0 { + return badRequest(ctx, errors.New(errInvalidLimit), errInvalidLimit, v2.Log) + } + + if *params.Limit > maxLimit { + limitErrMsg := fmt.Sprintf("limit %d exceeds max applications single batch limit %d", *params.Limit, maxLimit) + return badRequest(ctx, errors.New(limitErrMsg), limitErrMsg, v2.Log) + } + } else { + // default limit + l := DefaultApplicationResults + params.Limit = &l + } + + ledger := v2.Node.LedgerForAPI() + + // Logic + // 1. Get the account's application resources subject to limits + // 2. Handle empty response + // 3. Prepare JSON response + + // We intentionally request one more than the limit to determine if there are more applications. + records, lookupRound, err := ledger.LookupApplications(address, basics.AppIndex(appGreaterThan), *params.Limit+1, includeParams) + + if err != nil { + return internalError(ctx, err, errFailedLookingUpLedger, v2.Log) + } + + // prepare JSON response + response := model.AccountApplicationsInformationResponse{Round: lookupRound} + + // If the total count is greater than the limit, we set the next token to the last application ID being returned + if uint64(len(records)) > *params.Limit { + // we do not include the last record in the response + records = records[:*params.Limit] + nextTk := strconv.FormatUint(uint64(records[len(records)-1].AppID), 10) + response.NextToken = &nextTk + } + + applicationResources := make([]model.AccountApplicationResource, 0, len(records)) + + for _, record := range records { + // Skip if neither local state nor params exist (shouldn't happen) + if record.AppLocalState == nil && record.AppParams == nil { + v2.Log.Warnf("AccountApplicationsInformation: application %d has neither local state nor params", record.AppID) + continue + } + + aah := model.AccountApplicationResource{ + Id: record.AppID, + } + + if !record.Creator.IsZero() { + // App exists - don't set Deleted field (omit from JSON) + if includeParams && record.AppParams != nil { + app := AppParamsToApplication(record.Creator.String(), record.AppID, record.AppParams) + aah.Params = app.Params + } + } else { + // App deleted - set Deleted=true (only include when true) + deleted := true + aah.Deleted = &deleted + } + + if record.AppLocalState != nil { + appLocalState := AppLocalState(*record.AppLocalState, record.AppID) + aah.AppLocalState = &appLocalState + } + + applicationResources = append(applicationResources, aah) + } + + response.ApplicationResources = &applicationResources + + return ctx.JSON(http.StatusOK, response) +} + // PreEncodedSimulateTxnResult mirrors model.SimulateTransactionResult type PreEncodedSimulateTxnResult struct { Txn PreEncodedTxInfo `codec:"txn-result"` diff --git a/daemon/algod/api/server/v2/handlers_test.go b/daemon/algod/api/server/v2/handlers_test.go index 03186a7cef..fbf564407f 100644 --- a/daemon/algod/api/server/v2/handlers_test.go +++ b/daemon/algod/api/server/v2/handlers_test.go @@ -23,10 +23,11 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" + "github.com/algorand/go-algorand/test/partitiontest" ) func TestApplicationBoxesMaxKeys(t *testing.T) { diff --git a/daemon/algod/api/server/v2/test/genesis_types_test.go b/daemon/algod/api/server/v2/test/genesis_types_test.go index 3795a2b9b0..526396074c 100644 --- a/daemon/algod/api/server/v2/test/genesis_types_test.go +++ b/daemon/algod/api/server/v2/test/genesis_types_test.go @@ -21,10 +21,11 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // getCodecTag extracts the base name from a codec tag, ignoring any additional parameters 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 d4fe5daa70..6753001620 100644 --- a/daemon/algod/api/server/v2/test/handlers_resources_test.go +++ b/daemon/algod/api/server/v2/test/handlers_resources_test.go @@ -21,11 +21,10 @@ import ( "fmt" "net/http" "net/http/httptest" + "slices" "strconv" "testing" - "github.com/algorand/go-algorand/data/transactions/logic" - "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -38,11 +37,13 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger/ledgercore" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" + "github.com/algorand/go-algorand/util" ) type mockLedger struct { @@ -148,12 +149,71 @@ func (l *mockLedger) LookupApplication(rnd basics.Round, addr basics.Address, ai } return ar, nil } + +func (l *mockLedger) LookupApplications(addr basics.Address, appIDGT basics.AppIndex, limit uint64, includeParams bool) ([]ledgercore.AppResourceWithIDs, basics.Round, error) { + ad, ok := l.accounts[addr] + if !ok { + return nil, basics.Round(0), nil + } + + // Collect all app IDs owned by this account (both created and opted-in) + appIDSet := make(util.Set[basics.AppIndex]) + for appID := range ad.AppParams { + if appID > appIDGT { + appIDSet.Add(appID) + } + } + for appID := range ad.AppLocalStates { + if appID > appIDGT { + appIDSet.Add(appID) + } + } + + // Sort app IDs to match database ORDER BY behavior + var ownedAppIDs []basics.AppIndex + for appID := range appIDSet { + ownedAppIDs = append(ownedAppIDs, appID) + } + slices.Sort(ownedAppIDs) + + // Return up to 'limit' apps (matching database LIMIT behavior) + if uint64(len(ownedAppIDs)) > limit { + ownedAppIDs = ownedAppIDs[:limit] + } + + // Build results for the selected apps + var res []ledgercore.AppResourceWithIDs + for _, appID := range ownedAppIDs { + apr := ledgercore.AppResourceWithIDs{ + AppID: appID, + } + + if ap, ok := ad.AppParams[appID]; ok { + // Only populate AppParams if requested to match the optimization in the real implementation + if includeParams { + apr.AppParams = &ap + } + apr.Creator = addr + } + + if ls, ok := ad.AppLocalStates[appID]; ok { + apr.AppLocalState = &ls + } + + res = append(res, apr) + } + return res, basics.Round(0), nil +} + func (l *mockLedger) BlockCert(rnd basics.Round) (blk bookkeeping.Block, cert agreement.Certificate, err error) { panic("not implemented") } func (l *mockLedger) LatestTotals() (rnd basics.Round, at ledgercore.AccountTotals, err error) { panic("not implemented") } +func (l *mockLedger) OnlineCirculation(rnd, voteRnd basics.Round) (basics.MicroAlgos, error) { + panic("not implemented") +} func (l *mockLedger) BlockHdr(rnd basics.Round) (bookkeeping.BlockHeader, error) { blk, err := l.Block(rnd) if err != nil { @@ -277,7 +337,6 @@ func setupTestForLargeResources(t *testing.T, acctSize, maxResults int, accountM mockNode := makeMockNode(&ml, t.Name(), nil, cannedStatusReportGolden, false) mockNode.config.MaxAPIResourcesPerAccount = uint64(maxResults) - mockNode.config.EnableExperimentalAPI = true dummyShutdownChan := make(chan struct{}) handlers = v2.Handlers{ Node: mockNode, @@ -299,7 +358,8 @@ func accountInformationResourceLimitsTest(t *testing.T, accountMaker func(int) b handlers, addr, acctData := setupTestForLargeResources(t, acctSize, maxResults, accountMaker) params := model.AccountInformationParams{} if exclude != "" { - params.Exclude = (*model.AccountInformationParamsExclude)(&exclude) + excludeSlice := []model.AccountInformationParamsExclude{model.AccountInformationParamsExclude(exclude)} + params.Exclude = &excludeSlice } ctx, rec := newReq(t) err := handlers.AccountInformation(ctx, addr, params) @@ -381,7 +441,7 @@ func accountInformationResourceLimitsTest(t *testing.T, accountMaker func(int) b assert.Nil(t, ret.AssetHolding) ap := acctData.AssetParams[aidx] assetParams := v2.AssetParamsToAsset(addr.String(), aidx, &ap) - assert.Equal(t, ret.CreatedAsset, &assetParams.Params) + assert.Equal(t, ret.CreatedAsset, assetParams.Params) } for i := 0; i < ret.TotalApps; i++ { ctx, rec = newReq(t) @@ -545,3 +605,238 @@ func TestAccountInformationResourceLimits(t *testing.T) { }) } } + +func randomAccountWithSomeAppLocalStatesAndOverlappingAppParams(overlapN int, nonOverlapAppLocalStatesN int) basics.AccountData { + a := ledgertesting.RandomAccountData(0) + a.AppParams = make(map[basics.AppIndex]basics.AppParams) + a.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState) + + // Use sparse app IDs to test pagination with non-sequential IDs + // This exercises the fix for https://github.com/algorand/go-algorand/pull/6552#discussion_r2795538306 + const appIDGap = 100 // Create 100-ID gaps between apps + + // overlapN apps have both app params and app local states + for i := 1; i <= overlapN; i++ { + appID := basics.AppIndex(i * appIDGap) + a.AppParams[appID] = ledgertesting.RandomAppParams() + a.AppLocalStates[appID] = ledgertesting.RandomAppLocalState() + } + + // nonOverlapAppLocalStatesN apps have only app local states + for i := 1; i <= nonOverlapAppLocalStatesN; i++ { + appID := basics.AppIndex((overlapN + i) * appIDGap) + a.AppLocalStates[appID] = ledgertesting.RandomAppLocalState() + } + return a +} + +func accountApplicationInformationResourceLimitsTest(t *testing.T, handlers v2.Handlers, addr basics.Address, + acctData basics.AccountData, params model.AccountApplicationsInformationParams, inputNextToken int, maxResults int, expectToken bool) { + + ctx, rec := newReq(t) + err := handlers.AccountApplicationsInformation(ctx, addr, params) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + var ret model.AccountApplicationsInformationResponse + err = json.Unmarshal(rec.Body.Bytes(), &ret) + require.NoError(t, err) + + if expectToken { + nextRaw, err0 := strconv.ParseUint(*ret.NextToken, 10, 64) + require.NoError(t, err0) + // The next token decoded is actually the last app id returned + assert.EqualValues(t, (*ret.ApplicationResources)[maxResults-1].Id, nextRaw) + } + assert.Equal(t, maxResults, len(*ret.ApplicationResources)) + + // Build sorted list of expected app IDs from account data (handles sparse IDs) + var expectedAppIDs []basics.AppIndex + appIDSet := make(util.Set[basics.AppIndex]) + for appID := range acctData.AppParams { + if appID > basics.AppIndex(inputNextToken) { + appIDSet.Add(appID) + } + } + for appID := range acctData.AppLocalStates { + if appID > basics.AppIndex(inputNextToken) { + appIDSet.Add(appID) + } + } + for appID := range appIDSet { + expectedAppIDs = append(expectedAppIDs, appID) + } + slices.Sort(expectedAppIDs) + + // Verify returned apps match expected IDs + for i := 0; i < maxResults && i < len(expectedAppIDs); i++ { + expectedID := expectedAppIDs[i] + assert.Equal(t, expectedID, (*ret.ApplicationResources)[i].Id, "App at position %d should have ID %d", i, expectedID) + if (*ret.ApplicationResources)[i].AppLocalState != nil { + assert.Equal(t, expectedID, (*ret.ApplicationResources)[i].AppLocalState.Id) + } + } +} + +// TestAccountApplicationsInformation tests the account application information endpoint +func TestAccountApplicationsInformation(t *testing.T) { + partitiontest.PartitionTest(t) + + accountOverlappingAppParamsLocalStatesCount := 1000 + accountNonOverlappingAppLocalStatesCount := 25 + totalAppLocalStates := accountOverlappingAppParamsLocalStatesCount + accountNonOverlappingAppLocalStatesCount + + handlers, addr, acctData := setupTestForLargeResources(t, accountOverlappingAppParamsLocalStatesCount, 50, func(N int) basics.AccountData { + return randomAccountWithSomeAppLocalStatesAndOverlappingAppParams(N, accountNonOverlappingAppLocalStatesCount) + }) + + // 1. Query with no limit/pagination - should get DefaultApplicationResults back + accountApplicationInformationResourceLimitsTest(t, handlers, addr, acctData, model.AccountApplicationsInformationParams{}, + 0, int(v2.DefaultApplicationResults), false) + + rawLimit := 100 + limit := uint64(rawLimit) + // 2. Query with limit P/2 { @@ -293,6 +324,12 @@ func testingenvWithBalances(t testing.TB, minMoneyAtStart, maxMoneyAtStart, numA data := basics_testing.MakeAccountData(basics.Online, startamt) data.SelectionID = parts[i].VRFSecrets().PK data.VoteID = parts[i].VotingSecrets().OneTimeSignatureVerifier + if numExpiredOnline > 0 && i < numExpiredOnline { + if expiredVoteLastValid == 0 { + expiredVoteLastValid = 50 // default if zero + } + data.VoteLastValid = basics.Round(expiredVoteLastValid) + } genesis[short] = data } part.Close() diff --git a/daemon/algod/api/server/v2/utils.go b/daemon/algod/api/server/v2/utils.go index b2757e375d..2d26d401ac 100644 --- a/daemon/algod/api/server/v2/utils.go +++ b/daemon/algod/api/server/v2/utils.go @@ -27,9 +27,10 @@ import ( "unicode" "unicode/utf8" - "github.com/algorand/go-codec/codec" "github.com/labstack/echo/v4" + "github.com/algorand/go-codec/codec" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" "github.com/algorand/go-algorand/data/basics" diff --git a/daemon/algod/server_test.go b/daemon/algod/server_test.go index 53ec159b7c..c5ceaa84dc 100644 --- a/daemon/algod/server_test.go +++ b/daemon/algod/server_test.go @@ -23,8 +23,9 @@ import ( "net" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func isTCPPortAvailable(host string, port int) bool { diff --git a/daemon/kmd/server/server.go b/daemon/kmd/server/server.go index 1c441a9c71..e2cec6c8dc 100644 --- a/daemon/kmd/server/server.go +++ b/daemon/kmd/server/server.go @@ -26,9 +26,10 @@ import ( "syscall" "time" - "github.com/algorand/go-deadlock" "github.com/gofrs/flock" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/daemon/kmd/api" "github.com/algorand/go-algorand/daemon/kmd/session" "github.com/algorand/go-algorand/logging" diff --git a/daemon/kmd/session/session.go b/daemon/kmd/session/session.go index 3e6580228d..8d9009769b 100644 --- a/daemon/kmd/session/session.go +++ b/daemon/kmd/session/session.go @@ -20,9 +20,10 @@ import ( "context" "time" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/daemon/kmd/config" "github.com/algorand/go-algorand/daemon/kmd/wallet" - "github.com/algorand/go-deadlock" ) type walletHandle struct { diff --git a/daemon/kmd/wallet/driver/ledger_hid.go b/daemon/kmd/wallet/driver/ledger_hid.go index 32f5e96c3a..a89fa68d32 100644 --- a/daemon/kmd/wallet/driver/ledger_hid.go +++ b/daemon/kmd/wallet/driver/ledger_hid.go @@ -169,6 +169,11 @@ func (l *LedgerUSB) ReadPackets() ([]byte, error) { // Exchange sends a message to the Ledger device, waits for a response, // and returns the response data. func (l *LedgerUSB) Exchange(msg []byte) ([]byte, error) { + // Mask SIGURG to prevent Go's async preemption from interrupting + // macOS IOKit HID calls (causes kIOReturnError on Darwin). + cleanup := maskSIGURG() + defer cleanup() + err := l.WritePackets(msg) if err != nil { return nil, err diff --git a/daemon/kmd/wallet/driver/ledger_hid_darwin.go b/daemon/kmd/wallet/driver/ledger_hid_darwin.go new file mode 100644 index 0000000000..55aa2b9438 --- /dev/null +++ b/daemon/kmd/wallet/driver/ledger_hid_darwin.go @@ -0,0 +1,56 @@ +// Copyright (C) 2019-2026 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 . + +//go:build darwin + +package driver + +import ( + "runtime" +) + +// #include +import "C" + +// maskSIGURG temporarily blocks SIGURG signals during HID operations on macOS. +// +// Go's runtime uses SIGURG for async preemption (goroutine scheduling and GC), +// but this can interfere with macOS IOKit HID calls. When a SIGURG signal is +// delivered during an IOHIDDeviceSetReport call, macOS may return +// kIOReturnError (0xE00002BC), causing the HID operation to fail. +// +// By intercepting SIGURG signals during HID I/O, we prevent the Go runtime's +// async preemption from interrupting the underlying IOKit calls. +// +// Returns a cleanup function that must be called to restore normal signal +// handling. Typical usage: +// +// cleanup := maskSIGURG() +// defer cleanup() +// // ... perform HID operations ... +func maskSIGURG() func() { + runtime.LockOSThread() + + var oldset, newset C.sigset_t + C.sigemptyset(&newset) + C.sigaddset(&newset, C.SIGURG) + C.pthread_sigmask(C.SIG_BLOCK, &newset, &oldset) + + return func() { + C.pthread_sigmask(C.SIG_SETMASK, &oldset, nil) + runtime.UnlockOSThread() + } +} diff --git a/ledger/eval/prefetcher/error.go b/daemon/kmd/wallet/driver/ledger_hid_other.go similarity index 50% rename from ledger/eval/prefetcher/error.go rename to daemon/kmd/wallet/driver/ledger_hid_other.go index 9525525e2a..86e4b1796c 100644 --- a/ledger/eval/prefetcher/error.go +++ b/daemon/kmd/wallet/driver/ledger_hid_other.go @@ -14,30 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package prefetcher +//go:build !darwin -import ( - "fmt" +package driver - "github.com/algorand/go-algorand/data/basics" -) - -// GroupTaskError indicates the group index of the unfulfilled resource -type GroupTaskError struct { - err error - GroupIdx int64 - Address *basics.Address - CreatableIndex basics.CreatableIndex - CreatableType basics.CreatableType -} - -// Error satisfies builtin interface `error` -func (err *GroupTaskError) Error() string { - return fmt.Sprintf("prefetch failed for groupIdx %d, address: %s, creatableIndex %d, creatableType %d, cause: %v", - err.GroupIdx, err.Address, err.CreatableIndex, err.CreatableType, err.err) -} - -// Unwrap provides access to the underlying error -func (err *GroupTaskError) Unwrap() error { - return err.err +// maskSIGURG is a no-op on non-Darwin platforms. +// +// On macOS, Go's SIGURG-based async preemption can interfere with IOKit HID +// calls, but this is not an issue on other platforms. See ledger_hid_darwin.go +// for the Darwin-specific implementation. +func maskSIGURG() func() { + return func() {} } diff --git a/daemon/kmd/wallet/driver/sqlite.go b/daemon/kmd/wallet/driver/sqlite.go index 67099647a3..77b4ce9335 100644 --- a/daemon/kmd/wallet/driver/sqlite.go +++ b/daemon/kmd/wallet/driver/sqlite.go @@ -24,10 +24,12 @@ import ( "path/filepath" "regexp" - "github.com/algorand/go-deadlock" "github.com/jmoiron/sqlx" "github.com/mattn/go-sqlite3" + "github.com/algorand/go-codec/codec" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/kmd/config" "github.com/algorand/go-algorand/daemon/kmd/wallet" @@ -35,7 +37,6 @@ import ( "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-codec/codec" ) const ( diff --git a/data/account/participationRegistry.go b/data/account/participationRegistry.go index cbd4de2486..89f137f9d2 100644 --- a/data/account/participationRegistry.go +++ b/data/account/participationRegistry.go @@ -24,6 +24,8 @@ import ( "fmt" "time" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" @@ -31,7 +33,6 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" - "github.com/algorand/go-deadlock" ) const defaultTimeout = 5 * time.Second diff --git a/data/appRateLimiter.go b/data/appRateLimiter.go index 230fa8714a..b95b22798a 100644 --- a/data/appRateLimiter.go +++ b/data/appRateLimiter.go @@ -22,14 +22,16 @@ import ( "sync/atomic" "time" + "golang.org/x/crypto/blake2b" + + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config/bounds" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util" - "github.com/algorand/go-deadlock" - "golang.org/x/crypto/blake2b" ) const numBuckets = 128 diff --git a/data/appRateLimiter_test.go b/data/appRateLimiter_test.go index 690fcb2012..dc4080053b 100644 --- a/data/appRateLimiter_test.go +++ b/data/appRateLimiter_test.go @@ -21,6 +21,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/blake2b" + "golang.org/x/exp/rand" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/config/bounds" "github.com/algorand/go-algorand/crypto" @@ -28,9 +32,6 @@ import ( "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/blake2b" - "golang.org/x/exp/rand" ) func TestAppRateLimiter_Make(t *testing.T) { diff --git a/data/basics/fraction_test.go b/data/basics/fraction_test.go index 02f61b8a30..a73744a297 100644 --- a/data/basics/fraction_test.go +++ b/data/basics/fraction_test.go @@ -20,8 +20,9 @@ import ( "math" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestFraction(t *testing.T) { diff --git a/data/basics/serr_test.go b/data/basics/serr_test.go index 90f8a9b6d6..685c0690ec 100644 --- a/data/basics/serr_test.go +++ b/data/basics/serr_test.go @@ -21,8 +21,9 @@ import ( "fmt" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestNew(t *testing.T) { diff --git a/data/basics/testing/nearzero_test.go b/data/basics/testing/nearzero_test.go index ef47b13096..e2363e779d 100644 --- a/data/basics/testing/nearzero_test.go +++ b/data/basics/testing/nearzero_test.go @@ -19,8 +19,9 @@ package testing import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestNearZeros(t *testing.T) { diff --git a/data/basics/units_test.go b/data/basics/units_test.go index 8bd3ee6667..8d259922d0 100644 --- a/data/basics/units_test.go +++ b/data/basics/units_test.go @@ -21,9 +21,10 @@ import ( "math/big" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestODiff(t *testing.T) { diff --git a/data/bookkeeping/lightBlockHeader_test.go b/data/bookkeeping/lightBlockHeader_test.go index ad8600307a..8bd7349ca3 100644 --- a/data/bookkeeping/lightBlockHeader_test.go +++ b/data/bookkeeping/lightBlockHeader_test.go @@ -20,12 +20,13 @@ import ( "strings" "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/committee" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestConvertSha256Header(t *testing.T) { diff --git a/data/committee/credential.go b/data/committee/credential.go index 7d6ce35b1e..6f7e35a8b1 100644 --- a/data/committee/credential.go +++ b/data/committee/credential.go @@ -21,12 +21,13 @@ import ( "fmt" "math/big" + "github.com/algorand/sortition" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/sortition" ) type ( diff --git a/data/transactions/asset_test.go b/data/transactions/asset_test.go index f12f19a716..33261fa95b 100644 --- a/data/transactions/asset_test.go +++ b/data/transactions/asset_test.go @@ -21,11 +21,12 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestAxferWellFormedErrors(t *testing.T) { diff --git a/data/transactions/heartbeat_test.go b/data/transactions/heartbeat_test.go index 538901dccc..e5b898a865 100644 --- a/data/transactions/heartbeat_test.go +++ b/data/transactions/heartbeat_test.go @@ -20,14 +20,15 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/committee" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestWellFormedHeartbeatErrors(t *testing.T) { diff --git a/data/transactions/json_test.go b/data/transactions/json_test.go index ddaca98413..0ef15db1b4 100644 --- a/data/transactions/json_test.go +++ b/data/transactions/json_test.go @@ -26,12 +26,13 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func decode(t *testing.T, data string, v interface{}) { diff --git a/data/transactions/keyreg_test.go b/data/transactions/keyreg_test.go index 59d5dac223..8684f11a27 100644 --- a/data/transactions/keyreg_test.go +++ b/data/transactions/keyreg_test.go @@ -21,13 +21,14 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "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/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) var generateFlag = flag.Bool("generate", false, "") diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index aba822b138..63638b6e9e 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -34,6 +34,7 @@ import ( "unicode" "github.com/algorand/avm-abi/abi" + "github.com/algorand/go-algorand/data/basics" ) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index 7aab034ccb..8f9bffd1a1 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -22,11 +22,12 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // This test ensures a program compiled with by pre-AVM v2 go-algorand diff --git a/data/transactions/logic/crypto.go b/data/transactions/logic/crypto.go index f3e05ee120..38707d4496 100644 --- a/data/transactions/logic/crypto.go +++ b/data/transactions/logic/crypto.go @@ -26,14 +26,15 @@ import ( "hash" "math/big" + bls12_381mimc "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/mimc" + bn254mimc "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" + "golang.org/x/crypto/sha3" + + "github.com/algorand/go-sumhash" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/secp256k1" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-sumhash" - "golang.org/x/crypto/sha3" - - bls12_381mimc "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/mimc" - bn254mimc "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" ) // mimc is implemented for compatibility with zk circuits, diff --git a/data/transactions/logic/debugger_eval_test.go b/data/transactions/logic/debugger_eval_test.go index d2e6bf6efb..208e686165 100644 --- a/data/transactions/logic/debugger_eval_test.go +++ b/data/transactions/logic/debugger_eval_test.go @@ -21,11 +21,12 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" . "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/transactions/logic/mocktracer" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) const debuggerTestProgramApprove string = `intcblock 0 1 1 1 1 5 100 diff --git a/data/transactions/logic/debugger_test.go b/data/transactions/logic/debugger_test.go index fedf24bd9c..ebcb152787 100644 --- a/data/transactions/logic/debugger_test.go +++ b/data/transactions/logic/debugger_test.go @@ -19,8 +19,9 @@ package logic import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestLineToPC(t *testing.T) { diff --git a/data/transactions/logic/doc_test.go b/data/transactions/logic/doc_test.go index a1e3512fef..955345211d 100644 --- a/data/transactions/logic/doc_test.go +++ b/data/transactions/logic/doc_test.go @@ -20,9 +20,10 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestOpDocs(t *testing.T) { diff --git a/data/transactions/logic/evalAppTxn_test.go b/data/transactions/logic/evalAppTxn_test.go index f1a645ffef..9102f8cf6e 100644 --- a/data/transactions/logic/evalAppTxn_test.go +++ b/data/transactions/logic/evalAppTxn_test.go @@ -24,6 +24,8 @@ import ( "strings" "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/transactions" @@ -31,8 +33,6 @@ import ( "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - - "github.com/stretchr/testify/require" ) func TestInnerTypesV5(t *testing.T) { diff --git a/data/transactions/logic/evalBench_test.go b/data/transactions/logic/evalBench_test.go index 229f1780bc..226379ccb7 100644 --- a/data/transactions/logic/evalBench_test.go +++ b/data/transactions/logic/evalBench_test.go @@ -19,12 +19,13 @@ package logic_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) func BenchmarkCheckSignature(b *testing.B) { diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 5c81bcaf41..d088b60581 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -30,6 +30,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "pgregory.net/rapid" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/config/bounds" @@ -41,8 +42,6 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util" - - "pgregory.net/rapid" ) type protoOpt func(*config.ConsensusParams) diff --git a/data/transactions/logic/jsonspec_test.go b/data/transactions/logic/jsonspec_test.go index 3e78b4547c..63f047e168 100644 --- a/data/transactions/logic/jsonspec_test.go +++ b/data/transactions/logic/jsonspec_test.go @@ -22,10 +22,11 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" "golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode/utf32" + + "github.com/algorand/go-algorand/test/partitiontest" ) // As of go1.10, json implements encoding and decoding of JSON as defined in RFC 7159. https://pkg.go.dev/encoding/json diff --git a/data/transactions/logic/mocktracer/tracer.go b/data/transactions/logic/mocktracer/tracer.go index 050fa7bbe4..19c98cbd64 100644 --- a/data/transactions/logic/mocktracer/tracer.go +++ b/data/transactions/logic/mocktracer/tracer.go @@ -19,14 +19,15 @@ package mocktracer import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // EventType represents a type of logic.EvalTracer event diff --git a/data/transactions/logic/opcodes_test.go b/data/transactions/logic/opcodes_test.go index bdb8fb4441..43532c3ae5 100644 --- a/data/transactions/logic/opcodes_test.go +++ b/data/transactions/logic/opcodes_test.go @@ -22,8 +22,9 @@ import ( "slices" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestOpSpecs(t *testing.T) { diff --git a/data/transactions/logic/pairing.go b/data/transactions/logic/pairing.go index 25acf7a280..22941bf1ce 100644 --- a/data/transactions/logic/pairing.go +++ b/data/transactions/logic/pairing.go @@ -21,13 +21,12 @@ import ( "math/big" "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/bn254" - bn254fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" - bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" - bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" bls12381fp "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" bls12381fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bn254" + bn254fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" + bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" ) type sError string diff --git a/data/transactions/logic/pairing_test.go b/data/transactions/logic/pairing_test.go index f97e4e57c4..32a4676aba 100644 --- a/data/transactions/logic/pairing_test.go +++ b/data/transactions/logic/pairing_test.go @@ -24,7 +24,6 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" bls12381fp "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" bls12381fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" @@ -32,6 +31,8 @@ import ( bn254fp "github.com/consensys/gnark-crypto/ecc/bn254/fp" bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) const pairingNonsense = ` diff --git a/data/transactions/logic/resources_test.go b/data/transactions/logic/resources_test.go index 11691a0bc5..c93a91d81c 100644 --- a/data/transactions/logic/resources_test.go +++ b/data/transactions/logic/resources_test.go @@ -22,13 +22,14 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" . "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TestAppSharing confirms that as of v9, apps can be accessed across groups, diff --git a/data/transactions/logic/sourcemap_test.go b/data/transactions/logic/sourcemap_test.go index 04159bdc90..5f23231792 100644 --- a/data/transactions/logic/sourcemap_test.go +++ b/data/transactions/logic/sourcemap_test.go @@ -19,8 +19,9 @@ package logic import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestGetSourceMap(t *testing.T) { diff --git a/data/transactions/logic/tracer_test.go b/data/transactions/logic/tracer_test.go index 386c9ee3a7..0ea5124fbf 100644 --- a/data/transactions/logic/tracer_test.go +++ b/data/transactions/logic/tracer_test.go @@ -19,11 +19,12 @@ package logic_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" . "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/transactions/logic/mocktracer" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) type tracerTestCase struct { diff --git a/data/transactions/payset_test.go b/data/transactions/payset_test.go index 33f99dafbf..11dc6eded2 100644 --- a/data/transactions/payset_test.go +++ b/data/transactions/payset_test.go @@ -19,8 +19,9 @@ package transactions import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func generatePayset(txnCount, acctCount int) Payset { diff --git a/data/transactions/stateproof_test.go b/data/transactions/stateproof_test.go index 6ef95e8d6d..18acf8e56d 100644 --- a/data/transactions/stateproof_test.go +++ b/data/transactions/stateproof_test.go @@ -20,6 +20,8 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/stateproof" @@ -27,7 +29,6 @@ import ( "github.com/algorand/go-algorand/data/stateproofmsg" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) type stateproofTxnTestCase struct { diff --git a/data/transactions/teal_test.go b/data/transactions/teal_test.go index 954fe501ba..e7095188ef 100644 --- a/data/transactions/teal_test.go +++ b/data/transactions/teal_test.go @@ -20,10 +20,11 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestEvalDeltaEqual(t *testing.T) { diff --git a/data/transactions/verify/artifact_test.go b/data/transactions/verify/artifact_test.go index 748d7dccab..e7569f20b6 100644 --- a/data/transactions/verify/artifact_test.go +++ b/data/transactions/verify/artifact_test.go @@ -19,14 +19,14 @@ package verify import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/protocol" - - "github.com/stretchr/testify/require" ) // test/benchmark real programs found in the wild (testnet/mainnet). diff --git a/data/txDupCache.go b/data/txDupCache.go index 92b7087721..3f85c910af 100644 --- a/data/txDupCache.go +++ b/data/txDupCache.go @@ -23,11 +23,12 @@ import ( "sync" "time" - "github.com/algorand/go-algorand/config/bounds" - "github.com/algorand/go-algorand/crypto" + "golang.org/x/crypto/blake2b" + "github.com/algorand/go-deadlock" - "golang.org/x/crypto/blake2b" + "github.com/algorand/go-algorand/config/bounds" + "github.com/algorand/go-algorand/crypto" ) // digestCache is a rotating cache of size N accepting crypto.Digest as a key diff --git a/data/txDupCache_test.go b/data/txDupCache_test.go index 79899b313d..ac0f9be0ee 100644 --- a/data/txDupCache_test.go +++ b/data/txDupCache_test.go @@ -25,12 +25,12 @@ import ( "time" "github.com/stretchr/testify/require" + "golang.org/x/crypto/blake2b" - "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-deadlock" - "golang.org/x/crypto/blake2b" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/test/partitiontest" ) func TestTxHandlerDigestCache(t *testing.T) { diff --git a/data/txHandler.go b/data/txHandler.go index db3f8e03a5..d42824da2a 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -25,6 +25,8 @@ import ( "sync" "time" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/config/bounds" "github.com/algorand/go-algorand/crypto" @@ -38,7 +40,6 @@ import ( "github.com/algorand/go-algorand/util" "github.com/algorand/go-algorand/util/execpool" "github.com/algorand/go-algorand/util/metrics" - "github.com/algorand/go-deadlock" ) var transactionMessagesHandled = metrics.MakeCounter(metrics.TransactionMessagesHandled) diff --git a/data/txntest/defi.go b/data/txntest/defi.go index 490fc35b86..027badccc3 100644 --- a/data/txntest/defi.go +++ b/data/txntest/defi.go @@ -19,11 +19,12 @@ package txntest import ( "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/transactions" "github.com/algorand/go-algorand/data/transactions/logic" - "github.com/stretchr/testify/require" ) // Test/benchmark real programs found in the wild (testnet/mainnet). diff --git a/gen/generate_test.go b/gen/generate_test.go index c02bf5bd63..e8207bb107 100644 --- a/gen/generate_test.go +++ b/gen/generate_test.go @@ -27,14 +27,13 @@ import ( "sync" "testing" - "github.com/algorand/go-algorand/data/basics" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/account" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" diff --git a/heartbeat/service_test.go b/heartbeat/service_test.go index 652cfe153d..8fcc9a7056 100644 --- a/heartbeat/service_test.go +++ b/heartbeat/service_test.go @@ -21,6 +21,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/account" @@ -32,8 +36,6 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/algorand/go-deadlock" - "github.com/stretchr/testify/require" ) type table map[basics.Address]ledgercore.AccountData diff --git a/internal/rapidgen/rapidgenerators.go b/internal/rapidgen/rapidgenerators.go index d8fcf0cace..1f31da168b 100644 --- a/internal/rapidgen/rapidgenerators.go +++ b/internal/rapidgen/rapidgenerators.go @@ -20,8 +20,9 @@ package rapidgen import ( "fmt" - "pgregory.net/rapid" "strings" + + "pgregory.net/rapid" ) // DomainWithPort generates an RFC 1035 compliant domain name with a port. diff --git a/ledger/acctdeltas_test.go b/ledger/acctdeltas_test.go index 1e685e3ca8..9a22ebc17f 100644 --- a/ledger/acctdeltas_test.go +++ b/ledger/acctdeltas_test.go @@ -36,6 +36,7 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/avm-abi/apps" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/config/bounds" "github.com/algorand/go-algorand/crypto" @@ -3674,3 +3675,700 @@ func TestOnlineAccountsSuspended(t *testing.T) { require.NoError(t, err) require.Len(t, updated, 0) } + +// TestLookupAssetResourcesWithDeltas verifies that lookupAssetResources properly merges +// in-memory deltas with database results to return current-round data. +// It commits resources to DB, then adds uncommitted delta modifications across two rounds, +// and checks the merged view covers: new creations, holding deletions, holding modifications, +// params-only modifications, params deletions with holding retained, and multi-round +// backwards walking that picks the most recent delta. +func TestLookupAssetResourcesWithDeltas(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testProtocolVersion := protocol.ConsensusCurrentVersion + protoParams := config.Consensus[testProtocolVersion] + + accts := setupAccts(5) + + var creatorAddr, optinAddr basics.Address + for addr := range accts[0] { + if addr != testSinkAddr && addr != testPoolAddr { + if creatorAddr.IsZero() { + creatorAddr = addr + } else { + optinAddr = addr + break + } + } + } + + ml := makeMockLedgerForTracker(t, true, 1, testProtocolVersion, accts) + defer ml.Close() + + conf := config.GetDefaultLocal() + // Zero lookback so all committed rounds flush immediately, leaving deltas[0] + // as the first uncommitted round. This is critical for testing that the delta + // loop visits index 0. + conf.MaxAcctLookback = 0 + au, _ := newAcctUpdates(t, ml, conf) + + knownCreatables := make(map[basics.CreatableIndex]bool) + + // Round 1: create assets 1000-1005 with params and holdings + // 1000: will have holding modified, then overridden in a second delta round + // 1001: will have holding deleted in delta (params remain since account is creator) + // 1002: will remain unchanged + // 1003: will have params-only modification in delta + // 1004: will have params deleted in delta (holding remains) + // 1005: will have both holding and params deleted in delta + // 1007: will be fully deleted in a committed round; optinAddr opts in with zero balance + { + var updates ledgercore.AccountDeltas + updates.Upsert(creatorAddr, ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1_000_000}, + TotalAssetParams: 7, + TotalAssets: 7, + }, + }) + for assetIdx := uint64(1000); assetIdx <= 1005; assetIdx++ { + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(assetIdx), + ledgercore.AssetParamsDelta{ + Params: &basics.AssetParams{ + Total: assetIdx * 1000, + UnitName: fmt.Sprintf("A%d", assetIdx), + AssetName: fmt.Sprintf("Asset%d", assetIdx), + }, + }, + ledgercore.AssetHoldingDelta{ + Holding: &basics.AssetHolding{Amount: assetIdx * 100}, + }) + } + + // 1007: creatorAddr is creator; optinAddr opts in with zero balance and asset will be deleted + // in a committed round to test that surviving holdings of deleted assets are returned. + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1007), + ledgercore.AssetParamsDelta{ + Params: &basics.AssetParams{ + Total: 7000, + UnitName: "A1007", + AssetName: "Asset1007", + }, + }, + ledgercore.AssetHoldingDelta{ + Holding: &basics.AssetHolding{Amount: 7000}, + }) + updates.Upsert(optinAddr, ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1_000_000}, + TotalAssets: 1, + }, + }) + updates.UpsertAssetResource(optinAddr, basics.AssetIndex(1007), + ledgercore.AssetParamsDelta{}, + ledgercore.AssetHoldingDelta{Holding: &basics.AssetHolding{Amount: 0}}) + + base := accts[0] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, 1, au, base, opts, nil) + auCommitSync(t, 1, au, ml) + + for assetIdx := uint64(1000); assetIdx <= 1005; assetIdx++ { + knownCreatables[basics.CreatableIndex(assetIdx)] = true + } + knownCreatables[basics.CreatableIndex(1007)] = true + } + + // Round 2 destroys asset 1007; optinAddr's zero holding survives in DB. + // Additional empty rounds (if any) ensure earlier data flushes past MaxAcctLookback. + for i := basics.Round(2); i <= basics.Round(conf.MaxAcctLookback+2); i++ { + var updates ledgercore.AccountDeltas + if i == 2 { + updates.Upsert(creatorAddr, ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1_000_000}, + TotalAssetParams: 6, + TotalAssets: 6, + }, + }) + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1007), + ledgercore.AssetParamsDelta{Deleted: true}, + ledgercore.AssetHoldingDelta{Deleted: true}) + } + base := accts[i-1] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, i, au, base, opts, nil) + auCommitSync(t, i, au, ml) + } + + // Delta round 1 (uncommitted) + deltaRound1 := basics.Round(conf.MaxAcctLookback + 3) + { + var updates ledgercore.AccountDeltas + // 1005: delete both holding and params + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1005), + ledgercore.AssetParamsDelta{Deleted: true}, + ledgercore.AssetHoldingDelta{Deleted: true}) + // 1006: new creation (not in DB) + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1006), + ledgercore.AssetParamsDelta{ + Params: &basics.AssetParams{Total: 6000, UnitName: "A1006"}, + }, + ledgercore.AssetHoldingDelta{ + Holding: &basics.AssetHolding{Amount: 6000}, + }) + // 1001: delete holding + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1001), + ledgercore.AssetParamsDelta{}, + ledgercore.AssetHoldingDelta{Deleted: true}) + // 1000: modify holding (will be overridden by delta round 2) + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1000), + ledgercore.AssetParamsDelta{}, + ledgercore.AssetHoldingDelta{ + Holding: &basics.AssetHolding{Amount: 9999}, + }) + // 1003: modify params only (holding unchanged) + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1003), + ledgercore.AssetParamsDelta{ + Params: &basics.AssetParams{Total: 7777, UnitName: "A1003new"}, + }, + ledgercore.AssetHoldingDelta{}) + // 1004: delete params (holding remains) + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1004), + ledgercore.AssetParamsDelta{Deleted: true}, + ledgercore.AssetHoldingDelta{}) + + base := accts[deltaRound1-1] + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, deltaRound1, au, base, opts, nil) + } + + // Delta round 2 (uncommitted): override 1000's holding from round 1 + deltaRound2 := deltaRound1 + 1 + { + var updates ledgercore.AccountDeltas + updates.UpsertAssetResource(creatorAddr, basics.AssetIndex(1000), + ledgercore.AssetParamsDelta{}, + ledgercore.AssetHoldingDelta{ + Holding: &basics.AssetHolding{Amount: 5555}, + }) + + base := accts[deltaRound1-1] + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, deltaRound2, au, base, opts, nil) + } + + resources, rnd, err := au.LookupAssetResources(creatorAddr, 0, 100) + require.NoError(t, err) + require.Equal(t, deltaRound2, rnd) + + // Expected: 1000, 1001, 1002, 1003, 1004, 1006. + // 1005 fully deleted (both holding and params) — should not appear. + require.Len(t, resources, 6) + + assetMap := make(map[basics.AssetIndex]ledgercore.AssetResourceWithIDs) + for _, res := range resources { + assetMap[res.AssetID] = res + } + + // 1000: holding from delta round 2 (most recent), params preserved from DB + require.Equal(t, uint64(5555), assetMap[basics.AssetIndex(1000)].AssetHolding.Amount) + require.NotNil(t, assetMap[basics.AssetIndex(1000)].AssetParams) + require.Equal(t, uint64(1_000_000), assetMap[basics.AssetIndex(1000)].AssetParams.Total) + + // 1001: holding deleted but params remain (account is still creator) + require.Contains(t, assetMap, basics.AssetIndex(1001)) + require.Nil(t, assetMap[basics.AssetIndex(1001)].AssetHolding) + require.NotNil(t, assetMap[basics.AssetIndex(1001)].AssetParams) + require.Equal(t, uint64(1_001_000), assetMap[basics.AssetIndex(1001)].AssetParams.Total) + + // 1002: unchanged from DB + require.Equal(t, uint64(1002*100), assetMap[basics.AssetIndex(1002)].AssetHolding.Amount) + require.NotNil(t, assetMap[basics.AssetIndex(1002)].AssetParams) + require.Equal(t, uint64(1_002_000), assetMap[basics.AssetIndex(1002)].AssetParams.Total) + + // 1003: params updated in delta, holding preserved from DB + require.Equal(t, uint64(1003*100), assetMap[basics.AssetIndex(1003)].AssetHolding.Amount) + require.NotNil(t, assetMap[basics.AssetIndex(1003)].AssetParams) + require.Equal(t, uint64(7777), assetMap[basics.AssetIndex(1003)].AssetParams.Total) + require.Equal(t, "A1003new", assetMap[basics.AssetIndex(1003)].AssetParams.UnitName) + + // 1004: params deleted in delta, holding preserved from DB, no creator + require.Equal(t, uint64(1004*100), assetMap[basics.AssetIndex(1004)].AssetHolding.Amount) + require.Nil(t, assetMap[basics.AssetIndex(1004)].AssetParams) + require.True(t, assetMap[basics.AssetIndex(1004)].Creator.IsZero()) + + // 1005: both holding and params deleted — should not appear + require.NotContains(t, assetMap, basics.AssetIndex(1005)) + + // 1006: new creation from delta + require.Equal(t, uint64(6000), assetMap[basics.AssetIndex(1006)].AssetHolding.Amount) + require.NotNil(t, assetMap[basics.AssetIndex(1006)].AssetParams) + require.Equal(t, uint64(6000), assetMap[basics.AssetIndex(1006)].AssetParams.Total) + + // optinAddr opted in to asset 1007 with a zero balance before it was destroyed. The protocol + // does not track all opt-outs when an asset is deleted (only the creator's holding is + // enforced), so optinAddr's holding persists in the DB even after the asset params are gone. + // The lookup must return a non-nil holding with zero amount and no params or creator. + optinAddrResources, optinAddrRnd, err := au.LookupAssetResources(optinAddr, 0, 100) + require.NoError(t, err) + require.Equal(t, deltaRound2, optinAddrRnd) + require.Len(t, optinAddrResources, 1) + require.Equal(t, basics.AssetIndex(1007), optinAddrResources[0].AssetID) + require.NotNil(t, optinAddrResources[0].AssetHolding) + require.Equal(t, basics.AssetHolding{}, *optinAddrResources[0].AssetHolding) + require.Nil(t, optinAddrResources[0].AssetParams) + require.True(t, optinAddrResources[0].Creator.IsZero()) +} + +// TestLookupApplicationResourcesWithDeltas verifies that lookupApplicationResources properly +// merges in-memory deltas with database results to return current-round data. +// It covers: new creation, local state deletion, local state modification, params-only +// modification, params deletion with local state retained, multi-round backwards walking, +// and the includeParams flag. +func TestLookupApplicationResourcesWithDeltas(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testProtocolVersion := protocol.ConsensusCurrentVersion + protoParams := config.Consensus[testProtocolVersion] + + accts := setupAccts(5) + + var testAddr basics.Address + for addr := range accts[0] { + if addr != testSinkAddr && addr != testPoolAddr { + testAddr = addr + break + } + } + + ml := makeMockLedgerForTracker(t, true, 1, testProtocolVersion, accts) + defer ml.Close() + + conf := config.GetDefaultLocal() + // Zero lookback so all committed rounds flush immediately, leaving deltas[0] + // as the first uncommitted round. This is critical for testing that the delta + // loop visits index 0. + conf.MaxAcctLookback = 0 + au, _ := newAcctUpdates(t, ml, conf) + + knownCreatables := make(map[basics.CreatableIndex]bool) + + // Round 1: create apps 2000-2005 with params and local state + // 2000: will have local state modified, then overridden in a second delta round + // 2001: will have local state deleted in delta (params remain since account is creator) + // 2002: will remain unchanged + // 2003: will have params-only modification in delta + // 2004: will have params deleted in delta (local state remains) + // 2005: will have both local state and params deleted in delta + { + var updates ledgercore.AccountDeltas + updates.Upsert(testAddr, ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1_000_000}, + TotalAppParams: 6, + TotalAppLocalStates: 6, + }, + }) + for appIdx := uint64(2000); appIdx <= 2005; appIdx++ { + updates.UpsertAppResource(testAddr, basics.AppIndex(appIdx), + ledgercore.AppParamsDelta{ + Params: &basics.AppParams{ + ApprovalProgram: []byte{0x06, 0x81, 0x01}, + GlobalState: basics.TealKeyValue{}, + }, + }, + ledgercore.AppLocalStateDelta{ + LocalState: &basics.AppLocalState{ + Schema: basics.StateSchema{NumUint: appIdx - 2000}, + }, + }) + } + + base := accts[0] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, 1, au, base, opts, nil) + auCommitSync(t, 1, au, ml) + + for appIdx := uint64(2000); appIdx <= 2005; appIdx++ { + knownCreatables[basics.CreatableIndex(appIdx)] = true + } + } + + // Additional empty rounds (if any) ensure earlier data flushes past MaxAcctLookback into DB. + for i := basics.Round(2); i <= basics.Round(conf.MaxAcctLookback+2); i++ { + var updates ledgercore.AccountDeltas + base := accts[i-1] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, i, au, base, opts, nil) + auCommitSync(t, i, au, ml) + } + + // Delta round 1 (uncommitted) + deltaRound1 := basics.Round(conf.MaxAcctLookback + 3) + { + var updates ledgercore.AccountDeltas + // 2005: delete both local state and params + updates.UpsertAppResource(testAddr, basics.AppIndex(2005), + ledgercore.AppParamsDelta{Deleted: true}, + ledgercore.AppLocalStateDelta{Deleted: true}) + // 2006: new creation (not in DB) + updates.UpsertAppResource(testAddr, basics.AppIndex(2006), + ledgercore.AppParamsDelta{ + Params: &basics.AppParams{ApprovalProgram: []byte{0x06, 0x81, 0x01}}, + }, + ledgercore.AppLocalStateDelta{ + LocalState: &basics.AppLocalState{ + Schema: basics.StateSchema{NumUint: 60}, + }, + }) + // 2001: delete local state + updates.UpsertAppResource(testAddr, basics.AppIndex(2001), + ledgercore.AppParamsDelta{}, + ledgercore.AppLocalStateDelta{Deleted: true}) + // 2000: modify local state (will be overridden by delta round 2) + updates.UpsertAppResource(testAddr, basics.AppIndex(2000), + ledgercore.AppParamsDelta{}, + ledgercore.AppLocalStateDelta{ + LocalState: &basics.AppLocalState{ + Schema: basics.StateSchema{NumUint: 99}, + }, + }) + // 2003: modify params only (local state unchanged) + updates.UpsertAppResource(testAddr, basics.AppIndex(2003), + ledgercore.AppParamsDelta{ + Params: &basics.AppParams{ + ApprovalProgram: []byte{0x06, 0x81, 0x02}, + ClearStateProgram: []byte{0x06, 0x81, 0x01}, + }, + }, + ledgercore.AppLocalStateDelta{}) + // 2004: delete params (local state remains) + updates.UpsertAppResource(testAddr, basics.AppIndex(2004), + ledgercore.AppParamsDelta{Deleted: true}, + ledgercore.AppLocalStateDelta{}) + + base := accts[deltaRound1-1] + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, deltaRound1, au, base, opts, nil) + } + + // Delta round 2 (uncommitted): override 2000's local state from round 1 + deltaRound2 := deltaRound1 + 1 + { + var updates ledgercore.AccountDeltas + updates.UpsertAppResource(testAddr, basics.AppIndex(2000), + ledgercore.AppParamsDelta{}, + ledgercore.AppLocalStateDelta{ + LocalState: &basics.AppLocalState{ + Schema: basics.StateSchema{NumUint: 42}, + }, + }) + + base := accts[deltaRound1-1] + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, deltaRound2, au, base, opts, nil) + } + + // includeParams=true + resources, rnd, err := au.LookupApplicationResources(testAddr, 0, 100, true) + require.NoError(t, err) + require.Equal(t, deltaRound2, rnd) + + // Expected: 2000, 2001, 2002, 2003, 2004, 2006. + // 2005 fully deleted (both local state and params) — should not appear. + require.Len(t, resources, 6) + + appMap := make(map[basics.AppIndex]ledgercore.AppResourceWithIDs) + for _, res := range resources { + appMap[res.AppID] = res + } + + // 2000: local state from delta round 2 (most recent), params preserved from DB + require.Equal(t, uint64(42), appMap[basics.AppIndex(2000)].AppLocalState.Schema.NumUint) + require.NotNil(t, appMap[basics.AppIndex(2000)].AppParams) + require.Equal(t, []byte{0x06, 0x81, 0x01}, appMap[basics.AppIndex(2000)].AppParams.ApprovalProgram) + + // 2001: local state deleted but params remain (account is still creator) + require.Contains(t, appMap, basics.AppIndex(2001)) + require.Nil(t, appMap[basics.AppIndex(2001)].AppLocalState) + require.NotNil(t, appMap[basics.AppIndex(2001)].AppParams) + require.Equal(t, []byte{0x06, 0x81, 0x01}, appMap[basics.AppIndex(2001)].AppParams.ApprovalProgram) + + // 2002: unchanged from DB + require.Equal(t, uint64(2), appMap[basics.AppIndex(2002)].AppLocalState.Schema.NumUint) + require.NotNil(t, appMap[basics.AppIndex(2002)].AppParams) + + // 2003: params updated in delta, local state preserved from DB + require.Equal(t, uint64(3), appMap[basics.AppIndex(2003)].AppLocalState.Schema.NumUint) + require.NotNil(t, appMap[basics.AppIndex(2003)].AppParams) + require.Equal(t, []byte{0x06, 0x81, 0x02}, appMap[basics.AppIndex(2003)].AppParams.ApprovalProgram) + require.Equal(t, []byte{0x06, 0x81, 0x01}, appMap[basics.AppIndex(2003)].AppParams.ClearStateProgram) + + // 2004: params deleted in delta, local state preserved from DB, no creator + require.Equal(t, uint64(4), appMap[basics.AppIndex(2004)].AppLocalState.Schema.NumUint) + require.Nil(t, appMap[basics.AppIndex(2004)].AppParams) + require.True(t, appMap[basics.AppIndex(2004)].Creator.IsZero()) + + // 2005: both local state and params deleted — should not appear + require.NotContains(t, appMap, basics.AppIndex(2005)) + + // 2006: new creation from delta + require.Equal(t, uint64(60), appMap[basics.AppIndex(2006)].AppLocalState.Schema.NumUint) + require.NotNil(t, appMap[basics.AppIndex(2006)].AppParams) + + // includeParams=false should omit AppParams from all results + resourcesNoParams, _, err := au.LookupApplicationResources(testAddr, 0, 100, false) + require.NoError(t, err) + require.Len(t, resourcesNoParams, 5) + + for _, res := range resourcesNoParams { + require.Nil(t, res.AppParams, "AppParams should be nil when includeParams=false") + } +} + +// TestLookupAppResourcesParamsOnlyDeletion exercises the scenario where an app +// creator has params but no local state in the DB (never opted in) and a delta +// deletes those params. Two bugs interact here: +// +// 1. The else fallback in the DB merge calls GetAppLocalState() without first +// checking pd.Data.IsHolding(), producing a phantom zero-value AppLocalState +// on a row that has no local state. This causes the deleted-params row to +// survive the (AppLocalState != nil || AppParams != nil) filter when it +// should be excluded. +// +// 2. numDeltaDeleted only counts State.Deleted. A params-only deletion +// (Params.Deleted=true, State.Deleted=false) is not counted, so the DB +// over-request is too small and the result page is short once the phantom +// local state issue is also fixed. +// +// Setup: 4 apps (3000-3003) committed to DB. App 3000 has params only (no +// local state). Apps 3001-3003 have both params and local state. +// Delta: delete params for app 3000. +// Query with limit=3: should return 3001, 3002, 3003. +func TestLookupAppResourcesParamsOnlyDeletion(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testProtocolVersion := protocol.ConsensusCurrentVersion + protoParams := config.Consensus[testProtocolVersion] + + accts := setupAccts(5) + + var creatorAddr basics.Address + for addr := range accts[0] { + if addr != testSinkAddr && addr != testPoolAddr { + creatorAddr = addr + break + } + } + + ml := makeMockLedgerForTracker(t, true, 1, testProtocolVersion, accts) + defer ml.Close() + + conf := config.GetDefaultLocal() + conf.MaxAcctLookback = 0 + au, _ := newAcctUpdates(t, ml, conf) + + knownCreatables := make(map[basics.CreatableIndex]bool) + + // Round 1: create apps 3000-3003. + // 3000: params only, no local state (creator who never opted in) + // 3001-3003: both params and local state + { + var updates ledgercore.AccountDeltas + updates.Upsert(creatorAddr, ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1_000_000}, + TotalAppParams: 4, + TotalAppLocalStates: 3, + }, + }) + // 3000: params only + updates.UpsertAppResource(creatorAddr, basics.AppIndex(3000), + ledgercore.AppParamsDelta{ + Params: &basics.AppParams{ + ApprovalProgram: []byte{0x06, 0x81, 0x01}, + }, + }, + ledgercore.AppLocalStateDelta{}) + // 3001-3003: params + local state + for appIdx := uint64(3001); appIdx <= 3003; appIdx++ { + updates.UpsertAppResource(creatorAddr, basics.AppIndex(appIdx), + ledgercore.AppParamsDelta{ + Params: &basics.AppParams{ + ApprovalProgram: []byte{0x06, 0x81, 0x01}, + }, + }, + ledgercore.AppLocalStateDelta{ + LocalState: &basics.AppLocalState{ + Schema: basics.StateSchema{NumUint: appIdx - 3000}, + }, + }) + } + + base := accts[0] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, 1, au, base, opts, nil) + auCommitSync(t, 1, au, ml) + + for appIdx := uint64(3000); appIdx <= 3003; appIdx++ { + knownCreatables[basics.CreatableIndex(appIdx)] = true + } + } + + // Flush past MaxAcctLookback + for i := basics.Round(2); i <= basics.Round(conf.MaxAcctLookback+2); i++ { + var updates ledgercore.AccountDeltas + base := accts[i-1] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, i, au, base, opts, nil) + auCommitSync(t, i, au, ml) + } + + // Delta (uncommitted): delete params for app 3000 (which has no local state). + // State.Deleted is false because there was no local state to delete. + deltaRound := basics.Round(conf.MaxAcctLookback + 3) + { + var updates ledgercore.AccountDeltas + updates.UpsertAppResource(creatorAddr, basics.AppIndex(3000), + ledgercore.AppParamsDelta{Deleted: true}, + ledgercore.AppLocalStateDelta{}) + + base := accts[deltaRound-1] + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, deltaRound, au, base, opts, nil) + } + + // Query with limit=3. After filtering out 3000 (both params and local state + // nil), we should still get 3 results: 3001, 3002, 3003. + resources, rnd, err := au.LookupApplicationResources(creatorAddr, 0, 3, true) + require.NoError(t, err) + require.Equal(t, deltaRound, rnd) + require.Len(t, resources, 3, "params-only deletion should not cause a short page") + require.Equal(t, basics.AppIndex(3001), resources[0].AppID) + require.Equal(t, basics.AppIndex(3002), resources[1].AppID) + require.Equal(t, basics.AppIndex(3003), resources[2].AppID) +} + +// TestLookupAssetResourcesEmptyPageDoesNotError verifies that looking up an empty page +// (no resources for the account) returns an empty result and current round, not an error. +func TestLookupAssetResourcesEmptyPageDoesNotError(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testProtocolVersion := protocol.ConsensusCurrentVersion + protoParams := config.Consensus[testProtocolVersion] + + accts := setupAccts(2) + ml := makeMockLedgerForTracker(t, true, 1, testProtocolVersion, accts) + defer ml.Close() + + // Step 1: use default lookback config (do not override MaxAcctLookback). + // We then advance enough rounds so persisted DB round moves forward. + conf := config.GetDefaultLocal() + au, _ := newAcctUpdates(t, ml, conf) + + knownCreatables := make(map[basics.CreatableIndex]bool) + latestRound := basics.Round(conf.MaxAcctLookback + 2) + + // Step 2: commit empty rounds to push DB round forward while keeping + // the target address absent from all in-memory and persisted resources. + for i := basics.Round(1); i <= latestRound; i++ { + var updates ledgercore.AccountDeltas + base := accts[i-1] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, i, au, base, opts, nil) + auCommitSync(t, i, au, ml) + } + + // Step 3: choose an address that definitely does not exist in the fixture accounts. + var missingAddr basics.Address + missingAddr[0] = 0xA5 + for { + if _, ok := accts[0][missingAddr]; !ok { + break + } + missingAddr[0]++ + } + + // Step 4: lookup should produce an empty page at current round, not an error. + resources, rnd, err := au.LookupAssetResources(missingAddr, 0, 10) + require.NoError(t, err) + require.Equal(t, latestRound, rnd) + require.Len(t, resources, 0) +} + +// TestLookupApplicationResourcesEmptyPageDoesNotError verifies that looking up an empty page +// (no resources for the account) returns an empty result and current round, not an error. +func TestLookupApplicationResourcesEmptyPageDoesNotError(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testProtocolVersion := protocol.ConsensusCurrentVersion + protoParams := config.Consensus[testProtocolVersion] + + accts := setupAccts(2) + ml := makeMockLedgerForTracker(t, true, 1, testProtocolVersion, accts) + defer ml.Close() + + // Step 1: use default lookback config (do not override MaxAcctLookback). + // We then advance enough rounds so persisted DB round moves forward. + conf := config.GetDefaultLocal() + au, _ := newAcctUpdates(t, ml, conf) + + knownCreatables := make(map[basics.CreatableIndex]bool) + latestRound := basics.Round(conf.MaxAcctLookback + 2) + + // Step 2: commit empty rounds to push DB round forward while keeping + // the target address absent from all in-memory and persisted resources. + for i := basics.Round(1); i <= latestRound; i++ { + var updates ledgercore.AccountDeltas + base := accts[i-1] + newAccts := applyPartialDeltas(base, updates) + accts = append(accts, newAccts) + + opts := auNewBlockOpts{updates, testProtocolVersion, protoParams, knownCreatables} + auNewBlock(t, i, au, base, opts, nil) + auCommitSync(t, i, au, ml) + } + + // Step 3: choose an address that definitely does not exist in the fixture accounts. + var missingAddr basics.Address + missingAddr[0] = 0x5A + for { + if _, ok := accts[0][missingAddr]; !ok { + break + } + missingAddr[0]++ + } + + // Step 4: lookup should produce an empty page at current round, not an error. + resources, rnd, err := au.LookupApplicationResources(missingAddr, 0, 10, true) + require.NoError(t, err) + require.Equal(t, latestRound, rnd) + require.Len(t, resources, 0) +} diff --git a/ledger/acctonline_expired_test.go b/ledger/acctonline_expired_test.go index c35667316a..c748fa0e69 100644 --- a/ledger/acctonline_expired_test.go +++ b/ledger/acctonline_expired_test.go @@ -23,6 +23,8 @@ import ( "strconv" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" @@ -32,7 +34,6 @@ import ( 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" ) // onlineAcctModel provides a simple interface for tracking accounts diff --git a/ledger/acctonline_test.go b/ledger/acctonline_test.go index 4327f2ae24..4d4008668e 100644 --- a/ledger/acctonline_test.go +++ b/ledger/acctonline_test.go @@ -25,6 +25,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" @@ -35,7 +37,6 @@ import ( 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 commitSync(t *testing.T, oa *onlineAccounts, ml *mockLedgerForTracker, rnd basics.Round) { diff --git a/ledger/acctonlineexp.go b/ledger/acctonlineexp.go index 0a772e6b9b..c7f62dcf9e 100644 --- a/ledger/acctonlineexp.go +++ b/ledger/acctonlineexp.go @@ -17,8 +17,9 @@ package ledger import ( - "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/data/basics" ) type expiredCirculationCache struct { diff --git a/ledger/acctonlineexp_test.go b/ledger/acctonlineexp_test.go index 3f5b943b4e..e124cd2bf4 100644 --- a/ledger/acctonlineexp_test.go +++ b/ledger/acctonlineexp_test.go @@ -19,9 +19,10 @@ package ledger import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestAcctOnline_ExpiredCirculationCacheBasic(t *testing.T) { diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 64257813b9..d6b39621bf 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -17,10 +17,12 @@ package ledger import ( + "cmp" "context" "errors" "fmt" "io" + "slices" "sort" "strings" "sync" @@ -345,6 +347,10 @@ func (au *accountUpdates) LookupAssetResources(addr basics.Address, assetIDGT ba return au.lookupAssetResources(addr, assetIDGT, limit) } +func (au *accountUpdates) LookupApplicationResources(addr basics.Address, appIDGT basics.AppIndex, limit uint64, includeParams bool) ([]ledgercore.AppResourceWithIDs, basics.Round, error) { + return au.lookupApplicationResources(addr, appIDGT, limit, includeParams) +} + func (au *accountUpdates) LookupKv(rnd basics.Round, key string) ([]byte, error) { return au.lookupKv(rnd, key, true /* take lock */) } @@ -1165,9 +1171,9 @@ func (au *accountUpdates) lookupResource(rnd basics.Round, addr basics.Address, return macct.AccountResource(), rnd, nil } - // check baseAccoiunts again to see if it does not exist + // check baseResources again to see if it does not exist if au.baseResources.readNotFound(addr, aidx) { - // it seems the account doesnt exist + // it seems the resource doesn't exist return ledgercore.AccountResource{}, rnd, nil } @@ -1212,53 +1218,301 @@ func (au *accountUpdates) lookupResource(rnd basics.Round, addr basics.Address, } } -// lookupAssetResources returns all the resources for a given address, solely based on what is persisted to disk. It does not -// take into account any in-memory deltas; the round number returned is the latest round number that is known to the database. -func (au *accountUpdates) lookupAssetResources(addr basics.Address, assetIDGT basics.AssetIndex, limit uint64) (data []ledgercore.AssetResourceWithIDs, validThrough basics.Round, err error) { - // Look for resources on disk - persistedResources, resourceDbRound, err0 := au.accountsq.LookupLimitedResources(addr, basics.CreatableIndex(assetIDGT), limit, basics.AssetCreatable) - if err0 != nil { - return nil, basics.Round(0), err0 +// lookupAssetResources returns all the asset resources for a given address. +// It merges in-memory deltas with persisted data to provide current-round information. +func (au *accountUpdates) lookupAssetResources(addr basics.Address, assetIDGT basics.AssetIndex, limit uint64) ([]ledgercore.AssetResourceWithIDs, basics.Round, error) { + needUnlock := true + au.accountsMu.RLock() + defer func() { + if needUnlock { + au.accountsMu.RUnlock() + } + }() + + if limit == 0 { + return nil, au.cachedDBRound + basics.Round(len(au.deltas)), nil } - data = make([]ledgercore.AssetResourceWithIDs, 0, len(persistedResources)) - for _, pd := range persistedResources { - ah := pd.Data.GetAssetHolding() + for { + currentDBRound := au.cachedDBRound + currentDeltaLen := len(au.deltas) - var arwi ledgercore.AssetResourceWithIDs - if !pd.Creator.IsZero() { - ap := pd.Data.GetAssetParams() + // Walk deltas backwards; the first entry found for a given asset is the most recent. + deltaResults := make(map[basics.AssetIndex]ledgercore.AssetResourceRecord) + numDeltaDeleted := 0 - arwi = ledgercore.AssetResourceWithIDs{ - AssetID: basics.AssetIndex(pd.Aidx), - Creator: pd.Creator, + for i := currentDeltaLen; i > 0; { + i-- + for _, rec := range au.deltas[i].Accts.AssetResources { + if rec.Addr != addr || rec.Aidx <= assetIDGT { + continue + } + if _, ok := deltaResults[rec.Aidx]; ok { + continue + } + deltaResults[rec.Aidx] = rec + if rec.Holding.Deleted || rec.Params.Deleted { + numDeltaDeleted++ + } + } + } + + retRound := currentDBRound + basics.Round(currentDeltaLen) + + au.accountsMu.RUnlock() + needUnlock = false - AssetResource: ledgercore.AssetResource{ - AssetHolding: &ah, - AssetParams: &ap, - }, + // Over-request from DB to compensate for delta deletions that remove DB rows + // from the result set. Deletions are the only delta entries that shrink the + // page — modifications and new creations cannot reduce the DB contribution. + dbLimit := limit + uint64(numDeltaDeleted) + + persistedResources, resourceDbRound, err := au.accountsq.LookupLimitedResources(addr, basics.CreatableIndex(assetIDGT), dbLimit, basics.AssetCreatable) + if err != nil { + return nil, 0, err + } + + if resourceDbRound == currentDBRound || (len(persistedResources) == 0 && resourceDbRound == 0) { + seenInDB := make(map[basics.AssetIndex]bool, len(persistedResources)) + result := make([]ledgercore.AssetResourceWithIDs, 0, limit) + + // Determine the upper bound of the DB page so we only add delta entries + // within range and don't accidentally set a next-token that skips items. + var dbHasMore bool + var dbMaxID basics.AssetIndex + if len(persistedResources) > 0 { + dbMaxID = basics.AssetIndex(persistedResources[len(persistedResources)-1].Aidx) + dbHasMore = uint64(len(persistedResources)) == dbLimit } - } else { - arwi = ledgercore.AssetResourceWithIDs{ - AssetID: basics.AssetIndex(pd.Aidx), - AssetResource: ledgercore.AssetResource{ - AssetHolding: &ah, - }, + for _, pd := range persistedResources { + assetID := basics.AssetIndex(pd.Aidx) + seenInDB[assetID] = true + + d, inDelta := deltaResults[assetID] + + arwi := ledgercore.AssetResourceWithIDs{AssetID: assetID} + + if inDelta && d.Holding.Deleted { + // Holding removed by delta — leave AssetHolding nil. + } else if inDelta && d.Holding.Holding != nil { + arwi.AssetHolding = d.Holding.Holding + } else if pd.Data.IsHolding() { + ah := pd.Data.GetAssetHolding() + arwi.AssetHolding = &ah + } + + if inDelta && d.Params.Deleted { + // Delta deleted params — omit creator and params. + } else if inDelta && d.Params.Params != nil { + arwi.Creator = pd.Creator + arwi.AssetParams = d.Params.Params + } else if !pd.Creator.IsZero() { + arwi.Creator = pd.Creator + ap := pd.Data.GetAssetParams() + arwi.AssetParams = &ap + } + + if arwi.AssetHolding != nil || arwi.AssetParams != nil { + result = append(result, arwi) + } } + + // Add assets that exist only in deltas (new creations not yet in DB). + // Only include delta entries within the DB page range to avoid setting + // a next-token that would skip items still in the database. + for assetID, d := range deltaResults { + if seenInDB[assetID] { + continue + } + if dbHasMore && assetID > dbMaxID { + continue + } + arwi := ledgercore.AssetResourceWithIDs{AssetID: assetID} + if !d.Holding.Deleted && d.Holding.Holding != nil { + arwi.AssetHolding = d.Holding.Holding + } + if !d.Params.Deleted && d.Params.Params != nil { + arwi.Creator = addr + arwi.AssetParams = d.Params.Params + } + if arwi.AssetHolding != nil || arwi.AssetParams != nil { + result = append(result, arwi) + } + } + + slices.SortFunc(result, func(a, b ledgercore.AssetResourceWithIDs) int { + return cmp.Compare(a.AssetID, b.AssetID) + }) + if uint64(len(result)) > limit { + result = result[:limit] + } + + return result, retRound, nil } - data = append(data, arwi) - } - // We've found all the resources we could find for this address. - currentDbRound := resourceDbRound - // The resourceDbRound will not be set if there are no persisted resources - if len(data) == 0 { + if resourceDbRound < currentDBRound { + au.log.Errorf("accountUpdates.lookupAssetResources: database round %d is behind in-memory round %d", resourceDbRound, currentDBRound) + return nil, 0, &StaleDatabaseRoundError{databaseRound: resourceDbRound, memoryRound: currentDBRound} + } au.accountsMu.RLock() - currentDbRound = au.cachedDBRound + needUnlock = true + for currentDBRound >= au.cachedDBRound && currentDeltaLen == len(au.deltas) { + au.accountsReadCond.Wait() + } + } +} + +// lookupApplicationResources returns all the application resources for a given address. +// It merges in-memory deltas with persisted data to provide current-round information. +// If includeParams is false, AppParams will not be populated to save memory allocations (app params can be ~50KB each). +func (au *accountUpdates) lookupApplicationResources(addr basics.Address, appIDGT basics.AppIndex, limit uint64, includeParams bool) ([]ledgercore.AppResourceWithIDs, basics.Round, error) { + needUnlock := true + au.accountsMu.RLock() + defer func() { + if needUnlock { + au.accountsMu.RUnlock() + } + }() + + if limit == 0 { + return nil, au.cachedDBRound + basics.Round(len(au.deltas)), nil + } + + for { + currentDBRound := au.cachedDBRound + currentDeltaLen := len(au.deltas) + + // Walk deltas backwards; the first entry found for a given app is the most recent. + deltaResults := make(map[basics.AppIndex]ledgercore.AppResourceRecord) + numDeltaDeleted := 0 + + for i := currentDeltaLen; i > 0; { + i-- + for _, rec := range au.deltas[i].Accts.AppResources { + if rec.Addr != addr || rec.Aidx <= appIDGT { + continue + } + if _, ok := deltaResults[rec.Aidx]; ok { + continue + } + deltaResults[rec.Aidx] = rec + if rec.State.Deleted || rec.Params.Deleted { + numDeltaDeleted++ + } + } + } + + retRound := currentDBRound + basics.Round(currentDeltaLen) + au.accountsMu.RUnlock() + needUnlock = false + + // Over-request from DB to compensate for delta deletions that remove DB rows + // from the result set. Deletions are the only delta entries that shrink the + // page — modifications and new creations cannot reduce the DB contribution. + dbLimit := limit + uint64(numDeltaDeleted) + + persistedResources, resourceDbRound, err := au.accountsq.LookupLimitedResources(addr, basics.CreatableIndex(appIDGT), dbLimit, basics.AppCreatable) + if err != nil { + return nil, 0, err + } + + if resourceDbRound == currentDBRound || (len(persistedResources) == 0 && resourceDbRound == 0) { + seenInDB := make(map[basics.AppIndex]bool, len(persistedResources)) + result := make([]ledgercore.AppResourceWithIDs, 0, limit) + + // Determine the upper bound of the DB page so we only add delta entries + // within range and don't accidentally set a next-token that skips items. + var dbHasMore bool + var dbMaxID basics.AppIndex + if len(persistedResources) > 0 { + dbMaxID = basics.AppIndex(persistedResources[len(persistedResources)-1].Aidx) + dbHasMore = uint64(len(persistedResources)) == dbLimit + } + + for _, pd := range persistedResources { + appID := basics.AppIndex(pd.Aidx) + seenInDB[appID] = true + + d, inDelta := deltaResults[appID] + + arwi := ledgercore.AppResourceWithIDs{AppID: appID} + + if inDelta && d.State.Deleted { + // Local state removed by delta — leave AppLocalState nil. + } else if inDelta && d.State.LocalState != nil { + arwi.AppLocalState = d.State.LocalState + } else if pd.Data.IsHolding() { + als := pd.Data.GetAppLocalState() + arwi.AppLocalState = &als + } + + if inDelta && d.Params.Deleted { + // Delta deleted params — omit creator and params. + } else if inDelta && d.Params.Params != nil { + arwi.Creator = pd.Creator + if includeParams { + arwi.AppResource.AppParams = d.Params.Params + } + } else if !pd.Creator.IsZero() { + arwi.Creator = pd.Creator + if includeParams { + ap := pd.Data.GetAppParams() + arwi.AppResource.AppParams = &ap + } + } + + if arwi.AppLocalState != nil || arwi.AppParams != nil { + result = append(result, arwi) + } + } + + // Add apps that exist only in deltas (new opt-ins not yet in DB). + // Only include delta entries within the DB page range to avoid setting + // a next-token that would skip items still in the database. + for appID, d := range deltaResults { + if seenInDB[appID] { + continue + } + if dbHasMore && appID > dbMaxID { + continue + } + arwi := ledgercore.AppResourceWithIDs{AppID: appID} + if !d.State.Deleted && d.State.LocalState != nil { + arwi.AppLocalState = d.State.LocalState + } + if !d.Params.Deleted && d.Params.Params != nil { + arwi.Creator = addr + if includeParams { + arwi.AppResource.AppParams = d.Params.Params + } + } + if arwi.AppLocalState != nil || arwi.AppParams != nil { + result = append(result, arwi) + } + } + + slices.SortFunc(result, func(a, b ledgercore.AppResourceWithIDs) int { + return cmp.Compare(a.AppID, b.AppID) + }) + if uint64(len(result)) > limit { + result = result[:limit] + } + + return result, retRound, nil + } + + if resourceDbRound < currentDBRound { + au.log.Errorf("accountUpdates.lookupApplicationResources: database round %d is behind in-memory round %d", resourceDbRound, currentDBRound) + return nil, 0, &StaleDatabaseRoundError{databaseRound: resourceDbRound, memoryRound: currentDBRound} + } + au.accountsMu.RLock() + needUnlock = true + for currentDBRound >= au.cachedDBRound && currentDeltaLen == len(au.deltas) { + au.accountsReadCond.Wait() + } } - return data, currentDbRound, nil } func (au *accountUpdates) lookupStateDelta(rnd basics.Round) (ledgercore.StateDelta, error) { @@ -1339,9 +1593,9 @@ func (au *accountUpdates) lookupWithoutRewards(rnd basics.Round, addr basics.Add return macct.AccountData.GetLedgerCoreAccountData(), rnd, rewardsVersion, rewardsLevel, nil } - // check baseAccoiunts again to see if it does not exist + // check baseAccounts again to see if it does not exist if au.baseAccounts.readNotFound(addr) { - // it seems the account doesnt exist + // it seems the account doesn't exist return ledgercore.AccountData{}, rnd, rewardsVersion, rewardsLevel, nil } diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 4017a26835..4666bb1cb2 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -32,6 +32,8 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/avm-abi/apps" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" @@ -45,7 +47,6 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/db" - "github.com/algorand/go-deadlock" ) 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} diff --git a/ledger/apply/challenge_test.go b/ledger/apply/challenge_test.go index b0362a846b..15e6e55343 100644 --- a/ledger/apply/challenge_test.go +++ b/ledger/apply/challenge_test.go @@ -19,13 +19,14 @@ package apply import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestBitsMatch(t *testing.T) { diff --git a/ledger/boxtxn_test.go b/ledger/boxtxn_test.go index e1af25a8d9..9e79f66bcf 100644 --- a/ledger/boxtxn_test.go +++ b/ledger/boxtxn_test.go @@ -24,6 +24,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" @@ -33,7 +35,6 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) var boxAppSource = main(` diff --git a/ledger/bulletin_test.go b/ledger/bulletin_test.go index d5ecaa8558..402b959073 100644 --- a/ledger/bulletin_test.go +++ b/ledger/bulletin_test.go @@ -20,9 +20,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) const epsilon = 5 * time.Millisecond diff --git a/ledger/catchpointfilewriter_test.go b/ledger/catchpointfilewriter_test.go index b534995eb7..05874a53aa 100644 --- a/ledger/catchpointfilewriter_test.go +++ b/ledger/catchpointfilewriter_test.go @@ -35,6 +35,8 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/avm-abi/apps" + "github.com/algorand/msgp/msgp" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merkletrie" @@ -48,7 +50,6 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/algorand/msgp/msgp" ) type decodedCatchpointChunkData struct { @@ -848,6 +849,11 @@ func TestFullCatchpointWriter(t *testing.T) { // another approach is to modify the test and craft round numbers, // and make the ledger to generate catchpoint itself when it is time func testCatchpointFlushRound(l *Ledger) (basics.Round, basics.Round) { + // Ensure all blocks are persisted to the block DB before flushing trackers. + // Without this, LatestCommitted() may return a stale round if the blockQueue + // syncer hasn't finished yet. + l.WaitForCommit(l.Latest()) + // Clear the timer to ensure a flush l.trackers.mu.Lock() l.trackers.lastFlushTime = time.Time{} @@ -1035,6 +1041,7 @@ func TestCatchpointAfterTxns(t *testing.T) { catchpointDataFilePath := filepath.Join(tempDir, t.Name()+".data") catchpointFilePath := filepath.Join(tempDir, t.Name()+".catchpoint.tar.gz") + testCatchpointFlushRound(dl.validator) cph := testWriteCatchpoint(t, config.Consensus[proto], dl.validator.trackerDB(), catchpointDataFilePath, catchpointFilePath, 0, 0) require.EqualValues(t, 3, cph.TotalChunks) @@ -1051,6 +1058,7 @@ func TestCatchpointAfterTxns(t *testing.T) { dl.fullBlock(&newacctpay) // Write and read back in, and ensure even the last effect exists. + testCatchpointFlushRound(dl.validator) cph = testWriteCatchpoint(t, config.Consensus[proto], dl.validator.trackerDB(), catchpointDataFilePath, catchpointFilePath, 0, 0) require.EqualValues(t, cph.TotalChunks, 3) // Still only 3 chunks, as last was in a recent block @@ -1067,6 +1075,7 @@ func TestCatchpointAfterTxns(t *testing.T) { dl.fullBlock(pay.Noted(strconv.Itoa(i))) } + testCatchpointFlushRound(dl.validator) cph = testWriteCatchpoint(t, config.Consensus[proto], dl.validator.trackerDB(), catchpointDataFilePath, catchpointFilePath, 0, 0) require.EqualValues(t, cph.TotalChunks, 4) diff --git a/ledger/catchpointtracker.go b/ledger/catchpointtracker.go index d95d299ea6..9cb214efc0 100644 --- a/ledger/catchpointtracker.go +++ b/ledger/catchpointtracker.go @@ -33,9 +33,10 @@ import ( "sync/atomic" "time" - "github.com/algorand/go-deadlock" "github.com/golang/snappy" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merkletrie" diff --git a/ledger/catchupaccessor_test.go b/ledger/catchupaccessor_test.go index 87cb32aed8..77ef32f488 100644 --- a/ledger/catchupaccessor_test.go +++ b/ledger/catchupaccessor_test.go @@ -28,6 +28,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/algorand/msgp/msgp" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -41,7 +43,6 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/db" - "github.com/algorand/msgp/msgp" ) func createTestingEncodedChunks(accountsCount uint64) (encodedAccountChunks [][]byte, last64KIndex int) { diff --git a/ledger/double_test.go b/ledger/double_test.go index 728d4e65c6..73e31fd932 100644 --- a/ledger/double_test.go +++ b/ledger/double_test.go @@ -19,6 +19,8 @@ package ledger import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" @@ -28,7 +30,6 @@ import ( "github.com/algorand/go-algorand/ledger/eval" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) // DoubleLedger allows for easy "Double Entry bookkeeping" as a way to write diff --git a/ledger/encoded/recordsV5.go b/ledger/encoded/recordsV5.go index 12c133c251..91506f5319 100644 --- a/ledger/encoded/recordsV5.go +++ b/ledger/encoded/recordsV5.go @@ -17,8 +17,9 @@ package encoded import ( - "github.com/algorand/go-algorand/data/basics" "github.com/algorand/msgp/msgp" + + "github.com/algorand/go-algorand/data/basics" ) // BalanceRecordV5 is the encoded account balance record. diff --git a/ledger/encoded/recordsV6.go b/ledger/encoded/recordsV6.go index 12df070e64..232ee6d019 100644 --- a/ledger/encoded/recordsV6.go +++ b/ledger/encoded/recordsV6.go @@ -17,9 +17,10 @@ package encoded import ( + "github.com/algorand/msgp/msgp" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/msgp/msgp" ) // Adjust these to be big enough for boxes, but not directly tied to box values. diff --git a/ledger/encoded/recordsV6_test.go b/ledger/encoded/recordsV6_test.go index 42fb6c2e55..9bc973c6f5 100644 --- a/ledger/encoded/recordsV6_test.go +++ b/ledger/encoded/recordsV6_test.go @@ -20,12 +20,14 @@ import ( "math" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/avm-abi/apps" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestEncodedKVRecordV6Allocbounds(t *testing.T) { diff --git a/ledger/eval/applications.go b/ledger/eval/applications.go index 08dd68e587..b0ff12fa31 100644 --- a/ledger/eval/applications.go +++ b/ledger/eval/applications.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/algorand/avm-abi/apps" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger/apply" diff --git a/ledger/eval/eval.go b/ledger/eval/eval.go index 5c44073e0a..4b74f78518 100644 --- a/ledger/eval/eval.go +++ b/ledger/eval/eval.go @@ -129,7 +129,7 @@ type roundCowBase struct { // execution. The AccountData is always an historical one, then therefore won't be changing. // The underlying (accountupdates) infrastructure may provide additional cross-round caching which // are beyond the scope of this cache. - // The account data store here is always the account data without the rewards. + // The account data stored here is always the account data without the rewards. accounts map[basics.Address]ledgercore.AccountData // The online accounts that we've already accessed during this round evaluation. This is a @@ -2018,8 +2018,7 @@ func (validator *evalTxValidator) run() { RewardsPool: validator.block.BlockHeader.RewardsPool, } - var unverifiedTxnGroups [][]transactions.SignedTxn - unverifiedTxnGroups = make([][]transactions.SignedTxn, 0, len(validator.txgroups)) + unverifiedTxnGroups := make([][]transactions.SignedTxn, 0, len(validator.txgroups)) for _, group := range validator.txgroups { signedTxnGroup := make([]transactions.SignedTxn, len(group)) for j, txn := range group { @@ -2076,7 +2075,7 @@ func Eval(ctx context.Context, l LedgerForEvaluator, blk bookkeeping.Block, vali } accountLoadingCtx, accountLoadingCancel := context.WithCancel(ctx) - preloadedTxnsData := prefetcher.PrefetchAccounts(accountLoadingCtx, l, blk.Round()-1, paysetgroups, blk.BlockHeader.FeeSink, blk.ConsensusProtocol()) + preloadedTxnsData := prefetcher.BlockReferences(accountLoadingCtx, l, blk.Round()-1, paysetgroups, blk.BlockHeader.FeeSink, blk.ConsensusProtocol()) // ensure that before we exit from this method, the account loading is no longer active. defer func() { accountLoadingCancel() @@ -2163,6 +2162,11 @@ transactionGroupLoop: } } } + for _, lkv := range txgroup.KVs { + if _, have := base.kvStore[lkv.Key]; !have { + base.kvStore[lkv.Key] = lkv.Value + } + } } err = eval.TransactionGroup(txgroup.TxnGroup...) if err != nil { diff --git a/ledger/eval/prefetcher/prefetcher.go b/ledger/eval/prefetcher/prefetcher.go index 891611ffc6..5696464988 100644 --- a/ledger/eval/prefetcher/prefetcher.go +++ b/ledger/eval/prefetcher/prefetcher.go @@ -18,19 +18,24 @@ package prefetcher import ( "context" + "fmt" + "runtime" "sync/atomic" + "github.com/algorand/avm-abi/apps" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util" ) // asyncAccountLoadingThreadCount controls how many go routines would be used // to load the account data before the Eval() start processing individual // transaction group. -const asyncAccountLoadingThreadCount = 4 +var asyncAccountLoadingThreadCount = min(8, (runtime.NumCPU()+1)/2) // Ledger is a ledger interfaces for prefetcher. type Ledger interface { @@ -38,6 +43,7 @@ type Ledger interface { LookupAsset(basics.Round, basics.Address, basics.AssetIndex) (ledgercore.AssetResource, error) LookupApplication(basics.Round, basics.Address, basics.AppIndex) (ledgercore.AppResource, error) GetCreatorForRound(basics.Round, basics.CreatableIndex, basics.CreatableType) (basics.Address, bool, error) + LookupKv(basics.Round, string) ([]byte, error) } // LoadedAccountDataEntry describes a loaded account. @@ -46,8 +52,8 @@ type LoadedAccountDataEntry struct { Data *ledgercore.AccountData } -// LoadedResourcesEntry describes a loaded resource. -type LoadedResourcesEntry struct { +// LoadedResourceEntry describes a loaded resource. +type LoadedResourceEntry struct { // Resource is the loaded Resource entry. unless address is nil, Resource would always contain a valid ledgercore.AccountResource pointer. Resource *ledgercore.AccountResource // Address might be empty if the resource does not exist. In that case creatableIndex and creatableType would still be valid while resource would be nil. @@ -56,24 +62,33 @@ type LoadedResourcesEntry struct { CreatableType basics.CreatableType } +// LoadedKVEntry describes a loaded kv. +type LoadedKVEntry struct { + Key string + Value []byte +} + // LoadedTransactionGroup is a helper struct to allow asynchronous loading of the account data needed by the transaction groups type LoadedTransactionGroup struct { // the transaction group TxnGroup []transactions.SignedTxnWithAD - // Accounts is a list of all the Accounts balance records that the transaction group refer to and are needed. + // Accounts is a list of all the Account balance records for the transaction group. Accounts []LoadedAccountDataEntry - // the following four are the Resources used by the account - Resources []LoadedResourcesEntry + // Resources is the list of all Resources (apps/assets/hodling/locals) for the transaction group. + Resources []LoadedResourceEntry + + // KVs is the list of all kvs for the transaction group + KVs []LoadedKVEntry // Err indicates whether any of the balances in this structure have failed to load. In case of an error, at least // one of the entries in the balances would be uninitialized. - Err *GroupTaskError + Err error } -// accountPrefetcher used to prefetch accounts balances and resources before the evaluator is being called. -type accountPrefetcher struct { +// paysetPrefetcher used to prefetch accounts balances and resources before the evaluator is called. +type paysetPrefetcher struct { ledger Ledger rnd basics.Round txnGroups [][]transactions.SignedTxnWithAD @@ -82,10 +97,12 @@ type accountPrefetcher struct { outChan chan LoadedTransactionGroup } -// PrefetchAccounts loads the account data for the provided transaction group list. It also loads the feeSink account and add it to the first returned transaction group. -// The order of the transaction groups returned by the channel is identical to the one in the input array. -func PrefetchAccounts(ctx context.Context, l Ledger, rnd basics.Round, txnGroups [][]transactions.SignedTxnWithAD, feeSinkAddr basics.Address, consensusParams config.ConsensusParams) <-chan LoadedTransactionGroup { - prefetcher := &accountPrefetcher{ +// BlockReferences loads the resources for the provided transaction group list. It also +// loads the feeSink account and adds it to the first returned transaction +// group. The order of the transaction groups returned by the channel is +// identical to the one in the input array. +func BlockReferences(ctx context.Context, l Ledger, rnd basics.Round, txnGroups [][]transactions.SignedTxnWithAD, feeSinkAddr basics.Address, consensusParams config.ConsensusParams) <-chan LoadedTransactionGroup { + prefetcher := &paysetPrefetcher{ ledger: l, rnd: rnd, txnGroups: txnGroups, @@ -101,25 +118,32 @@ func PrefetchAccounts(ctx context.Context, l Ledger, rnd basics.Round, txnGroups // groupTask helps to organize the account loading for each transaction group. type groupTask struct { // incompleteCount is the number of resources+balances still pending and need to be loaded - // this variable is used by as atomic variable to synchronize the readiness of the group taks. + // it is used to synchronize the readiness of the group task. incompleteCount atomic.Int64 // the group task index - aligns with the index of the transaction group in the // provided groups slice. groupTaskIndex atomic.Int64 + // balances contains the loaded balances each transaction group have balances []LoadedAccountDataEntry - // balancesCount is the number of balances that nees to be loaded per transaction group + // balancesCount is the number of balances that need to be loaded for this transaction group balancesCount int + // resources contains the loaded resources each of the transaction groups have - resources []LoadedResourcesEntry - // resourcesCount is the number of resources that nees to be loaded per transaction group + resources []LoadedResourceEntry + // resourcesCount is the number of resources that need to be loaded for this transaction group resourcesCount int + // kvs contains the loaded resources each of the transaction groups have + kvs []LoadedKVEntry + // kvCount is the number of kvs that need to be loaded for this transaction group + kvCount int + // error while processing this group task - err *GroupTaskError + err error } -// preloaderTask manage the loading of a single element, whether it's a resource or an account address. +// preloaderTask manage the loading of a single element, whether account, creatable, or kv type preloaderTask struct { // account address to fetch address *basics.Address @@ -127,64 +151,39 @@ type preloaderTask struct { creatableIndex basics.CreatableIndex // resource type creatableType basics.CreatableType - // a list of transaction group tasks that depends on this address or resource - groupTasks []*groupTask - // a list of indices into the groupTask.balances or groupTask.resources where the address would be stored - groupTasksIndices []int -} -// preloaderTaskQueue is a dynamic linked list of enqueued entries, optimized for non-syncronized insertion and -// syncronized extraction -type preloaderTaskQueue struct { - next *preloaderTaskQueue - used int - entries []*preloaderTask - baseIdx int - maxTxnGroupEntries int -} + // key is the kv to fetch, if this is a kv task + key string -type groupTaskDone struct { - groupIdx int64 - err error - task *preloaderTask + // the transaction group task to put the loaded data into + groupTask *groupTask + // the index at which to place the resource (in groupTask) + groupTaskIndex int } -func allocPreloaderQueue(count int, maxTxnGroupEntries int) preloaderTaskQueue { - return preloaderTaskQueue{ - entries: make([]*preloaderTask, count*2+maxTxnGroupEntries*2), - maxTxnGroupEntries: maxTxnGroupEntries, - } +type preloaderTaskQueue util.PagedQueue[*preloaderTask] + +func allocPreloaderQueue(count int) *preloaderTaskQueue { + return (*preloaderTaskQueue)(util.NewPagedQueue[*preloaderTask](count)) } -// enqueue places the queued entry on the queue, returning the latest queue -// ( in case the current "page" ran out of space ) -func (pq *preloaderTaskQueue) enqueue(t *preloaderTask) { - pq.entries[pq.used] = t - pq.used++ +func (pq *preloaderTaskQueue) append(t *preloaderTask) *preloaderTaskQueue { + return (*preloaderTaskQueue)((*util.PagedQueue[*preloaderTask])(pq).Append(t)) } -func (pq *preloaderTaskQueue) expand() *preloaderTaskQueue { - if cap(pq.entries)-pq.used < pq.maxTxnGroupEntries { - pq.next = &preloaderTaskQueue{ - entries: make([]*preloaderTask, cap(pq.entries)*2), - used: 0, - baseIdx: pq.baseIdx + pq.used, - maxTxnGroupEntries: pq.maxTxnGroupEntries, - } - return pq.next - } - return pq +func (pq *preloaderTaskQueue) length() int64 { + return int64((*util.PagedQueue[*preloaderTask])(pq).Len()) } func (pq *preloaderTaskQueue) getTaskAtIndex(idx int) (*preloaderTaskQueue, *preloaderTask) { - localIdx := idx - pq.baseIdx - if pq.used > localIdx { - return pq, pq.entries[localIdx] - } - if pq.next != nil { - return pq.next.getTaskAtIndex(idx) - } - return pq, nil + page, task := (*util.PagedQueue[*preloaderTask])(pq).Get(idx) + return (*preloaderTaskQueue)(page), task +} + +type groupTaskDone struct { + groupIdx int64 + err error + task *preloaderTask } type accountCreatableKey struct { @@ -192,31 +191,42 @@ type accountCreatableKey struct { cidx basics.CreatableIndex } -func loadAccountsAddAccountTask(addr *basics.Address, wt *groupTask, accountTasks map[basics.Address]*preloaderTask, queue *preloaderTaskQueue) { +func (pq *preloaderTaskQueue) addAccountTask(addr *basics.Address, wt *groupTask, accountTasks map[basics.Address]*preloaderTask) *preloaderTaskQueue { if addr.IsZero() { - return + return pq } - if task, have := accountTasks[*addr]; !have { + if _, have := accountTasks[*addr]; !have { newTask := &preloaderTask{ - address: addr, - groupTasks: make([]*groupTask, 1, 4), - groupTasksIndices: make([]int, 1, 4), + address: addr, + groupTask: wt, + groupTaskIndex: wt.balancesCount, } - newTask.groupTasks[0] = wt - newTask.groupTasksIndices[0] = wt.balancesCount - + wt.balancesCount++ accountTasks[*addr] = newTask - queue.enqueue(newTask) - } else { - task.groupTasks = append(task.groupTasks, wt) - task.groupTasksIndices = append(task.groupTasksIndices, wt.balancesCount) + pq = pq.append(newTask) } - wt.balancesCount++ + return pq +} + +func (pq *preloaderTaskQueue) addAssetTask(aid basics.AssetIndex, wt *groupTask, resourceTasks map[accountCreatableKey]*preloaderTask) *preloaderTaskQueue { + return pq.addResourceTask(nil, basics.CreatableIndex(aid), basics.AssetCreatable, wt, resourceTasks) +} + +func (pq *preloaderTaskQueue) addHoldingTask(addr basics.Address, aid basics.AssetIndex, wt *groupTask, resourceTasks map[accountCreatableKey]*preloaderTask) *preloaderTaskQueue { + return pq.addResourceTask(&addr, basics.CreatableIndex(aid), basics.AssetCreatable, wt, resourceTasks) +} + +func (pq *preloaderTaskQueue) addAppTask(aid basics.AppIndex, wt *groupTask, resourceTasks map[accountCreatableKey]*preloaderTask) *preloaderTaskQueue { + return pq.addResourceTask(nil, basics.CreatableIndex(aid), basics.AppCreatable, wt, resourceTasks) +} + +func (pq *preloaderTaskQueue) addLocalsTask(addr basics.Address, aid basics.AppIndex, wt *groupTask, resourceTasks map[accountCreatableKey]*preloaderTask) *preloaderTaskQueue { + return pq.addResourceTask(&addr, basics.CreatableIndex(aid), basics.AppCreatable, wt, resourceTasks) } -func loadAccountsAddResourceTask(addr *basics.Address, cidx basics.CreatableIndex, ctype basics.CreatableType, wt *groupTask, resourceTasks map[accountCreatableKey]*preloaderTask, queue *preloaderTaskQueue) { +func (pq *preloaderTaskQueue) addResourceTask(addr *basics.Address, cidx basics.CreatableIndex, ctype basics.CreatableType, wt *groupTask, resourceTasks map[accountCreatableKey]*preloaderTask) *preloaderTaskQueue { if cidx == 0 { - return + return pq } key := accountCreatableKey{ cidx: cidx, @@ -224,48 +234,58 @@ func loadAccountsAddResourceTask(addr *basics.Address, cidx basics.CreatableInde if addr != nil { key.address = *addr } - if task, have := resourceTasks[key]; !have { + if _, have := resourceTasks[key]; !have { newTask := &preloaderTask{ - address: addr, - groupTasks: make([]*groupTask, 1, 4), - groupTasksIndices: make([]int, 1, 4), - creatableIndex: cidx, - creatableType: ctype, + address: addr, + groupTask: wt, + groupTaskIndex: wt.resourcesCount, + creatableIndex: cidx, + creatableType: ctype, } - newTask.groupTasks[0] = wt - newTask.groupTasksIndices[0] = wt.resourcesCount - + wt.resourcesCount++ resourceTasks[key] = newTask - queue.enqueue(newTask) - } else { - task.groupTasks = append(task.groupTasks, wt) - task.groupTasksIndices = append(task.groupTasksIndices, wt.resourcesCount) + pq = pq.append(newTask) + } + return pq +} + +func (pq *preloaderTaskQueue) addKvTask(app basics.AppIndex, name []byte, wt *groupTask, kvTasks map[string]*preloaderTask) *preloaderTaskQueue { + if app == 0 || len(name) == 0 { + return pq + } + key := apps.MakeBoxKey(uint64(app), string(name)) + if _, have := kvTasks[key]; !have { + newTask := &preloaderTask{ + key: key, + groupTask: wt, + groupTaskIndex: wt.kvCount, + } + wt.kvCount++ + kvTasks[key] = newTask + pq = pq.append(newTask) } - wt.resourcesCount++ + return pq } // prefetch would process the input transaction groups by analyzing each of the transaction groups and building // an execution queue that would allow us to fetch all the dependencies for the input transaction groups in order // and output these onto a channel. -func (p *accountPrefetcher) prefetch(ctx context.Context) { +func (p *paysetPrefetcher) prefetch(ctx context.Context) { defer close(p.outChan) accountTasks := make(map[basics.Address]*preloaderTask) resourceTasks := make(map[accountCreatableKey]*preloaderTask) + kvTasks := make(map[string]*preloaderTask) - var maxTxnGroupEntries int - if p.consensusParams.Application { - // the extra two are for the sender account data, plus the application global state - maxTxnGroupEntries = p.consensusParams.MaxTxGroupSize * (2 + p.consensusParams.MaxAppTxnAccounts + p.consensusParams.MaxAppTxnForeignApps + p.consensusParams.MaxAppTxnForeignAssets) - } else { - // 8 is the number of resources+account used in the AssetTransferTx, which is the largest one. - maxTxnGroupEntries = p.consensusParams.MaxTxGroupSize * 8 + txnCount := 0 + for _, group := range p.txnGroups { + txnCount += len(group) } - - tasksQueue := allocPreloaderQueue(len(p.txnGroups), maxTxnGroupEntries) + tasksQueue := allocPreloaderQueue(4 * txnCount) // 4 is just an approximation // totalBalances counts the total number of balances over all the transaction groups totalBalances := 0 totalResources := 0 + totalKVs := 0 // initialize empty groupTasks for groupsReady groupsReady := make([]*groupTask, len(p.txnGroups)) @@ -273,108 +293,172 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { groupsReady[i] = new(groupTask) // this ensures each allocated groupTask is 64-bit aligned } + // iterate over the transaction groups and add resources that are very likely to be accessed + queue := tasksQueue + // 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]}, - groupTasksIndices: []int{0}, + address: &p.feeSinkAddr, + groupTask: groupsReady[0], + groupTaskIndex: 0, } - groupsReady[0].balancesCount = 1 + groupsReady[0].balancesCount++ accountTasks[p.feeSinkAddr] = feeSinkPreloader - tasksQueue.enqueue(feeSinkPreloader) + queue = queue.append(feeSinkPreloader) } - - // iterate over the transaction groups and add all their account addresses to the list - queue := &tasksQueue for i := range p.txnGroups { task := groupsReady[i] for j := range p.txnGroups[i] { stxn := &p.txnGroups[i][j] switch stxn.Txn.Type { case protocol.PaymentTx: - loadAccountsAddAccountTask(&stxn.Txn.Receiver, task, accountTasks, queue) - loadAccountsAddAccountTask(&stxn.Txn.CloseRemainderTo, task, accountTasks, queue) + queue = queue.addAccountTask(&stxn.Txn.Receiver, task, accountTasks) + queue = queue.addAccountTask(&stxn.Txn.CloseRemainderTo, task, accountTasks) case protocol.AssetConfigTx: - loadAccountsAddResourceTask(nil, basics.CreatableIndex(stxn.Txn.ConfigAsset), basics.AssetCreatable, task, resourceTasks, queue) + queue = queue.addAssetTask(stxn.Txn.ConfigAsset, task, resourceTasks) case protocol.AssetTransferTx: if !stxn.Txn.AssetSender.IsZero() { - loadAccountsAddResourceTask(nil, basics.CreatableIndex(stxn.Txn.XferAsset), basics.AssetCreatable, task, resourceTasks, queue) - loadAccountsAddResourceTask(&stxn.Txn.AssetSender, basics.CreatableIndex(stxn.Txn.XferAsset), basics.AssetCreatable, task, resourceTasks, queue) + queue = queue.addAssetTask(stxn.Txn.XferAsset, task, resourceTasks) + queue = queue.addHoldingTask(stxn.Txn.AssetSender, stxn.Txn.XferAsset, task, resourceTasks) } else { if stxn.Txn.AssetAmount == 0 && (stxn.Txn.AssetReceiver == stxn.Txn.Sender) { // opt in - loadAccountsAddResourceTask(nil, basics.CreatableIndex(stxn.Txn.XferAsset), basics.AssetCreatable, task, resourceTasks, queue) + queue = queue.addAssetTask(stxn.Txn.XferAsset, task, resourceTasks) } if stxn.Txn.AssetAmount != 0 { // zero transfer is noop - loadAccountsAddResourceTask(&stxn.Txn.Sender, basics.CreatableIndex(stxn.Txn.XferAsset), basics.AssetCreatable, task, resourceTasks, queue) + queue = queue.addHoldingTask(stxn.Txn.Sender, stxn.Txn.XferAsset, task, resourceTasks) } } if !stxn.Txn.AssetReceiver.IsZero() { if stxn.Txn.AssetAmount != 0 || (stxn.Txn.AssetReceiver == stxn.Txn.Sender) { // if not zero transfer or opt in then prefetch - loadAccountsAddResourceTask(&stxn.Txn.AssetReceiver, basics.CreatableIndex(stxn.Txn.XferAsset), basics.AssetCreatable, task, resourceTasks, queue) + queue = queue.addHoldingTask(stxn.Txn.AssetReceiver, stxn.Txn.XferAsset, task, resourceTasks) } } if !stxn.Txn.AssetCloseTo.IsZero() { - loadAccountsAddResourceTask(&stxn.Txn.AssetCloseTo, basics.CreatableIndex(stxn.Txn.XferAsset), basics.AssetCreatable, task, resourceTasks, queue) + queue = queue.addHoldingTask(stxn.Txn.AssetCloseTo, stxn.Txn.XferAsset, task, resourceTasks) } case protocol.AssetFreezeTx: if !stxn.Txn.FreezeAccount.IsZero() { - loadAccountsAddResourceTask(nil, basics.CreatableIndex(stxn.Txn.FreezeAsset), basics.AssetCreatable, task, resourceTasks, queue) - loadAccountsAddResourceTask(&stxn.Txn.FreezeAccount, basics.CreatableIndex(stxn.Txn.FreezeAsset), basics.AssetCreatable, task, resourceTasks, queue) - loadAccountsAddAccountTask(&stxn.Txn.FreezeAccount, task, accountTasks, queue) + queue = queue.addAssetTask(stxn.Txn.FreezeAsset, task, resourceTasks) + queue = queue.addHoldingTask(stxn.Txn.FreezeAccount, stxn.Txn.FreezeAsset, task, resourceTasks) } case protocol.ApplicationCallTx: if stxn.Txn.ApplicationID != 0 { // load the global - so that we'll have the program - loadAccountsAddResourceTask(nil, basics.CreatableIndex(stxn.Txn.ApplicationID), basics.AppCreatable, task, resourceTasks, queue) + queue = queue.addAppTask(stxn.Txn.ApplicationID, task, resourceTasks) // load the local - so that we'll have the local state // TODO: this is something we need to decide if we want to enable, since not // every application call would use local storage. if (stxn.Txn.ApplicationCallTxnFields.OnCompletion == transactions.OptInOC) || (stxn.Txn.ApplicationCallTxnFields.OnCompletion == transactions.CloseOutOC) || (stxn.Txn.ApplicationCallTxnFields.OnCompletion == transactions.ClearStateOC) { - loadAccountsAddResourceTask(&stxn.Txn.Sender, basics.CreatableIndex(stxn.Txn.ApplicationID), basics.AppCreatable, task, resourceTasks, queue) + queue = queue.addLocalsTask(stxn.Txn.Sender, stxn.Txn.ApplicationID, task, resourceTasks) } } - // do not preload Txn.ForeignApps, Txn.ForeignAssets, Txn.Accounts - // since they might be non-used arbitrary values + // Prefetch ForeignApps since they're likely to be accessed + for _, appID := range stxn.Txn.ForeignApps { + queue = queue.addAppTask(appID, task, resourceTasks) + } + + // Prefetch boxes, they ought to be precise + for _, br := range stxn.Txn.Boxes { + if len(br.Name) == 0 { + continue + } + // defense: we don't even know if WellFormed yet. + if br.Index > uint64(len(stxn.Txn.ForeignApps)) { + continue + } + app := stxn.Txn.ApplicationID + if br.Index != 0 { + app = stxn.Txn.ForeignApps[br.Index-1] + } + if app != 0 { + queue = queue.addKvTask(app, br.Name, task, kvTasks) + } + } + + // With tx.Access, cross-products are explicit, so we fetch them + // (and the apps and boxes, as with foreign arrays). We also + // fetch the accounts and assets if they are NOT used in + // cross-products. That implies they are directly needed. + if len(stxn.Txn.Access) > 0 { + // Track which accounts and assets appear in cross-products + accountInCrossProduct := util.MakeSet[basics.Address]() + assetInCrossProduct := util.MakeSet[basics.AssetIndex]() + + for _, rr := range stxn.Txn.Access { + if rr.App != 0 { + queue = queue.addResourceTask(nil, basics.CreatableIndex(rr.App), basics.AppCreatable, task, resourceTasks) + } + if !rr.Holding.Empty() { + addr, asset, err := rr.Holding.Resolve(stxn.Txn.Access, stxn.Txn.Sender) + if err == nil { + queue = queue.addHoldingTask(addr, asset, task, resourceTasks) + accountInCrossProduct.Add(addr) + assetInCrossProduct.Add(asset) + } + } + if !rr.Locals.Empty() { + addr, app, err := rr.Locals.Resolve(stxn.Txn.Access, stxn.Txn.Sender, stxn.Txn.ApplicationID) + if err == nil { + queue = queue.addLocalsTask(addr, app, task, resourceTasks) + accountInCrossProduct.Add(addr) + } + } + if !rr.Box.Empty() { + app, name, err := rr.Box.Resolve(stxn.Txn.Access) + if err == nil { + if app == 0 { + app = stxn.Txn.ApplicationID + } + queue = queue.addKvTask(app, []byte(name), task, kvTasks) + } + } + } + + // Presumably, accounts and assets that don't appear in + // cross-products are present to be directly accessed. + for _, rr := range stxn.Txn.Access { + if !rr.Address.IsZero() && !accountInCrossProduct.Contains(rr.Address) { + queue = queue.addAccountTask(&rr.Address, task, accountTasks) + } + if rr.Asset != 0 && !assetInCrossProduct.Contains(rr.Asset) { + queue = queue.addAssetTask(rr.Asset, task, resourceTasks) + } + } + } case protocol.StateProofTx: case protocol.KeyRegistrationTx: // No extra accounts besides the sender case protocol.HeartbeatTx: - loadAccountsAddAccountTask(&stxn.Txn.HbAddress, task, accountTasks, queue) + queue = queue.addAccountTask(&stxn.Txn.HbAddress, task, accountTasks) } // If you add new addresses here, also add them in getTxnAddresses(). if !stxn.Txn.Sender.IsZero() { - loadAccountsAddAccountTask(&stxn.Txn.Sender, task, accountTasks, queue) + queue = queue.addAccountTask(&stxn.Txn.Sender, task, accountTasks) } } totalBalances += task.balancesCount totalResources += task.resourcesCount - // expand the queue if needed. - queue = queue.expand() + totalKVs += task.kvCount } - // find the number of tasks - tasksCount := int64(0) - for lastQueueEntry := &tasksQueue; ; lastQueueEntry = lastQueueEntry.next { - if lastQueueEntry.next == nil { - tasksCount = int64(lastQueueEntry.baseIdx + lastQueueEntry.used) - break - } - } + tasksCount := queue.length() // update all the groups task : // allocate the correct number of balances, as well as // enough space on the "done" channel. allBalances := make([]LoadedAccountDataEntry, totalBalances) - allResources := make([]LoadedResourcesEntry, totalResources) + allResources := make([]LoadedResourceEntry, totalResources) + allKVs := make([]LoadedKVEntry, totalKVs) usedBalances := 0 usedResources := 0 + usedKVs := 0 // groupDoneCh is used to communicate the completion signal for a single // resource/address load between the go-routines and the main output channel @@ -385,13 +469,13 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { for grpIdx := range groupsReady { gr := groupsReady[grpIdx] gr.groupTaskIndex.Store(int64(grpIdx)) - gr.incompleteCount.Store(int64(gr.balancesCount + gr.resourcesCount)) + gr.incompleteCount.Store(int64(gr.balancesCount + gr.resourcesCount + gr.kvCount)) gr.balances = allBalances[usedBalances : usedBalances+gr.balancesCount] - if gr.resourcesCount > 0 { - gr.resources = allResources[usedResources : usedResources+gr.resourcesCount] - usedResources += gr.resourcesCount - } usedBalances += gr.balancesCount + gr.resources = allResources[usedResources : usedResources+gr.resourcesCount] + usedResources += gr.resourcesCount + gr.kvs = allKVs[usedKVs : usedKVs+gr.kvCount] + usedKVs += gr.kvCount if gr.incompleteCount.Load() == 0 { gr.incompleteCount.Store(dependencyFreeGroup) } @@ -400,9 +484,9 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { var taskIdx atomic.Int64 taskIdx.Store(-1) defer taskIdx.Store(tasksCount) - // create few go-routines to load asyncroniously the account data. - for i := 0; i < asyncAccountLoadingThreadCount; i++ { - go p.asyncPrefetchRoutine(&tasksQueue, &taskIdx, groupDoneCh) + // create a few go-routines to asynchronously perform prefetches + for range asyncAccountLoadingThreadCount { + go p.asyncPrefetchRoutine(tasksQueue, &taskIdx, groupDoneCh) } // iterate on the transaction groups tasks. This array retains the original order. @@ -414,16 +498,18 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { select { case done := <-groupDoneCh: if done.err != nil { - groupsReady[done.groupIdx].err = &GroupTaskError{ - err: done.err, - GroupIdx: done.groupIdx, - Address: done.task.address, - CreatableIndex: done.task.creatableIndex, - CreatableType: done.task.creatableType, + var e error + if done.task.key != "" { + e = fmt.Errorf("prefetch failed for groupIdx %d, kv: %x, cause: %w", + done.groupIdx, done.task.key, done.err) + } else { + e = fmt.Errorf("prefetch failed for groupIdx %d, address: %s, creatableIndex %d, creatableType %d, cause: %w", + done.groupIdx, done.task.address, done.task.creatableIndex, done.task.creatableType, done.err) } + groupsReady[done.groupIdx].err = e } if done.groupIdx > i { - // mark future txn as ready. + // mark future txngroup as ready. completed[done.groupIdx] = true goto wait } else if done.groupIdx < i { @@ -434,8 +520,8 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { return } } - next := i - for ; next < int64(len(p.txnGroups)); next++ { + + for next := i; next < int64(len(p.txnGroups)); next++ { if !completed[next] { if next > i { i = next @@ -453,9 +539,10 @@ func (p *accountPrefetcher) prefetch(ctx context.Context) { TxnGroup: p.txnGroups[next], Accounts: groupsReady[next].balances, Resources: groupsReady[next].resources, + KVs: groupsReady[next].kvs, } } - // if we get to this point, it means that we have no more transaction to process. + // if we get to this point, it means that we have no more groups to process. break } } @@ -467,14 +554,21 @@ func (gt *groupTask) markCompletionAcct(idx int, br LoadedAccountDataEntry, grou } } -func (gt *groupTask) markCompletionResource(idx int, res LoadedResourcesEntry, groupDoneCh chan groupTaskDone) { +func (gt *groupTask) markCompletionResource(idx int, res LoadedResourceEntry, groupDoneCh chan groupTaskDone) { gt.resources[idx] = res if gt.incompleteCount.Add(-1) == 0 { groupDoneCh <- groupTaskDone{groupIdx: gt.groupTaskIndex.Load()} } } -func (gt *groupTask) markCompletionAcctError(err error, task *preloaderTask, groupDoneCh chan groupTaskDone) { +func (gt *groupTask) markCompletionKv(idx int, kv LoadedKVEntry, groupDoneCh chan groupTaskDone) { + gt.kvs[idx] = kv + if gt.incompleteCount.Add(-1) == 0 { + groupDoneCh <- groupTaskDone{groupIdx: gt.groupTaskIndex.Load()} + } +} + +func (gt *groupTask) markCompletionError(err error, task *preloaderTask, groupDoneCh chan groupTaskDone) { for { curVal := gt.incompleteCount.Load() if curVal <= 0 { @@ -491,9 +585,8 @@ func (gt *groupTask) markCompletionAcctError(err error, task *preloaderTask, gro } } -func (p *accountPrefetcher) asyncPrefetchRoutine(queue *preloaderTaskQueue, taskIdx *atomic.Int64, groupDoneCh chan groupTaskDone) { +func (p *paysetPrefetcher) asyncPrefetchRoutine(queue *preloaderTaskQueue, taskIdx *atomic.Int64, groupDoneCh chan groupTaskDone) { var task *preloaderTask - var err error for { nextTaskIdx := taskIdx.Add(1) queue, task = queue.getTaskAtIndex(int(nextTaskIdx)) @@ -501,84 +594,81 @@ func (p *accountPrefetcher) asyncPrefetchRoutine(queue *preloaderTaskQueue, task // no more tasks. return } + if task.key != "" { + value, err := p.ledger.LookupKv(p.rnd, task.key) + if err != nil { + // notify the channel of the error. + task.groupTask.markCompletionError(err, task, groupDoneCh) + continue + } + br := LoadedKVEntry{ + Key: task.key, + Value: value, + } + task.groupTask.markCompletionKv(task.groupTaskIndex, br, groupDoneCh) + continue + } if task.creatableIndex == 0 { // lookup the account data directly from the ledger. - var acctData ledgercore.AccountData - acctData, _, err = p.ledger.LookupWithoutRewards(p.rnd, *task.address) - // if there was an error.. + acctData, _, err := p.ledger.LookupWithoutRewards(p.rnd, *task.address) if err != nil { - // there was an error loading that entry. - for _, wt := range task.groupTasks { - // notify the channel of the error. - wt.markCompletionAcctError(err, task, groupDoneCh) - } + // notify the channel of the error. + task.groupTask.markCompletionError(err, task, groupDoneCh) continue } br := LoadedAccountDataEntry{ Address: task.address, Data: &acctData, } - // update all the group tasks with the new acquired balance. - for i, wt := range task.groupTasks { - wt.markCompletionAcct(task.groupTasksIndices[i], br, groupDoneCh) - } + task.groupTask.markCompletionAcct(task.groupTaskIndex, br, groupDoneCh) continue } if task.address == nil { // start off by figuring out the creator in case it's a global resource. - var creator basics.Address - var ok bool - creator, ok, err = p.ledger.GetCreatorForRound(p.rnd, task.creatableIndex, task.creatableType) + creator, ok, err := p.ledger.GetCreatorForRound(p.rnd, task.creatableIndex, task.creatableType) if err != nil { // there was an error loading that entry. - for _, wt := range task.groupTasks { - // notify the channel of the error. - wt.markCompletionAcctError(err, task, groupDoneCh) - } + task.groupTask.markCompletionError(err, task, groupDoneCh) continue } if !ok { - re := LoadedResourcesEntry{ + re := LoadedResourceEntry{ CreatableIndex: task.creatableIndex, CreatableType: task.creatableType, } - // update all the group tasks with the new acquired balance. - for i, wt := range task.groupTasks { - wt.markCompletionResource(task.groupTasksIndices[i], re, groupDoneCh) - } + task.groupTask.markCompletionResource(task.groupTaskIndex, re, groupDoneCh) continue } task.address = &creator } var resource ledgercore.AccountResource if task.creatableType == basics.AppCreatable { - var appResource ledgercore.AppResource - appResource, err = p.ledger.LookupApplication(p.rnd, *task.address, basics.AppIndex(task.creatableIndex)) + appResource, err := p.ledger.LookupApplication(p.rnd, *task.address, basics.AppIndex(task.creatableIndex)) + if err != nil { + // notify the channel of the error. + task.groupTask.markCompletionError(err, task, groupDoneCh) + continue + } resource.AppParams = appResource.AppParams resource.AppLocalState = appResource.AppLocalState } else { var assetResource ledgercore.AssetResource - assetResource, err = p.ledger.LookupAsset(p.rnd, *task.address, basics.AssetIndex(task.creatableIndex)) - resource.AssetParams = assetResource.AssetParams - resource.AssetHolding = assetResource.AssetHolding - } - if err != nil { - // there was an error loading that entry. - for _, wt := range task.groupTasks { + assetResource, err := p.ledger.LookupAsset(p.rnd, *task.address, basics.AssetIndex(task.creatableIndex)) + if err != nil { // notify the channel of the error. - wt.markCompletionAcctError(err, task, groupDoneCh) + task.groupTask.markCompletionError(err, task, groupDoneCh) + continue } - continue + resource.AssetParams = assetResource.AssetParams + resource.AssetHolding = assetResource.AssetHolding } - re := LoadedResourcesEntry{ + re := LoadedResourceEntry{ Resource: &resource, Address: task.address, CreatableIndex: task.creatableIndex, CreatableType: task.creatableType, } - // update all the group tasks with the new acquired balance. - for i, wt := range task.groupTasks { - wt.markCompletionResource(task.groupTasksIndices[i], re, groupDoneCh) - } + // update the group task with the new acquired balance. + task.groupTask.markCompletionResource(task.groupTaskIndex, re, groupDoneCh) } } diff --git a/ledger/eval/prefetcher/prefetcher_alignment_test.go b/ledger/eval/prefetcher/prefetcher_alignment_test.go index 433f518172..75c2aa7b56 100644 --- a/ledger/eval/prefetcher/prefetcher_alignment_test.go +++ b/ledger/eval/prefetcher/prefetcher_alignment_test.go @@ -24,6 +24,9 @@ import ( "github.com/stretchr/testify/require" + "github.com/algorand/avm-abi/apps" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" @@ -33,12 +36,12 @@ import ( "github.com/algorand/go-algorand/data/committee" "github.com/algorand/go-algorand/data/stateproofmsg" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger/eval" "github.com/algorand/go-algorand/ledger/eval/prefetcher" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/algorand/go-deadlock" ) type creatable struct { @@ -77,11 +80,13 @@ type prefetcherAlignmentTestLedger struct { apps map[basics.Address]map[basics.AppIndex]ledgercore.AppResource assets map[basics.Address]map[basics.AssetIndex]ledgercore.AssetResource creators map[basics.CreatableIndex]basics.Address + kvs map[string][]byte requestedBalances map[basics.Address]struct{} requestedApps map[basics.Address]map[basics.AppIndex]struct{} requestedAssets map[basics.Address]map[basics.AssetIndex]struct{} requestedCreators map[creatable]struct{} + requestedKvs map[string]struct{} // Protects requested* variables. mu deadlock.Mutex @@ -170,7 +175,17 @@ func (l *prefetcherAlignmentTestLedger) LookupAsset(rnd basics.Round, addr basic } func (l *prefetcherAlignmentTestLedger) LookupKv(rnd basics.Round, key string) ([]byte, error) { - panic("not implemented") + l.mu.Lock() + if l.requestedKvs == nil { + l.requestedKvs = make(map[string]struct{}) + } + l.requestedKvs[key] = struct{}{} + l.mu.Unlock() + + if value, has := l.kvs[key]; has { + return value, nil + } + return nil, nil } func (l *prefetcherAlignmentTestLedger) GetCreatorForRound(_ basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { @@ -213,7 +228,19 @@ func parseLoadedAccountDataEntries(loadedAccountDataEntries []prefetcher.LoadedA return res } -func parseLoadedResourcesEntries(loadedResourcesEntries []prefetcher.LoadedResourcesEntry) (apps map[basics.Address]map[basics.AppIndex]struct{}, assets map[basics.Address]map[basics.AssetIndex]struct{}, creators map[creatable]struct{}) { +func parseLoadedKVEntries(loadedKVEntries []prefetcher.LoadedKVEntry) map[string]struct{} { + if len(loadedKVEntries) == 0 { + return nil + } + + res := make(map[string]struct{}) + for _, e := range loadedKVEntries { + res[e.Key] = struct{}{} + } + return res +} + +func parseLoadedResourcesEntries(loadedResourcesEntries []prefetcher.LoadedResourceEntry) (apps map[basics.Address]map[basics.AppIndex]struct{}, assets map[basics.Address]map[basics.AssetIndex]struct{}, creators map[creatable]struct{}) { for _, e := range loadedResourcesEntries { cr := creatable{ cindex: e.CreatableIndex, @@ -282,11 +309,12 @@ type ledgerData struct { Apps map[basics.Address]map[basics.AppIndex]struct{} Assets map[basics.Address]map[basics.AssetIndex]struct{} Creators map[creatable]struct{} + KVs map[string]struct{} } // pretend adds the `before` addresses to the Accounts. It "pretends" that the // addresses were prefetched, so we can get agreement with what was actually -// requested. We do this to include two addresses that are going to end up +// requested. We do this to include the rewards pool which is going to end up // requested *before* prefetch is even attempted. So there's no point in // PrefetchAccounts being modified to return them, they have been "prefetched" // simply by accessing them. @@ -299,7 +327,7 @@ func (ld *ledgerData) pretend(before ...basics.Address) { func prefetch(t *testing.T, l prefetcher.Ledger, txn transactions.Transaction) ledgerData { group := makeGroupFromTxn(txn) - ch := prefetcher.PrefetchAccounts( + ch := prefetcher.BlockReferences( context.Background(), l, 1, [][]transactions.SignedTxnWithAD{group}, feeSink(), config.Consensus[proto]) @@ -314,12 +342,14 @@ func prefetch(t *testing.T, l prefetcher.Ledger, txn transactions.Transaction) l accounts := parseLoadedAccountDataEntries(loaded.Accounts) apps, assets, creators := parseLoadedResourcesEntries(loaded.Resources) + kvs := parseLoadedKVEntries(loaded.KVs) return ledgerData{ Accounts: accounts, Apps: apps, Assets: assets, Creators: creators, + KVs: kvs, } } @@ -342,6 +372,7 @@ func run(t *testing.T, l *prefetcherAlignmentTestLedger, txn transactions.Transa l.requestedApps = nil l.requestedAssets = nil l.requestedCreators = nil + l.requestedKvs = nil runEval(t, l, txn) requestedData := ledgerData{ @@ -349,6 +380,7 @@ func run(t *testing.T, l *prefetcherAlignmentTestLedger, txn transactions.Transa Apps: l.requestedApps, Assets: l.requestedAssets, Creators: l.requestedCreators, + KVs: l.requestedKvs, } return requestedData, prefetched @@ -783,7 +815,7 @@ func TestEvaluatorPrefetcherAlignmentAssetClawback(t *testing.T) { func TestEvaluatorPrefetcherAlignmentAssetFreeze(t *testing.T) { partitiontest.PartitionTest(t) - assetID := basics.AssetIndex(5) + const assetID = 5 l := &prefetcherAlignmentTestLedger{ balances: map[basics.Address]ledgercore.AccountData{ rewardsPool(): { @@ -827,12 +859,12 @@ func TestEvaluatorPrefetcherAlignmentAssetFreeze(t *testing.T) { }, }, creators: map[basics.CreatableIndex]basics.Address{ - basics.CreatableIndex(assetID): makeAddress(1), + assetID: makeAddress(1), }, } txn := transactions.Transaction{ - Type: protocol.AssetTransferTx, + Type: protocol.AssetFreezeTx, Header: transactions.Header{ Sender: makeAddress(2), GenesisHash: genesisHash(), @@ -1249,7 +1281,19 @@ func TestEvaluatorPrefetcherAlignmentApplicationCallAccountsDeclaration(t *testi func TestEvaluatorPrefetcherAlignmentApplicationCallForeignAppsDeclaration(t *testing.T) { partitiontest.PartitionTest(t) - appID := basics.AppIndex(5) + const appID = 1115 + + // We're going to access app1 and app2 to match the prefetcher's assumption + // that it should prefetch apps + ops, err := logic.AssembleString(`#pragma version 5 +int 1; byte "A" +app_global_get_ex; pop; pop +int 2; byte "A" +app_global_get_ex; pop; pop +int 1`) + require.NoError(t, err) + getGlobals := ops.Program + l := &prefetcherAlignmentTestLedger{ balances: map[basics.Address]ledgercore.AccountData{ rewardsPool(): { @@ -1275,7 +1319,21 @@ func TestEvaluatorPrefetcherAlignmentApplicationCallForeignAppsDeclaration(t *te makeAddress(1): { appID: { AppParams: &basics.AppParams{ - ApprovalProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + ApprovalProgram: getGlobals, + ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + }, + AppLocalState: &basics.AppLocalState{}, + }, + 1116: { + AppParams: &basics.AppParams{ + ApprovalProgram: getGlobals, + ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + }, + AppLocalState: &basics.AppLocalState{}, + }, + 1118: { + AppParams: &basics.AppParams{ + ApprovalProgram: getGlobals, ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, }, AppLocalState: &basics.AppLocalState{}, @@ -1288,7 +1346,9 @@ func TestEvaluatorPrefetcherAlignmentApplicationCallForeignAppsDeclaration(t *te }, }, creators: map[basics.CreatableIndex]basics.Address{ - basics.CreatableIndex(appID): makeAddress(1), + appID: makeAddress(1), + 1116: makeAddress(1), + 1118: makeAddress(1), }, } @@ -1300,17 +1360,17 @@ func TestEvaluatorPrefetcherAlignmentApplicationCallForeignAppsDeclaration(t *te }, ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ ApplicationID: appID, - ForeignApps: []basics.AppIndex{6, 8}, + ForeignApps: []basics.AppIndex{1116, 1118}, }, } requested, prefetched := run(t, l, txn) prefetched.pretend(rewardsPool()) - // Foreign apps are not loaded, ensure they are not prefetched - require.NotContains(t, prefetched.Creators, creatable{cindex: 6, ctype: basics.AppCreatable}) - require.NotContains(t, prefetched.Creators, creatable{cindex: 8, ctype: basics.AppCreatable}) - require.Equal(t, requested, prefetched) + // Foreign apps are loaded, ensure they are prefetched + require.Contains(t, prefetched.Creators, creatable{cindex: 1116, ctype: basics.AppCreatable}) + require.Contains(t, prefetched.Creators, creatable{cindex: 1118, ctype: basics.AppCreatable}) + require.Equal(t, requested, prefetched) // Need to make the byte code use those app globals } func TestEvaluatorPrefetcherAlignmentApplicationCallForeignAssetsDeclaration(t *testing.T) { @@ -1477,3 +1537,367 @@ func TestEvaluatorPrefetcherAlignmentHeartbeat(t *testing.T) { prefetched.pretend(rewardsPool()) require.Equal(t, requested, prefetched) } + +func TestEvaluatorPrefetcherAlignmentApplicationCallWithBoxReference(t *testing.T) { + partitiontest.PartitionTest(t) + + appID := basics.AppIndex(5) + boxName := []byte("test-box") + boxKey := apps.MakeBoxKey(uint64(appID), string(boxName)) + boxValue := []byte("test-value") + + l := &prefetcherAlignmentTestLedger{ + balances: map[basics.Address]ledgercore.AccountData{ + rewardsPool(): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1234567890}, + }, + }, + makeAddress(1): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000001}, + TotalAppParams: 1, + TotalAppLocalStates: 1, + }, + }, + makeAddress(2): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000002}, + TotalAppLocalStates: 1, + }, + }, + }, + apps: map[basics.Address]map[basics.AppIndex]ledgercore.AppResource{ + makeAddress(1): { + appID: { + AppParams: &basics.AppParams{ + ApprovalProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + }, + AppLocalState: &basics.AppLocalState{}, + }, + }, + makeAddress(2): { + appID: { + AppLocalState: &basics.AppLocalState{}, + }, + }, + }, + creators: map[basics.CreatableIndex]basics.Address{ + basics.CreatableIndex(appID): makeAddress(1), + }, + kvs: map[string][]byte{ + boxKey: boxValue, + }, + } + + txn := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: makeAddress(2), + GenesisHash: genesisHash(), + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appID, + Boxes: []transactions.BoxRef{ + { + Index: 0, // refers to the ApplicationID + Name: boxName, + }, + }, + }, + } + + requested, prefetched := run(t, l, txn) + + prefetched.pretend(rewardsPool()) + require.Equal(t, requested, prefetched) +} + +func TestEvaluatorPrefetcherAlignmentApplicationCallWithMultipleBoxes(t *testing.T) { + partitiontest.PartitionTest(t) + + appID := basics.AppIndex(5) + box1Name := []byte("box1") + box2Name := []byte("box2") + box1Key := apps.MakeBoxKey(uint64(appID), string(box1Name)) + box2Key := apps.MakeBoxKey(uint64(appID), string(box2Name)) + box1Value := []byte("value1") + box2Value := []byte("value2") + + l := &prefetcherAlignmentTestLedger{ + balances: map[basics.Address]ledgercore.AccountData{ + rewardsPool(): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1234567890}, + }, + }, + makeAddress(1): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000001}, + TotalAppParams: 1, + TotalAppLocalStates: 1, + }, + }, + makeAddress(2): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000002}, + TotalAppLocalStates: 1, + }, + }, + }, + apps: map[basics.Address]map[basics.AppIndex]ledgercore.AppResource{ + makeAddress(1): { + appID: { + AppParams: &basics.AppParams{ + ApprovalProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + }, + AppLocalState: &basics.AppLocalState{}, + }, + }, + makeAddress(2): { + appID: { + AppLocalState: &basics.AppLocalState{}, + }, + }, + }, + creators: map[basics.CreatableIndex]basics.Address{ + basics.CreatableIndex(appID): makeAddress(1), + }, + kvs: map[string][]byte{ + box1Key: box1Value, + box2Key: box2Value, + }, + } + + txn := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: makeAddress(2), + GenesisHash: genesisHash(), + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appID, + Boxes: []transactions.BoxRef{ + { + Index: 0, + Name: box1Name, + }, + { + Index: 0, + Name: box2Name, + }, + }, + }, + } + + requested, prefetched := run(t, l, txn) + + prefetched.pretend(rewardsPool()) + require.Equal(t, requested, prefetched) +} + +func TestEvaluatorPrefetcherAlignmentApplicationCallWithNonExistentBox(t *testing.T) { + partitiontest.PartitionTest(t) + + appID := basics.AppIndex(5) + boxName := []byte("nonexistent-box") + + l := &prefetcherAlignmentTestLedger{ + balances: map[basics.Address]ledgercore.AccountData{ + rewardsPool(): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1234567890}, + }, + }, + makeAddress(1): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000001}, + TotalAppParams: 1, + TotalAppLocalStates: 1, + }, + }, + makeAddress(2): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000002}, + TotalAppLocalStates: 1, + }, + }, + }, + apps: map[basics.Address]map[basics.AppIndex]ledgercore.AppResource{ + makeAddress(1): { + appID: { + AppParams: &basics.AppParams{ + ApprovalProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + }, + AppLocalState: &basics.AppLocalState{}, + }, + }, + makeAddress(2): { + appID: { + AppLocalState: &basics.AppLocalState{}, + }, + }, + }, + creators: map[basics.CreatableIndex]basics.Address{ + basics.CreatableIndex(appID): makeAddress(1), + }, + kvs: map[string][]byte{ + // No box data - testing non-existent box + }, + } + + txn := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: makeAddress(2), + GenesisHash: genesisHash(), + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appID, + Boxes: []transactions.BoxRef{ + { + Index: 0, + Name: boxName, + }, + }, + }, + } + + requested, prefetched := run(t, l, txn) + + prefetched.pretend(rewardsPool()) + require.Equal(t, requested, prefetched) +} + +func TestEvaluatorPrefetcherAlignmentApplicationCallWithForeignAppBox(t *testing.T) { + partitiontest.PartitionTest(t) + + const appID = 1115 + const foreignAppID = 1110 + boxName := []byte("foreign-app-box") + boxKey := apps.MakeBoxKey(foreignAppID, string(boxName)) + boxValue := []byte("foreign-value") + + // We're going to access app1's globals to match our prefetcher's expectation that apps get accessed. + ops, err := logic.AssembleString(`#pragma version 10 +int 1 +byte "A" +app_global_get_ex +pop +pop +int 1`) + require.NoError(t, err) + globalApp1 := ops.Program + + l := &prefetcherAlignmentTestLedger{ + balances: map[basics.Address]ledgercore.AccountData{ + rewardsPool(): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1234567890}, + }, + }, + makeAddress(1): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000001}, + TotalAppParams: 1, + TotalAppLocalStates: 1, + }, + }, + makeAddress(2): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000002}, + TotalAppLocalStates: 1, + }, + }, + makeAddress(3): { + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000003}, + TotalAppParams: 1, + TotalAppLocalStates: 1, + }, + }, + }, + apps: map[basics.Address]map[basics.AppIndex]ledgercore.AppResource{ + makeAddress(1): { + appID: { + AppParams: &basics.AppParams{ + ApprovalProgram: globalApp1, + ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + }, + AppLocalState: &basics.AppLocalState{}, + }, + }, + makeAddress(2): { + appID: { + AppLocalState: &basics.AppLocalState{}, + }, + }, + makeAddress(3): { + foreignAppID: { + AppParams: &basics.AppParams{ + ApprovalProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + ClearStateProgram: []byte{0x02, 0x20, 0x01, 0x01, 0x22}, + }, + AppLocalState: &basics.AppLocalState{}, + }, + }, + }, + creators: map[basics.CreatableIndex]basics.Address{ + appID: makeAddress(1), + foreignAppID: makeAddress(3), + }, + kvs: map[string][]byte{ + boxKey: boxValue, + }, + } + + txn := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: makeAddress(2), + GenesisHash: genesisHash(), + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appID, + ForeignApps: []basics.AppIndex{foreignAppID}, + Boxes: []transactions.BoxRef{ + { + Index: 1, // refers to ForeignApps[0] + Name: boxName, + }, + }, + }, + } + + requested, prefetched := run(t, l, txn) + prefetched.pretend(rewardsPool()) + require.Equal(t, requested, prefetched) + + txnWithAccess := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: makeAddress(2), + GenesisHash: genesisHash(), + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appID, + Access: []transactions.ResourceRef{ + {App: foreignAppID}, + {Box: transactions.BoxRef{ + Index: 1, + Name: boxName, + }}, + }, + }, + } + + requestedWithA, prefetchedWithA := run(t, l, txnWithAccess) + prefetchedWithA.pretend(rewardsPool()) + require.Equal(t, requestedWithA, prefetchedWithA) + + // fetches for the txn with Access should match + require.Equal(t, requestedWithA, requested) +} diff --git a/ledger/eval/prefetcher/prefetcher_test.go b/ledger/eval/prefetcher/prefetcher_test.go index cd7c7f27b2..f8e3fda994 100644 --- a/ledger/eval/prefetcher/prefetcher_test.go +++ b/ledger/eval/prefetcher/prefetcher_test.go @@ -18,6 +18,7 @@ package prefetcher_test import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -33,18 +34,16 @@ import ( "github.com/algorand/go-algorand/test/partitiontest" ) -func makeAddressPtr(seed int) (o *basics.Address) { - o = new(basics.Address) +func makeAddressPtr(seed int) *basics.Address { + o := new(basics.Address) o[0] = byte(seed) o[1] = byte(seed >> 8) o[2] = byte(seed >> 16) - return + return o } -func makeAddress(addressSeed int) (o basics.Address) { - t := *makeAddressPtr(addressSeed) - copy(o[:], t[:]) - return +func makeAddress(seed int) basics.Address { + return *makeAddressPtr(seed) } // It would be nice to test current and future, but until that change is made, @@ -74,11 +73,13 @@ type prefetcherTestLedger struct { round basics.Round balances map[basics.Address]ledgercore.AccountData creators map[basics.CreatableIndex]basics.Address - errorTriggerAddress map[basics.Address]bool + kvs map[string][]byte + errorTriggerAddress *basics.Address } const errorTriggerCreatableIndex = 1000001 const errorTriggerAssetIndex = 1000002 +const errorTriggerKvName = "BADKV" func (l *prefetcherTestLedger) BlockHdr(basics.Round) (bookkeeping.BlockHeader, error) { return bookkeeping.BlockHeader{}, nil @@ -87,7 +88,7 @@ func (l *prefetcherTestLedger) CheckDup(config.ConsensusParams, basics.Round, ba return nil } func (l *prefetcherTestLedger) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (ledgercore.AccountData, basics.Round, error) { - if _, has := l.errorTriggerAddress[addr]; has { + if l.errorTriggerAddress != nil && *l.errorTriggerAddress == addr { return ledgercore.AccountData{}, l.round, lookupError{} } if data, has := l.balances[addr]; has { @@ -113,6 +114,17 @@ func (l *prefetcherTestLedger) GetCreatorForRound(_ basics.Round, cidx basics.Cr } return basics.Address{}, false, nil } + +func (l *prefetcherTestLedger) LookupKv(_ basics.Round, name string) ([]byte, error) { + if name[11:] == errorTriggerKvName { + return nil, errors.New("error looking up kv") + } + if val, has := l.kvs[name]; has { + return val, nil + } + return nil, nil +} + func (l *prefetcherTestLedger) GenesisHash() crypto.Digest { return crypto.Digest{} } @@ -160,7 +172,7 @@ type loadedResourcesEntryKey struct { creatableType basics.CreatableType } -func convertLoadedResourcesEntries(entries []prefetcher.LoadedResourcesEntry) map[loadedResourcesEntryKey]*ledgercore.AccountResource { +func convertLoadedResourcesEntries(entries []prefetcher.LoadedResourceEntry) map[loadedResourcesEntryKey]*ledgercore.AccountResource { res := make(map[loadedResourcesEntryKey]*ledgercore.AccountResource) for _, e := range entries { @@ -179,7 +191,8 @@ func convertLoadedResourcesEntries(entries []prefetcher.LoadedResourcesEntry) ma return res } -func compareLoadedResourcesEntries(t *testing.T, expected []prefetcher.LoadedResourcesEntry, actual []prefetcher.LoadedResourcesEntry) { +func compareLoadedResourcesEntries(t *testing.T, expected []prefetcher.LoadedResourceEntry, actual []prefetcher.LoadedResourceEntry) { + t.Helper() expectedForTest := convertLoadedResourcesEntries(expected) actualForTest := convertLoadedResourcesEntries(actual) require.Equal(t, expectedForTest, actualForTest) @@ -188,10 +201,10 @@ func compareLoadedResourcesEntries(t *testing.T, expected []prefetcher.LoadedRes func getPrefetcherTestLedger(rnd basics.Round) *prefetcherTestLedger { var ledger = &prefetcherTestLedger{ - round: rnd, - balances: make(map[basics.Address]ledgercore.AccountData), - creators: make(map[basics.CreatableIndex]basics.Address), - errorTriggerAddress: make(map[basics.Address]bool), + round: rnd, + balances: make(map[basics.Address]ledgercore.AccountData), + creators: make(map[basics.CreatableIndex]basics.Address), + kvs: make(map[string][]byte), } ledger.balances[makeAddress(1)] = ledgercore.AccountData{ AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 100000000}}, @@ -214,7 +227,7 @@ func TestEvaluatorPrefetcher(t *testing.T) { skip bool signedTxn transactions.SignedTxn accounts []prefetcher.LoadedAccountDataEntry - resources []prefetcher.LoadedResourcesEntry + resources []prefetcher.LoadedResourceEntry } testCases := []testCase{ @@ -286,7 +299,7 @@ func TestEvaluatorPrefetcher(t *testing.T) { }, }, }, - resources: []prefetcher.LoadedResourcesEntry{ + resources: []prefetcher.LoadedResourceEntry{ { Address: nil, CreatableIndex: 1000, @@ -322,7 +335,7 @@ func TestEvaluatorPrefetcher(t *testing.T) { }, }, }, - resources: []prefetcher.LoadedResourcesEntry{ + resources: []prefetcher.LoadedResourceEntry{ { Address: makeAddressPtr(2), CreatableIndex: 1001, @@ -362,7 +375,7 @@ func TestEvaluatorPrefetcher(t *testing.T) { }, }, }, - resources: []prefetcher.LoadedResourcesEntry{ + resources: []prefetcher.LoadedResourceEntry{ { Address: makeAddressPtr(2), CreatableIndex: 1001, @@ -413,7 +426,7 @@ func TestEvaluatorPrefetcher(t *testing.T) { }, }, }, - resources: []prefetcher.LoadedResourcesEntry{ + resources: []prefetcher.LoadedResourceEntry{ { Address: makeAddressPtr(2), CreatableIndex: 1001, @@ -455,16 +468,10 @@ func TestEvaluatorPrefetcher(t *testing.T) { AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 100000000}}, }, }, - { - Address: makeAddressPtr(3), - Data: &ledgercore.AccountData{ - AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 0}}, - }, - }, }, - resources: []prefetcher.LoadedResourcesEntry{ + resources: []prefetcher.LoadedResourceEntry{ { - Address: makeAddressPtr(2), + Address: makeAddressPtr(2), // The creator of 1001 in the test ledger CreatableIndex: 1001, CreatableType: basics.AssetCreatable, Resource: &ledgercore.AccountResource{}, @@ -529,14 +536,15 @@ func TestEvaluatorPrefetcher(t *testing.T) { }, */ }, - resources: []prefetcher.LoadedResourcesEntry{ - /* - if we'll decide that we want to prefetch the foreign apps/assets, then this should be enabled + resources: []prefetcher.LoadedResourceEntry{ + /* - if we'll decide that we want to prefetch the foreign assets, then this should be enabled { Address: makeAddressPtr(2), CreatableIndex: 1001, CreatableType: basics.AssetCreatable, Resource: &ledgercore.AccountResource{}, }, + */ { Address: makeAddressPtr(15), CreatableIndex: 2001, @@ -549,7 +557,6 @@ func TestEvaluatorPrefetcher(t *testing.T) { CreatableType: basics.AppCreatable, Resource: nil, }, - */ /* - if we'll decide that we want to prefetch the account local state, then this should be enabled. { address: acctAddrPtr(1), @@ -565,6 +572,95 @@ func TestEvaluatorPrefetcher(t *testing.T) { }, }, }, + { + name: "application transaction using access", + signedTxn: transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: makeAddress(1), + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 10, + Access: []transactions.ResourceRef{ + {Address: makeAddress(4)}, + {Address: makeAddress(5)}, + {Address: makeAddress(6)}, + {App: 2001}, + {App: 2002}, + {Asset: 1001}, + {Asset: 1002}, + // Since we have this cross product we won't prefetch the Address or Asset in it. + {Holding: transactions.HoldingRef{ + Address: 3, + Asset: 7, + }}, + }, + }, + }, + }, + accounts: []prefetcher.LoadedAccountDataEntry{ + { + Address: &feeSinkAddr, + Data: &ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 0}}, + }, + }, + { + Address: makeAddressPtr(1), + Data: &ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 100000000}}, + }, + }, + // 4 and 5 are here because when using Access, if accounts are + // not used in a cross product, they are assumed prefetch + // worthy. + { + Address: makeAddressPtr(4), + Data: &ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 0}}, + }, + }, + { + Address: makeAddressPtr(5), + Data: &ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{MicroAlgos: basics.MicroAlgos{Raw: 0}}, + }, + }, + }, + resources: []prefetcher.LoadedResourceEntry{ + { // Since 1001 is not used in a cross product, it is prefetched + Address: makeAddressPtr(2), // 2 is the creator for 1001 + CreatableIndex: 1001, + CreatableType: basics.AssetCreatable, + Resource: &ledgercore.AccountResource{}, + }, + { + Address: makeAddressPtr(15), // 15 is the creator for 2001 + CreatableIndex: 2001, + CreatableType: basics.AppCreatable, + Resource: &ledgercore.AccountResource{}, + }, + { + Address: nil, + CreatableIndex: 2002, + CreatableType: basics.AppCreatable, + Resource: nil, + }, + { + Address: nil, + CreatableIndex: 10, + CreatableType: basics.AppCreatable, + Resource: nil, + }, + { // Fetch the explicit cross-product resource + Address: makeAddressPtr(6), + CreatableIndex: 1002, + CreatableType: basics.AssetCreatable, + Resource: &ledgercore.AccountResource{}, + }, + }, + }, } for _, testCase := range testCases { @@ -576,7 +672,7 @@ func TestEvaluatorPrefetcher(t *testing.T) { groups[0] = make([]transactions.SignedTxnWithAD, 1) groups[0][0].SignedTxn = testCase.signedTxn - preloadedTxnGroupsCh := prefetcher.PrefetchAccounts(context.Background(), ledger, rnd, groups, feeSinkAddr, config.Consensus[proto]) + preloadedTxnGroupsCh := prefetcher.BlockReferences(context.Background(), ledger, rnd, groups, feeSinkAddr, config.Consensus[proto]) loadedTxnGroup, ok := <-preloadedTxnGroupsCh require.True(t, ok) @@ -628,18 +724,72 @@ func TestAssetLookupError(t *testing.T) { } } - preloadedTxnGroupsCh := prefetcher.PrefetchAccounts(context.Background(), ledger, rnd+100, groups, feeSinkAddr, config.Consensus[proto]) + preloadedTxnGroupsCh := prefetcher.BlockReferences(context.Background(), ledger, rnd+100, groups, feeSinkAddr, config.Consensus[proto]) receivedNumGroups := 0 for loadedTxnGroup := range preloadedTxnGroupsCh { receivedNumGroups++ - if loadedTxnGroup.Err != nil { + if err := loadedTxnGroup.Err; err != nil { errorReceived = true - require.Equal(t, int64(2), loadedTxnGroup.Err.GroupIdx) - require.ErrorIs(t, loadedTxnGroup.Err, assetLookupError{}) - require.Equal(t, makeAddress(2), *loadedTxnGroup.Err.Address) - require.Equal(t, errorTriggerAssetIndex, int(loadedTxnGroup.Err.CreatableIndex)) - require.Equal(t, basics.AssetCreatable, loadedTxnGroup.Err.CreatableType) + require.ErrorContains(t, err, "prefetch failed for groupIdx 2, address: AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGFFWAF4, creatableIndex 1000002, creatableType 0, cause: asset lookup error") + } + require.Equal(t, txnPerGroup, len(loadedTxnGroup.TxnGroup)) + } + require.True(t, errorReceived) + require.Equal(t, numGroups, receivedNumGroups) +} + +// Test for error from LookupKV +func TestBoxLookupError(t *testing.T) { + partitiontest.PartitionTest(t) + + const rnd = 5 + var feeSinkAddr = 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} + ledger := getPrefetcherTestLedger(rnd) + appTxn := + transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: makeAddress(1), + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 1002, + ForeignApps: []basics.AppIndex{2002}, + Boxes: []transactions.BoxRef{{Index: 1, Name: []byte("GOODKV")}}, + }, + }, + } + + errorReceived := false + const numGroups = 5 + const txnPerGroup = 2 + groups := make([][]transactions.SignedTxnWithAD, numGroups) + for i := 0; i < numGroups; i++ { + groups[i] = make([]transactions.SignedTxnWithAD, txnPerGroup) + for j := 0; j < txnPerGroup; j++ { + groups[i][j].SignedTxn = appTxn + if i == 2 { + // force error in asset lookup in the second txn group only + // (need to asssigned the entire Boxes slice to avoid modifying + // other txns) + groups[i][j].SignedTxn.Txn.ApplicationCallTxnFields.Boxes = []transactions.BoxRef{{ + Index: 1, + Name: []byte("BADKV"), + }} + } + } + } + + preloadedTxnGroupsCh := prefetcher.BlockReferences(context.Background(), ledger, rnd+100, groups, feeSinkAddr, config.Consensus[proto]) + + receivedNumGroups := 0 + for loadedTxnGroup := range preloadedTxnGroupsCh { + receivedNumGroups++ + if err := loadedTxnGroup.Err; err != nil { + errorReceived = true + require.ErrorContains(t, err, "prefetch failed for groupIdx 2, kv:") + require.ErrorContains(t, err, "cause: error looking up kv") } require.Equal(t, txnPerGroup, len(loadedTxnGroup.TxnGroup)) } @@ -685,17 +835,14 @@ func TestGetCreatorForRoundError(t *testing.T) { } } } - preloadedTxnGroupsCh := prefetcher.PrefetchAccounts(context.Background(), ledger, rnd+100, groups, feeSinkAddr, config.Consensus[proto]) + preloadedTxnGroupsCh := prefetcher.BlockReferences(context.Background(), ledger, rnd+100, groups, feeSinkAddr, config.Consensus[proto]) receivedNumGroups := 0 for loadedTxnGroup := range preloadedTxnGroupsCh { receivedNumGroups++ - if loadedTxnGroup.Err != nil { + if err := loadedTxnGroup.Err; err != nil { errorReceived = true - require.ErrorIs(t, loadedTxnGroup.Err, getCreatorError{}) - require.Nil(t, loadedTxnGroup.Err.Address) - require.Equal(t, errorTriggerCreatableIndex, int(loadedTxnGroup.Err.CreatableIndex)) - require.Equal(t, basics.AssetCreatable, loadedTxnGroup.Err.CreatableType) + require.ErrorContains(t, err, "prefetch failed for groupIdx 0, address: , creatableIndex 1000001, creatableType 0, cause: get creator error") } require.Equal(t, txnPerGroup, len(loadedTxnGroup.TxnGroup)) } @@ -741,18 +888,15 @@ func TestLookupWithoutRewards(t *testing.T) { } } } - ledger.errorTriggerAddress[createAssetFailedTxn.Txn.Sender] = true - preloadedTxnGroupsCh := prefetcher.PrefetchAccounts(context.Background(), ledger, rnd+100, groups, feeSinkAddr, config.Consensus[proto]) + ledger.errorTriggerAddress = &createAssetFailedTxn.Txn.Sender + preloadedTxnGroupsCh := prefetcher.BlockReferences(context.Background(), ledger, rnd+100, groups, feeSinkAddr, config.Consensus[proto]) receivedNumGroups := 0 for loadedTxnGroup := range preloadedTxnGroupsCh { receivedNumGroups++ - if loadedTxnGroup.Err != nil { + if err := loadedTxnGroup.Err; err != nil { errorReceived = true - require.ErrorIs(t, loadedTxnGroup.Err, lookupError{}) - require.Equal(t, makeAddress(10), *loadedTxnGroup.Err.Address) - require.Equal(t, 0, int(loadedTxnGroup.Err.CreatableIndex)) - require.Equal(t, basics.AssetCreatable, loadedTxnGroup.Err.CreatableType) + require.ErrorContains(t, err, "prefetch failed for groupIdx 0, address: BIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYVWV3M, creatableIndex 0, creatableType 0, cause: lookup error") } require.Equal(t, txnPerGroup, len(loadedTxnGroup.TxnGroup)) } @@ -777,7 +921,7 @@ func TestEvaluatorPrefetcherQueueExpansion(t *testing.T) { type testTransactionCases struct { signedTxn transactions.SignedTxn accounts []prefetcher.LoadedAccountDataEntry - resources []prefetcher.LoadedResourcesEntry + resources []prefetcher.LoadedResourceEntry } txnGroups := make([][]transactions.SignedTxnWithAD, 20000) @@ -800,7 +944,7 @@ func TestEvaluatorPrefetcherQueueExpansion(t *testing.T) { addr += 2 } } - preloadedTxnGroupsCh := prefetcher.PrefetchAccounts(context.Background(), ledger, rnd, txnGroups, feeSinkAddr, config.Consensus[proto]) + preloadedTxnGroupsCh := prefetcher.BlockReferences(context.Background(), ledger, rnd, txnGroups, feeSinkAddr, config.Consensus[proto]) groupsCount := 0 addressCount := 0 uniqueAccounts := make(map[basics.Address]bool) @@ -814,8 +958,13 @@ func TestEvaluatorPrefetcherQueueExpansion(t *testing.T) { } require.Equal(t, len(txnGroups), groupsCount) // the +1 below is for the fee sink address. - require.Equal(t, len(txnGroups)*16*3+1, addressCount) require.Equal(t, len(txnGroups)*16*2+1, len(uniqueAccounts)) + // We no longer bother to return all of the addresses used in a group (which + // would be 3 per transaction). Callers don't care about receiving every + // address/resource, they care about receiving any _new_ addr/resource that + // wasn't in a previous transaction group. Previous ones were returned + // earlier and put in their cache. + require.Equal(t, len(uniqueAccounts), addressCount) } func BenchmarkPrefetcherApps(b *testing.B) { @@ -834,15 +983,19 @@ func BenchmarkPrefetcherApps(b *testing.B) { ApplicationID: 10, Accounts: []basics.Address{ makeAddress(grpIdx + txnIdx + 1), - makeAddress(grpIdx + txnIdx + 1), + makeAddress(grpIdx + txnIdx + 2), }, ForeignApps: []basics.AppIndex{ 2001, - 2002, + 2002 + basics.AppIndex(txnIdx), }, ForeignAssets: []basics.AssetIndex{ - 1001, + 1001 + basics.AssetIndex(txnIdx), }, + Boxes: []transactions.BoxRef{{ + Index: 1, + Name: []byte("some name"), + }}, }, }, } @@ -860,7 +1013,7 @@ func BenchmarkPrefetcherApps(b *testing.B) { } b.ResetTimer() - preloadedTxnGroupsCh := prefetcher.PrefetchAccounts(context.Background(), ledger, rnd, groups, feeSinkAddr, config.Consensus[proto]) + preloadedTxnGroupsCh := prefetcher.BlockReferences(context.Background(), ledger, rnd, groups, feeSinkAddr, config.Consensus[proto]) for k := range preloadedTxnGroupsCh { require.NoError(b, k.Err) } @@ -898,7 +1051,7 @@ func BenchmarkPrefetcherPayment(b *testing.B) { } b.ResetTimer() - preloadedTxnGroupsCh := prefetcher.PrefetchAccounts(context.Background(), ledger, rnd, groups, feeSinkAddr, config.Consensus[proto]) + preloadedTxnGroupsCh := prefetcher.BlockReferences(context.Background(), ledger, rnd, groups, feeSinkAddr, config.Consensus[proto]) for k := range preloadedTxnGroupsCh { require.NoError(b, k.Err) } diff --git a/ledger/evalbench_test.go b/ledger/evalbench_test.go index acd855d6b4..4a1062c5e0 100644 --- a/ledger/evalbench_test.go +++ b/ledger/evalbench_test.go @@ -27,9 +27,10 @@ import ( "testing" "time" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" diff --git a/ledger/ledger.go b/ledger/ledger.go index e1c962129f..9cf818c7e6 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -597,10 +597,22 @@ func (l *Ledger) LookupAsset(rnd basics.Round, addr basics.Address, aidx basics. // LookupAssets loads asset resources that match the request parameters from the ledger. func (l *Ledger) LookupAssets(addr basics.Address, assetIDGT basics.AssetIndex, limit uint64) ([]ledgercore.AssetResourceWithIDs, basics.Round, error) { + l.trackerMu.RLock() + defer l.trackerMu.RUnlock() + resources, lookupRound, err := l.accts.LookupAssetResources(addr, assetIDGT, limit) return resources, lookupRound, err } +// LookupApplications returns the application resources (local state and params) for a given address, with pagination support. +// If includeParams is false, AppParams will not be populated to save memory allocations. +func (l *Ledger) LookupApplications(addr basics.Address, appIDGT basics.AppIndex, limit uint64, includeParams bool) ([]ledgercore.AppResourceWithIDs, basics.Round, error) { + l.trackerMu.RLock() + defer l.trackerMu.RUnlock() + + return l.accts.LookupApplicationResources(addr, appIDGT, limit, includeParams) +} + // lookupResource loads a resource that matches the request parameters from the accounts update func (l *Ledger) lookupResource(rnd basics.Round, addr basics.Address, aidx basics.CreatableIndex, ctype basics.CreatableType) (ledgercore.AccountResource, error) { l.trackerMu.RLock() diff --git a/ledger/ledger_perf_test.go b/ledger/ledger_perf_test.go index 754b8319da..f77145ab7d 100644 --- a/ledger/ledger_perf_test.go +++ b/ledger/ledger_perf_test.go @@ -24,9 +24,10 @@ import ( "strings" "testing" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index 55b28c4e3c..97dd9aaec2 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -31,6 +31,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -51,7 +53,6 @@ import ( "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/db" "github.com/algorand/go-algorand/util/execpool" - "github.com/algorand/go-deadlock" ) const preReleaseDBVersion = 6 diff --git a/ledger/ledgercore/accountdata_test.go b/ledger/ledgercore/accountdata_test.go index caa1a672aa..a6dd01e298 100644 --- a/ledger/ledgercore/accountdata_test.go +++ b/ledger/ledgercore/accountdata_test.go @@ -19,10 +19,11 @@ package ledgercore import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TestBasicsAccountDataRoundtripConversion ensures that basics.AccountData can be converted to diff --git a/ledger/ledgercore/accountresource.go b/ledger/ledgercore/accountresource.go index 8a08c7b935..0cf206ffb3 100644 --- a/ledger/ledgercore/accountresource.go +++ b/ledger/ledgercore/accountresource.go @@ -48,6 +48,14 @@ type AppResource struct { AppParams *basics.AppParams } +// AppResourceWithIDs is used to retrieve an app resource information from the data tier, +// inclusive of the app ID and creator address +type AppResourceWithIDs struct { + AppResource + AppID basics.AppIndex + Creator basics.Address +} + // AssignAccountResourceToAccountData assigns the Asset/App params/holdings contained // in the AccountResource to the given basics.AccountData, creating maps if necessary. // Returns true if the AccountResource contained a new or updated resource, diff --git a/ledger/lruaccts.go b/ledger/lruaccts.go index c344235515..8b49ae8f80 100644 --- a/ledger/lruaccts.go +++ b/ledger/lruaccts.go @@ -166,7 +166,6 @@ func (m *lruAccounts) prune(newSize int) (removed int) { removed++ } - // clear the notFound list - m.notFound = make(map[basics.Address]struct{}, len(m.notFound)) + clear(m.notFound) return } diff --git a/ledger/lruresources.go b/ledger/lruresources.go index 1623b54c79..97070391b7 100644 --- a/ledger/lruresources.go +++ b/ledger/lruresources.go @@ -186,7 +186,6 @@ func (m *lruResources) prune(newSize int) (removed int) { removed++ } - // clear the notFound list - m.notFound = make(map[accountCreatable]struct{}, len(m.notFound)) + clear(m.notFound) return } diff --git a/ledger/perf_test.go b/ledger/perf_test.go index 7b76483d33..d8cf5f6497 100644 --- a/ledger/perf_test.go +++ b/ledger/perf_test.go @@ -21,9 +21,10 @@ import ( "fmt" "testing" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" diff --git a/ledger/simple_test.go b/ledger/simple_test.go index 638f8055b7..250067b3e6 100644 --- a/ledger/simple_test.go +++ b/ledger/simple_test.go @@ -23,6 +23,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -37,7 +39,6 @@ import ( "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) type simpleLedgerCfg struct { diff --git a/ledger/simulation/resources_test.go b/ledger/simulation/resources_test.go index c7f20c0b6a..c060863158 100644 --- a/ledger/simulation/resources_test.go +++ b/ledger/simulation/resources_test.go @@ -19,13 +19,14 @@ package simulation import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestAppAccounts(t *testing.T) { diff --git a/ledger/simulation/simulation_eval_test.go b/ledger/simulation/simulation_eval_test.go index 7e60d829e0..4cb4ae5f44 100644 --- a/ledger/simulation/simulation_eval_test.go +++ b/ledger/simulation/simulation_eval_test.go @@ -25,6 +25,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" @@ -38,9 +41,6 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func uint64ToBytes(num uint64) []byte { diff --git a/ledger/simulation/simulator_test.go b/ledger/simulation/simulator_test.go index 7f416021a6..62baa5bf4a 100644 --- a/ledger/simulation/simulator_test.go +++ b/ledger/simulation/simulator_test.go @@ -21,6 +21,8 @@ import ( "slices" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data" "github.com/algorand/go-algorand/data/basics" @@ -34,7 +36,6 @@ import ( simulationtesting "github.com/algorand/go-algorand/ledger/simulation/testing" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // We want to be careful that the Algod ledger does not move on to another round diff --git a/ledger/simulation/testing/utils.go b/ledger/simulation/testing/utils.go index 0910e0061f..099d0810e1 100644 --- a/ledger/simulation/testing/utils.go +++ b/ledger/simulation/testing/utils.go @@ -21,6 +21,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -35,7 +37,6 @@ import ( ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) // Account contains public and private keys, as well as the state of an account diff --git a/ledger/simulation/tracer_test.go b/ledger/simulation/tracer_test.go index 8335d5ac43..6d07c9d794 100644 --- a/ledger/simulation/tracer_test.go +++ b/ledger/simulation/tracer_test.go @@ -19,11 +19,12 @@ package simulation import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/transactions/logic/mocktracer" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestCursorEvalTracer(t *testing.T) { diff --git a/ledger/spverificationtracker_test.go b/ledger/spverificationtracker_test.go index a46ba5bde4..1a3fdb3b97 100644 --- a/ledger/spverificationtracker_test.go +++ b/ledger/spverificationtracker_test.go @@ -20,6 +20,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" @@ -27,7 +29,6 @@ import ( "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) const defaultStateProofInterval = uint64(256) diff --git a/ledger/store/testing/helpers.go b/ledger/store/testing/helpers.go index f05093c924..5384a06313 100644 --- a/ledger/store/testing/helpers.go +++ b/ledger/store/testing/helpers.go @@ -21,10 +21,11 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "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. diff --git a/ledger/store/trackerdb/data_test.go b/ledger/store/trackerdb/data_test.go index 3078f0b1a3..b77c7a56d9 100644 --- a/ledger/store/trackerdb/data_test.go +++ b/ledger/store/trackerdb/data_test.go @@ -23,6 +23,9 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" @@ -31,8 +34,6 @@ import ( 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/assert" - "github.com/stretchr/testify/require" ) func TestResourcesDataApp(t *testing.T) { diff --git a/ledger/store/trackerdb/dualdriver/accounts_reader.go b/ledger/store/trackerdb/dualdriver/accounts_reader.go index 27bede02b7..ff3961843a 100644 --- a/ledger/store/trackerdb/dualdriver/accounts_reader.go +++ b/ledger/store/trackerdb/dualdriver/accounts_reader.go @@ -17,9 +17,10 @@ package dualdriver import ( + "github.com/google/go-cmp/cmp" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/store/trackerdb" - "github.com/google/go-cmp/cmp" ) type accountsReader struct { diff --git a/ledger/store/trackerdb/dualdriver/accounts_reader_ext.go b/ledger/store/trackerdb/dualdriver/accounts_reader_ext.go index 5696955516..d469635841 100644 --- a/ledger/store/trackerdb/dualdriver/accounts_reader_ext.go +++ b/ledger/store/trackerdb/dualdriver/accounts_reader_ext.go @@ -19,11 +19,12 @@ package dualdriver import ( "context" + "github.com/google/go-cmp/cmp" + "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/ledger/store/trackerdb" - "github.com/google/go-cmp/cmp" ) type accountsReaderExt struct { diff --git a/ledger/store/trackerdb/dualdriver/accounts_writer_ext.go b/ledger/store/trackerdb/dualdriver/accounts_writer_ext.go index ced4d0aa72..303e0fd334 100644 --- a/ledger/store/trackerdb/dualdriver/accounts_writer_ext.go +++ b/ledger/store/trackerdb/dualdriver/accounts_writer_ext.go @@ -18,6 +18,7 @@ package dualdriver import ( "context" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/ledger/store/trackerdb" diff --git a/ledger/store/trackerdb/dualdriver/dualdriver.go b/ledger/store/trackerdb/dualdriver/dualdriver.go index c88729636d..7cd1e3e2ce 100644 --- a/ledger/store/trackerdb/dualdriver/dualdriver.go +++ b/ledger/store/trackerdb/dualdriver/dualdriver.go @@ -24,12 +24,13 @@ import ( "sync" "time" + "github.com/google/go-cmp/cmp" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/encoded" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/util/db" - "github.com/google/go-cmp/cmp" ) // ErrInconsistentResult is returned when the two stores return different results. diff --git a/ledger/store/trackerdb/dualdriver/online_accounts_reader.go b/ledger/store/trackerdb/dualdriver/online_accounts_reader.go index 8ffcb91334..f30ed07a4d 100644 --- a/ledger/store/trackerdb/dualdriver/online_accounts_reader.go +++ b/ledger/store/trackerdb/dualdriver/online_accounts_reader.go @@ -17,10 +17,11 @@ package dualdriver import ( + "github.com/google/go-cmp/cmp" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/ledger/store/trackerdb" - "github.com/google/go-cmp/cmp" ) type onlineAccountsReader struct { diff --git a/ledger/store/trackerdb/dualdriver/stateproof_reader.go b/ledger/store/trackerdb/dualdriver/stateproof_reader.go index ef15ed2ee6..926b8abb46 100644 --- a/ledger/store/trackerdb/dualdriver/stateproof_reader.go +++ b/ledger/store/trackerdb/dualdriver/stateproof_reader.go @@ -19,10 +19,11 @@ package dualdriver import ( "context" + "github.com/google/go-cmp/cmp" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/ledger/store/trackerdb" - "github.com/google/go-cmp/cmp" ) type stateproofReader struct { diff --git a/ledger/store/trackerdb/dualdriver/stateproof_writer.go b/ledger/store/trackerdb/dualdriver/stateproof_writer.go index 026522bf77..46a5806cb4 100644 --- a/ledger/store/trackerdb/dualdriver/stateproof_writer.go +++ b/ledger/store/trackerdb/dualdriver/stateproof_writer.go @@ -18,6 +18,7 @@ package dualdriver import ( "context" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/ledger/store/trackerdb" diff --git a/ledger/store/trackerdb/generickv/init_accounts.go b/ledger/store/trackerdb/generickv/init_accounts.go index e38b77093a..32a2d20407 100644 --- a/ledger/store/trackerdb/generickv/init_accounts.go +++ b/ledger/store/trackerdb/generickv/init_accounts.go @@ -20,10 +20,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) type dbForInit interface { diff --git a/ledger/store/trackerdb/pebbledbdriver/pebbledriver.go b/ledger/store/trackerdb/pebbledbdriver/pebbledriver.go index 341457e024..2fe5d1301a 100644 --- a/ledger/store/trackerdb/pebbledbdriver/pebbledriver.go +++ b/ledger/store/trackerdb/pebbledbdriver/pebbledriver.go @@ -24,14 +24,15 @@ import ( "runtime" "time" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/vfs" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/ledger/store/trackerdb/generickv" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/util/db" - "github.com/cockroachdb/pebble" - "github.com/cockroachdb/pebble/bloom" - "github.com/cockroachdb/pebble/vfs" ) const ( diff --git a/ledger/store/trackerdb/pebbledbdriver/testing.go b/ledger/store/trackerdb/pebbledbdriver/testing.go index 57483aa1b7..6b694ce5c6 100644 --- a/ledger/store/trackerdb/pebbledbdriver/testing.go +++ b/ledger/store/trackerdb/pebbledbdriver/testing.go @@ -20,11 +20,12 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) // OpenForTesting opens a sqlite db file for testing purposes. diff --git a/ledger/store/trackerdb/sqlitedriver/accountsV2.go b/ledger/store/trackerdb/sqlitedriver/accountsV2.go index d8eae963ae..97777ae994 100644 --- a/ledger/store/trackerdb/sqlitedriver/accountsV2.go +++ b/ledger/store/trackerdb/sqlitedriver/accountsV2.go @@ -24,13 +24,14 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "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/ledger/store/trackerdb" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" - "github.com/stretchr/testify/require" ) type accountsV2Reader struct { diff --git a/ledger/store/trackerdb/sqlitedriver/accountsV2_test.go b/ledger/store/trackerdb/sqlitedriver/accountsV2_test.go index d3c6dad6e6..d6ba02dcb8 100644 --- a/ledger/store/trackerdb/sqlitedriver/accountsV2_test.go +++ b/ledger/store/trackerdb/sqlitedriver/accountsV2_test.go @@ -19,8 +19,9 @@ package sqlitedriver import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestRowidsToChunkedArgs(t *testing.T) { diff --git a/ledger/store/trackerdb/sqlitedriver/catchpoint.go b/ledger/store/trackerdb/sqlitedriver/catchpoint.go index f3d8242af7..8a986a3b9f 100644 --- a/ledger/store/trackerdb/sqlitedriver/catchpoint.go +++ b/ledger/store/trackerdb/sqlitedriver/catchpoint.go @@ -23,13 +23,14 @@ import ( "fmt" "time" + "github.com/mattn/go-sqlite3" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/encoded" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" - "github.com/mattn/go-sqlite3" ) type catchpointReader struct { diff --git a/ledger/store/trackerdb/sqlitedriver/catchpoint_test.go b/ledger/store/trackerdb/sqlitedriver/catchpoint_test.go index aa5c54137c..600fbd7331 100644 --- a/ledger/store/trackerdb/sqlitedriver/catchpoint_test.go +++ b/ledger/store/trackerdb/sqlitedriver/catchpoint_test.go @@ -21,12 +21,13 @@ import ( "crypto/rand" "testing" + "github.com/stretchr/testify/require" + "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/ledger/store/trackerdb" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // Test functions operating on catchpointfirststageinfo table. diff --git a/ledger/store/trackerdb/sqlitedriver/encodedAccountsIter.go b/ledger/store/trackerdb/sqlitedriver/encodedAccountsIter.go index 79861c74d6..3266e84f50 100644 --- a/ledger/store/trackerdb/sqlitedriver/encodedAccountsIter.go +++ b/ledger/store/trackerdb/sqlitedriver/encodedAccountsIter.go @@ -20,11 +20,12 @@ import ( "context" "database/sql" + "github.com/algorand/msgp/msgp" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/encoded" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/util/db" - "github.com/algorand/msgp/msgp" ) // encodedAccountsBatchIter allows us to iterate over the accounts data stored in the accountbase table. diff --git a/ledger/store/trackerdb/sqlitedriver/schema.go b/ledger/store/trackerdb/sqlitedriver/schema.go index be2276ff6f..92b957b039 100644 --- a/ledger/store/trackerdb/sqlitedriver/schema.go +++ b/ledger/store/trackerdb/sqlitedriver/schema.go @@ -25,6 +25,8 @@ import ( "fmt" "time" + "github.com/mattn/go-sqlite3" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto/merklesignature" "github.com/algorand/go-algorand/crypto/merkletrie" @@ -35,7 +37,6 @@ import ( "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{ diff --git a/ledger/store/trackerdb/sqlitedriver/schema_test.go b/ledger/store/trackerdb/sqlitedriver/schema_test.go index 683d0932f6..f1b46ab168 100644 --- a/ledger/store/trackerdb/sqlitedriver/schema_test.go +++ b/ledger/store/trackerdb/sqlitedriver/schema_test.go @@ -23,6 +23,8 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" @@ -35,7 +37,6 @@ import ( "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) { diff --git a/ledger/store/trackerdb/sqlitedriver/sql.go b/ledger/store/trackerdb/sqlitedriver/sql.go index f88adb7611..eeb298690f 100644 --- a/ledger/store/trackerdb/sqlitedriver/sql.go +++ b/ledger/store/trackerdb/sqlitedriver/sql.go @@ -19,6 +19,7 @@ package sqlitedriver import ( "database/sql" "fmt" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/ledger/store/trackerdb" diff --git a/ledger/store/trackerdb/sqlitedriver/sql_test.go b/ledger/store/trackerdb/sqlitedriver/sql_test.go index ac00b397d9..f1b1ac3299 100644 --- a/ledger/store/trackerdb/sqlitedriver/sql_test.go +++ b/ledger/store/trackerdb/sqlitedriver/sql_test.go @@ -21,13 +21,14 @@ import ( "database/sql" "testing" + "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" storetesting "github.com/algorand/go-algorand/ledger/store/testing" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/mattn/go-sqlite3" - "github.com/stretchr/testify/require" ) func TestKeyPrefixIntervalPreprocessing(t *testing.T) { diff --git a/ledger/store/trackerdb/sqlitedriver/sqlitedriver.go b/ledger/store/trackerdb/sqlitedriver/sqlitedriver.go index 5f8e2c040a..a3d988b849 100644 --- a/ledger/store/trackerdb/sqlitedriver/sqlitedriver.go +++ b/ledger/store/trackerdb/sqlitedriver/sqlitedriver.go @@ -23,13 +23,14 @@ import ( "testing" "time" + "github.com/mattn/go-sqlite3" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/encoded" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" - "github.com/mattn/go-sqlite3" ) type trackerSQLStore struct { diff --git a/ledger/store/trackerdb/sqlitedriver/testing.go b/ledger/store/trackerdb/sqlitedriver/testing.go index 0f7bab6307..33021b3f79 100644 --- a/ledger/store/trackerdb/sqlitedriver/testing.go +++ b/ledger/store/trackerdb/sqlitedriver/testing.go @@ -22,6 +22,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" @@ -29,7 +31,6 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" - "github.com/stretchr/testify/require" ) // OpenForTesting opens a sqlite db file for testing purposes. diff --git a/ledger/store/trackerdb/testsuite/accounts_ext_kv_test.go b/ledger/store/trackerdb/testsuite/accounts_ext_kv_test.go index f2b5c79fd2..189c188c4d 100644 --- a/ledger/store/trackerdb/testsuite/accounts_ext_kv_test.go +++ b/ledger/store/trackerdb/testsuite/accounts_ext_kv_test.go @@ -19,13 +19,14 @@ package testsuite import ( "context" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) func init() { diff --git a/ledger/store/trackerdb/testsuite/accounts_kv_test.go b/ledger/store/trackerdb/testsuite/accounts_kv_test.go index 99c7d00e95..aed556f696 100644 --- a/ledger/store/trackerdb/testsuite/accounts_kv_test.go +++ b/ledger/store/trackerdb/testsuite/accounts_kv_test.go @@ -17,10 +17,11 @@ package testsuite import ( + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/store/trackerdb" ledgertesting "github.com/algorand/go-algorand/ledger/testing" - "github.com/stretchr/testify/require" ) func init() { diff --git a/ledger/store/trackerdb/testsuite/dbsemantics_test.go b/ledger/store/trackerdb/testsuite/dbsemantics_test.go index 72ea207462..fe12dd4da4 100644 --- a/ledger/store/trackerdb/testsuite/dbsemantics_test.go +++ b/ledger/store/trackerdb/testsuite/dbsemantics_test.go @@ -19,8 +19,9 @@ package testsuite import ( "context" - "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/ledger/store/trackerdb" ) func init() { diff --git a/ledger/store/trackerdb/testsuite/migration_test.go b/ledger/store/trackerdb/testsuite/migration_test.go index aa3060765d..fb6ce4d285 100644 --- a/ledger/store/trackerdb/testsuite/migration_test.go +++ b/ledger/store/trackerdb/testsuite/migration_test.go @@ -19,13 +19,14 @@ package testsuite import ( "context" + "github.com/stretchr/testify/require" + "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/ledger/store/trackerdb" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) func init() { diff --git a/ledger/store/trackerdb/testsuite/onlineaccounts_kv_test.go b/ledger/store/trackerdb/testsuite/onlineaccounts_kv_test.go index fb65f49b93..b2acba56ab 100644 --- a/ledger/store/trackerdb/testsuite/onlineaccounts_kv_test.go +++ b/ledger/store/trackerdb/testsuite/onlineaccounts_kv_test.go @@ -17,11 +17,12 @@ package testsuite import ( + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/store/trackerdb" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) func init() { diff --git a/ledger/store/trackerdb/testsuite/pebbledb_test.go b/ledger/store/trackerdb/testsuite/pebbledb_test.go index fd3d42864c..6bfbe59f7f 100644 --- a/ledger/store/trackerdb/testsuite/pebbledb_test.go +++ b/ledger/store/trackerdb/testsuite/pebbledb_test.go @@ -20,10 +20,11 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/ledger/store/trackerdb/pebbledbdriver" "github.com/algorand/go-algorand/logging" - "github.com/stretchr/testify/require" ) func TestPebbleDB(t *testing.T) { diff --git a/ledger/store/trackerdb/testsuite/sqlitedb_test.go b/ledger/store/trackerdb/testsuite/sqlitedb_test.go index 94a6b93634..a145a9507e 100644 --- a/ledger/store/trackerdb/testsuite/sqlitedb_test.go +++ b/ledger/store/trackerdb/testsuite/sqlitedb_test.go @@ -20,10 +20,11 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/ledger/store/trackerdb/sqlitedriver" "github.com/algorand/go-algorand/logging" - "github.com/stretchr/testify/require" ) func TestSqliteDB(t *testing.T) { diff --git a/ledger/store/trackerdb/testsuite/stateproofs_kv_test.go b/ledger/store/trackerdb/testsuite/stateproofs_kv_test.go index a211257826..e0d2558693 100644 --- a/ledger/store/trackerdb/testsuite/stateproofs_kv_test.go +++ b/ledger/store/trackerdb/testsuite/stateproofs_kv_test.go @@ -19,10 +19,11 @@ package testsuite import ( "context" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/ledger/store/trackerdb" - "github.com/stretchr/testify/require" ) func init() { diff --git a/ledger/store/trackerdb/testsuite/utils_test.go b/ledger/store/trackerdb/testsuite/utils_test.go index 658c852231..ec59df1daa 100644 --- a/ledger/store/trackerdb/testsuite/utils_test.go +++ b/ledger/store/trackerdb/testsuite/utils_test.go @@ -26,6 +26,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" @@ -35,7 +37,6 @@ import ( "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" ) type customT struct { diff --git a/ledger/testing/consensusRange.go b/ledger/testing/consensusRange.go index 13b5f2f61b..9f52c647bd 100644 --- a/ledger/testing/consensusRange.go +++ b/ledger/testing/consensusRange.go @@ -21,9 +21,10 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) var consensusByNumber = []protocol.ConsensusVersion{ diff --git a/ledger/testing/consensusRange_test.go b/ledger/testing/consensusRange_test.go index 1b50e4b1ef..d6fa019605 100644 --- a/ledger/testing/consensusRange_test.go +++ b/ledger/testing/consensusRange_test.go @@ -19,10 +19,11 @@ package testing import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TestReleasedVersion ensures that the necessary tidying is done when a new diff --git a/ledger/testing/randomAccounts.go b/ledger/testing/randomAccounts.go index ee5a5a6f66..af86364236 100644 --- a/ledger/testing/randomAccounts.go +++ b/ledger/testing/randomAccounts.go @@ -23,9 +23,8 @@ import ( "github.com/algorand/go-algorand/config/bounds" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" ) 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} diff --git a/ledger/testing/randomAccounts_test.go b/ledger/testing/randomAccounts_test.go index cadd9aa1cb..334710eb18 100644 --- a/ledger/testing/randomAccounts_test.go +++ b/ledger/testing/randomAccounts_test.go @@ -20,10 +20,11 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/test/reflectionhelpers" - "github.com/stretchr/testify/assert" ) func TestAccounts(t *testing.T) { diff --git a/ledger/tracker.go b/ledger/tracker.go index 3b95df3240..413050c2e5 100644 --- a/ledger/tracker.go +++ b/ledger/tracker.go @@ -25,6 +25,8 @@ import ( "sync/atomic" "time" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" @@ -37,7 +39,6 @@ import ( "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" - "github.com/algorand/go-deadlock" ) // ledgerTracker defines part of the API for any state machine that diff --git a/ledger/txnbench_test.go b/ledger/txnbench_test.go index 99d854998a..53a08a101a 100644 --- a/ledger/txnbench_test.go +++ b/ledger/txnbench_test.go @@ -22,13 +22,14 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/ledger/ledgercore" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/protocol" - "github.com/stretchr/testify/require" ) // BenchmarkTxnTypes compares the execution time of various txn types diff --git a/ledger/voters_test.go b/ledger/voters_test.go index 56a7f61fc8..836ac2e453 100644 --- a/ledger/voters_test.go +++ b/ledger/voters_test.go @@ -20,6 +20,8 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" @@ -29,7 +31,6 @@ import ( 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 addBlockToAccountsUpdate(t *testing.T, blk bookkeeping.Block, ml *mockLedgerForTracker) { diff --git a/libgoal/libgoal.go b/libgoal/libgoal.go index 876c0778ff..da6106e1e5 100644 --- a/libgoal/libgoal.go +++ b/libgoal/libgoal.go @@ -24,20 +24,19 @@ import ( "path/filepath" "time" - algodclient "github.com/algorand/go-algorand/daemon/algod/api/client" - v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" - kmdclient "github.com/algorand/go-algorand/daemon/kmd/client" - "github.com/algorand/go-algorand/ledger/ledgercore" - "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" + algodclient "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" "github.com/algorand/go-algorand/daemon/algod/api/spec/common" modelV2 "github.com/algorand/go-algorand/daemon/algod/api/spec/v2" + kmdclient "github.com/algorand/go-algorand/daemon/kmd/client" "github.com/algorand/go-algorand/daemon/kmd/lib/kmdapi" "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/nodecontrol" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util" @@ -1196,20 +1195,22 @@ func MakeDryrunStateGenerated(client Client, txnOrStxnOrSlice interface{}, other apps := []basics.AppIndex{tx.ApplicationID} apps = append(apps, tx.ForeignApps...) for _, appIdx := range apps { - var appParams model.ApplicationParams + var appParams *model.ApplicationParams if appIdx == 0 { // if it is an app create txn then use params from the txn - appParams.ApprovalProgram = tx.ApprovalProgram - appParams.ClearStateProgram = tx.ClearStateProgram - appParams.GlobalStateSchema = &model.ApplicationStateSchema{ - NumUint: tx.GlobalStateSchema.NumUint, - NumByteSlice: tx.GlobalStateSchema.NumByteSlice, - } - appParams.LocalStateSchema = &model.ApplicationStateSchema{ - NumUint: tx.LocalStateSchema.NumUint, - NumByteSlice: tx.LocalStateSchema.NumByteSlice, + appParams = &model.ApplicationParams{ + ApprovalProgram: tx.ApprovalProgram, + ClearStateProgram: tx.ClearStateProgram, + GlobalStateSchema: &model.ApplicationStateSchema{ + NumUint: tx.GlobalStateSchema.NumUint, + NumByteSlice: tx.GlobalStateSchema.NumByteSlice, + }, + LocalStateSchema: &model.ApplicationStateSchema{ + NumUint: tx.LocalStateSchema.NumUint, + NumByteSlice: tx.LocalStateSchema.NumByteSlice, + }, + Creator: tx.Sender.String(), } - appParams.Creator = tx.Sender.String() // zero is not acceptable by ledger in dryrun/debugger appIdx = defaultAppIdx } else { diff --git a/libgoal/libgoal_test.go b/libgoal/libgoal_test.go index 9a008d9bf2..6fd3261a7c 100644 --- a/libgoal/libgoal_test.go +++ b/libgoal/libgoal_test.go @@ -20,12 +20,13 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestValidRounds(t *testing.T) { diff --git a/libgoal/participation/participation_test.go b/libgoal/participation/participation_test.go index e5f8187dc8..9d6746fb39 100644 --- a/libgoal/participation/participation_test.go +++ b/libgoal/participation/participation_test.go @@ -17,13 +17,13 @@ package participation import ( - "github.com/algorand/go-algorand/data/account" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/test/partitiontest" + "testing" "github.com/stretchr/testify/require" - "testing" + "github.com/algorand/go-algorand/data/account" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/test/partitiontest" ) func TestGenParticipationKeysTo_Install(t *testing.T) { diff --git a/logging/cyclicWriter.go b/logging/cyclicWriter.go index 45f40718a5..eb8b6deb8a 100644 --- a/logging/cyclicWriter.go +++ b/logging/cyclicWriter.go @@ -26,8 +26,9 @@ import ( "text/template" "time" - "github.com/algorand/go-algorand/util" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/util" ) // CyclicFileWriter implements the io.Writer interface and wraps an underlying file. diff --git a/logging/cyclicWriter_test.go b/logging/cyclicWriter_test.go index 7c16e730b7..f5b16caa9d 100644 --- a/logging/cyclicWriter_test.go +++ b/logging/cyclicWriter_test.go @@ -24,8 +24,9 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func testCyclicWrite(t *testing.T, liveFileName, archiveFileName string) { diff --git a/logging/logBuffer_test.go b/logging/logBuffer_test.go index cb448d40ba..c378b93a26 100644 --- a/logging/logBuffer_test.go +++ b/logging/logBuffer_test.go @@ -22,8 +22,9 @@ import ( "io" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) type logBufferTestFixture struct { diff --git a/logging/log_test.go b/logging/log_test.go index 4df219f26e..b75c7eacf9 100644 --- a/logging/log_test.go +++ b/logging/log_test.go @@ -22,8 +22,9 @@ import ( "sync/atomic" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) /* diff --git a/logging/telemetryCommon.go b/logging/telemetryCommon.go index a7d087d1aa..695acdee8d 100644 --- a/logging/telemetryCommon.go +++ b/logging/telemetryCommon.go @@ -20,8 +20,9 @@ import ( "context" "sync" - "github.com/algorand/go-deadlock" "github.com/sirupsen/logrus" + + "github.com/algorand/go-deadlock" ) type telemetryHook interface { diff --git a/logging/telemetryConfig_test.go b/logging/telemetryConfig_test.go index b99415e5ae..3eb44fe725 100644 --- a/logging/telemetryConfig_test.go +++ b/logging/telemetryConfig_test.go @@ -22,8 +22,9 @@ import ( "path/filepath" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func Test_loadTelemetryConfig(t *testing.T) { diff --git a/netdeploy/remote/bootstrappedNetwork_test.go b/netdeploy/remote/bootstrappedNetwork_test.go index b0761c4895..316acce273 100644 --- a/netdeploy/remote/bootstrappedNetwork_test.go +++ b/netdeploy/remote/bootstrappedNetwork_test.go @@ -20,8 +20,9 @@ import ( "path/filepath" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestLoadBootstrappedData(t *testing.T) { diff --git a/network/addr/addr.go b/network/addr/addr.go index 53db9e30b8..68dbd5cbbb 100644 --- a/network/addr/addr.go +++ b/network/addr/addr.go @@ -18,6 +18,7 @@ package addr import ( "errors" + "fmt" "net/url" "regexp" "strings" @@ -29,6 +30,8 @@ var errURLNoHost = errors.New("could not parse a host from url") var errURLColonHost = errors.New("host name starts with a colon") +var errMultiaddrParse = errors.New("failed to parse multiaddr") + // HostColonPortPattern matches "^[-a-zA-Z0-9.]+:\\d+$" e.g. "foo.com.:1234" var HostColonPortPattern = regexp.MustCompile(`^[-a-zA-Z0-9.]+:\d+$`) @@ -78,7 +81,10 @@ func IsMultiaddr(addr string) bool { func ParseHostOrURLOrMultiaddr(addr string) (string, error) { if strings.HasPrefix(addr, "/") && !strings.HasPrefix(addr, "//") { // multiaddr starts with '/' but not '//' which is possible for scheme relative URLS _, err := multiaddr.NewMultiaddr(addr) - return addr, err + if err != nil { + return "", fmt.Errorf("%w: %v", errMultiaddrParse, err) + } + return addr, nil } url, err := ParseHostOrURL(addr) if err != nil { diff --git a/network/addr/addr_test.go b/network/addr/addr_test.go index 9bf6a2ce9f..f725606983 100644 --- a/network/addr/addr_test.go +++ b/network/addr/addr_test.go @@ -20,8 +20,9 @@ import ( "net/url" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) type urlCase struct { @@ -114,14 +115,18 @@ func TestParseHostURLOrMultiaddr(t *testing.T) { "/ip4/192.255.2.8/tcp/8180/ws", } - badMultiAddrs := []string{ - "/ip4/256.256.256.256/tcp/8080", // Invalid IPv4 address. - "/ip4/127.0.0.1/abc/8080", // abc is not a valid protocol. - "/ip4/127.0.0.1/tcp/abc", // Port is not a valid number. - "/unix", // Unix protocol without a path is invalid. - "/ip4/127.0.0.1/tcp", // Missing a port after tcp - "/p2p/invalidPeerID", // Invalid peer ID after p2p. - "ip4/127.0.0.1/tcp/8080", // Missing starting /. + badMultiAddrs := []struct { + addr string + err error + }{ + {"/ip4/256.256.256.256/tcp/8080", errMultiaddrParse}, // Invalid IPv4 address. + {"/ip4/127.0.0.1/abc/8080", errMultiaddrParse}, // abc is not a valid protocol. + {"/ip4/127.0.0.1/tcp/abc", errMultiaddrParse}, // Port is not a valid number. + {"/unix", errMultiaddrParse}, // Unix protocol without a path is invalid. + {"/ip4/127.0.0.1/tcp", errMultiaddrParse}, // Missing a port after tcp + {"/p2p/invalidPeerID", errMultiaddrParse}, // Invalid peer ID after p2p. + {"ip4/127.0.0.1/tcp/8080", errURLNoHost}, // Missing starting / - parsed as URL. + {":1234", errURLColonHost}, // Host starts with colon (not IPv6). } for _, addr := range validMultiAddrs { @@ -133,11 +138,11 @@ func TestParseHostURLOrMultiaddr(t *testing.T) { }) } - for _, addr := range badMultiAddrs { - t.Run(addr, func(t *testing.T) { - _, err := ParseHostOrURLOrMultiaddr(addr) - require.Error(t, err) - require.False(t, IsMultiaddr(addr)) + for _, tc := range badMultiAddrs { + t.Run(tc.addr, func(t *testing.T) { + _, err := ParseHostOrURLOrMultiaddr(tc.addr) + require.ErrorIs(t, err, tc.err) + require.False(t, IsMultiaddr(tc.addr)) }) } diff --git a/network/hybridNetwork_test.go b/network/hybridNetwork_test.go index d15ca20066..7d975555e3 100644 --- a/network/hybridNetwork_test.go +++ b/network/hybridNetwork_test.go @@ -21,11 +21,12 @@ import ( "testing" "time" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/stretchr/testify/require" ) // TestHybridNetwork_DuplicateConn checks the same nodes do not connect over ws and p2p. diff --git a/network/limitcaller/rateLimitingTransport_test.go b/network/limitcaller/rateLimitingTransport_test.go index 6e7e007f20..fb8955b34d 100644 --- a/network/limitcaller/rateLimitingTransport_test.go +++ b/network/limitcaller/rateLimitingTransport_test.go @@ -21,8 +21,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) type ctStore struct { diff --git a/network/limited_reader_slurper_test.go b/network/limited_reader_slurper_test.go index 7e02dbb323..55c535f0ca 100644 --- a/network/limited_reader_slurper_test.go +++ b/network/limited_reader_slurper_test.go @@ -190,7 +190,7 @@ func TestLimitedReaderSlurperPerMessageMaxSize(t *testing.T) { b = make([]byte, dataSize) crypto.RandBytes(b[:]) err := slurper.Read(bytes.NewBuffer(b)) - require.Error(t, err) + require.ErrorIs(t, err, ErrIncomingMsgTooLarge) } } } diff --git a/network/mesh.go b/network/mesh.go index c755f72e20..4f21ed329e 100644 --- a/network/mesh.go +++ b/network/mesh.go @@ -23,9 +23,10 @@ import ( "sync" "time" + "github.com/libp2p/go-libp2p/p2p/discovery/backoff" + "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network/p2p" - "github.com/libp2p/go-libp2p/p2p/discovery/backoff" ) const meshThreadInterval = time.Minute diff --git a/network/mesh_test.go b/network/mesh_test.go index b2f8a76d7c..b929e7347c 100644 --- a/network/mesh_test.go +++ b/network/mesh_test.go @@ -23,16 +23,17 @@ import ( "testing" "time" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network/limitcaller" p2piface "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - pubsub "github.com/libp2p/go-libp2p-pubsub" - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/stretchr/testify/require" ) // mockP2PService implements p2p.Service and counts DialPeersUntilTargetCount invocations. @@ -46,6 +47,7 @@ func (m *mockP2PService) IDSigner() *p2piface.PeerIDChallengeSigner { return nil func (m *mockP2PService) AddrInfo() peer.AddrInfo { return peer.AddrInfo{} } func (m *mockP2PService) NetworkNotify(network.Notifiee) {} func (m *mockP2PService) NetworkStopNotify(network.Notifiee) {} +func (m *mockP2PService) UnprotectPeer(peer.ID) {} func (m *mockP2PService) DialPeersUntilTargetCount(int) bool { m.dialCount.Add(1); return true } func (m *mockP2PService) ClosePeer(peer.ID) error { return nil } func (m *mockP2PService) Conns() []network.Conn { return nil } diff --git a/network/msgCompressor.go b/network/msgCompressor.go index 0aa79538d9..b0d4c7ec75 100644 --- a/network/msgCompressor.go +++ b/network/msgCompressor.go @@ -18,6 +18,7 @@ package network import ( "bytes" + "errors" "fmt" "io" "sync/atomic" @@ -219,7 +220,11 @@ func (c *wsPeerMsgCodec) decompress(tag protocol.Tag, data []byte) ([]byte, erro if c.avdec.enabled { res, err := c.avdec.convert(data) if err != nil { - c.log.Warnf("peer %s vote decompress error: %v", c.origin, err) + if errors.Is(err, vpack.ErrLikelyUncompressed) { + // allow uncompressed AV to pass through without logging an error + } else { + c.log.Warnf("peer %s vote decompress error: %v", c.origin, err) + } // fall back to original data return data, nil } diff --git a/network/msgCompressor_test.go b/network/msgCompressor_test.go index 76717b4914..9372b80258 100644 --- a/network/msgCompressor_test.go +++ b/network/msgCompressor_test.go @@ -24,15 +24,16 @@ import ( "time" "github.com/DataDog/zstd" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var ( @@ -79,7 +80,7 @@ func TestZstdDecompress(t *testing.T) { compressed, err = zstd.Compress(nil, msg) require.NoError(t, err) decompressed, err = d.convert(compressed) - require.Error(t, err) + require.ErrorContains(t, err, `proposal data is too large: 20971530`) require.Nil(t, decompressed) } diff --git a/network/netidentity.go b/network/netidentity.go index 317f7f0017..02555fd3c4 100644 --- a/network/netidentity.go +++ b/network/netidentity.go @@ -21,9 +21,10 @@ import ( "fmt" "net/http" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-deadlock" ) // netidentity.go implements functionality to participate in an "Identity Challenge Exchange" diff --git a/network/netidentity_test.go b/network/netidentity_test.go index 396defde0d..16625699ad 100644 --- a/network/netidentity_test.go +++ b/network/netidentity_test.go @@ -21,10 +21,11 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // if the scheme has a dedup name, attach to headers. otherwise, don't @@ -88,7 +89,8 @@ func TestIdentityChallengeSchemeVerifyRequestAndAttachResponse(t *testing.T) { require.Empty(t, r.Get(IdentityChallengeHeader)) require.Empty(t, chal) require.Empty(t, key) - require.Error(t, err) + var corruptInputErr base64.CorruptInputError + require.ErrorAs(t, err, &corruptInputErr) // happy path: response should be attached here h = http.Header{} @@ -198,7 +200,7 @@ func TestIdentityChallengeSchemeBadSignature(t *testing.T) { require.Empty(t, r.Get(IdentityChallengeHeader)) require.Empty(t, respChal) require.Empty(t, key) - require.Error(t, err) + require.ErrorContains(t, err, `identity challenge incorrectly signed`) } // TestIdentityChallengeSchemeBadPayload tests that the scheme will @@ -216,7 +218,8 @@ func TestIdentityChallengeSchemeBadPayload(t *testing.T) { require.Empty(t, r.Get(IdentityChallengeHeader)) require.Empty(t, respChal) require.Empty(t, key) - require.Error(t, err) + var corruptInputErr base64.CorruptInputError + require.ErrorAs(t, err, &corruptInputErr) } // TestIdentityChallengeSchemeBadResponseSignature tests that the scheme will @@ -247,7 +250,7 @@ func TestIdentityChallengeSchemeBadResponseSignature(t *testing.T) { key2, verificationMsg, err := i.VerifyResponse(r, origChal) require.Empty(t, key2) require.Empty(t, verificationMsg) - require.Error(t, err) + require.ErrorContains(t, err, `challenge response incorrectly signed`) } // TestIdentityChallengeSchemeBadResponsePayload tests that the scheme will @@ -269,7 +272,8 @@ func TestIdentityChallengeSchemeBadResponsePayload(t *testing.T) { key2, verificationMsg, err := i.VerifyResponse(r, origChal) require.Empty(t, key2) require.Empty(t, verificationMsg) - require.Error(t, err) + var corruptInputErr base64.CorruptInputError + require.ErrorAs(t, err, &corruptInputErr) } // TestIdentityChallengeSchemeWrongChallenge the scheme will @@ -295,7 +299,7 @@ func TestIdentityChallengeSchemeWrongChallenge(t *testing.T) { key2, verificationMsg, err := i.VerifyResponse(r, newIdentityChallengeValue()) require.Empty(t, key2) require.Empty(t, verificationMsg) - require.Error(t, err) + require.ErrorContains(t, err, `challenge response did not contain originally issued challenge value`) } func TestNewIdentityTracker(t *testing.T) { diff --git a/network/p2p/http.go b/network/p2p/http.go index ae3468aa23..f2373e135b 100644 --- a/network/p2p/http.go +++ b/network/p2p/http.go @@ -25,8 +25,6 @@ import ( "sync" "time" - "github.com/algorand/go-algorand/logging" - "github.com/algorand/go-algorand/network/limitcaller" "github.com/gorilla/mux" "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/host" @@ -36,6 +34,9 @@ import ( bhost "github.com/libp2p/go-libp2p/p2p/host/basic" libp2phttp "github.com/libp2p/go-libp2p/p2p/http" "github.com/multiformats/go-multiaddr" + + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/network/limitcaller" ) // algorandP2pHTTPProtocol defines a libp2p protocol name for algorand's http over p2p messages diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 44677ecb14..de420a0837 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -19,22 +19,16 @@ package p2p import ( "context" "fmt" + "math" "net" "net/http" "runtime" "strings" "time" - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/logging" - "github.com/algorand/go-algorand/network/limitcaller" - pstore "github.com/algorand/go-algorand/network/p2p/peerstore" - "github.com/algorand/go-algorand/network/phonebook" - "github.com/algorand/go-algorand/util/metrics" - "github.com/algorand/go-deadlock" - "github.com/libp2p/go-libp2p" pubsub "github.com/libp2p/go-libp2p-pubsub" + connmgrcore "github.com/libp2p/go-libp2p/core/connmgr" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" @@ -42,10 +36,20 @@ import ( "github.com/libp2p/go-libp2p/core/protocol" rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager" "github.com/libp2p/go-libp2p/p2p/muxer/yamux" + "github.com/libp2p/go-libp2p/p2p/net/connmgr" "github.com/libp2p/go-libp2p/p2p/security/noise" "github.com/libp2p/go-libp2p/p2p/transport/tcp" "github.com/multiformats/go-multiaddr" manet "github.com/multiformats/go-multiaddr/net" + + "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/network/limitcaller" + pstore "github.com/algorand/go-algorand/network/p2p/peerstore" + "github.com/algorand/go-algorand/network/phonebook" + "github.com/algorand/go-algorand/util/metrics" ) // SubNextCancellable is an abstraction for pubsub.Subscription @@ -66,6 +70,7 @@ type Service interface { DialPeersUntilTargetCount(targetConnCount int) bool ClosePeer(peer.ID) error + UnprotectPeer(peer.ID) Conns() []network.Conn ListPeersForTopic(topic string) []peer.ID @@ -76,15 +81,9 @@ type Service interface { GetHTTPClient(addrInfo *peer.AddrInfo, connTimeStore limitcaller.ConnectionTimeStore, queueingTimeout time.Duration) (*http.Client, error) } -// subset of config.Local needed here -type nodeSubConfig interface { - IsHybridServer() bool -} - // serviceImpl manages integration with libp2p and implements the Service interface type serviceImpl struct { log logging.Logger - subcfg nodeSubConfig listenAddr string host host.Host streams *streamManager @@ -105,7 +104,7 @@ const AlgorandWsProtocolV22 = "/algorand-ws/2.2.0" const dialTimeout = 30 * time.Second -const psmdkDialed = "dialed" +const cnmgrTag = "algorand-ws-mesh" // MakeHost creates a libp2p host but does not start listening. // Use host.Network().Listen() on the returned address to start listening. @@ -142,6 +141,9 @@ func MakeHost(cfg config.Local, datadir string, pstore *pstore.PeerStore) (host. logging.Base().Debug("p2p NetAddress is not set, not listening") listenAddr = "" } + if cfg.IncomingConnectionsLimit == 0 { + listenAddr = "" + } var enableMetrics = func(cfg *libp2p.Config) error { cfg.DisableMetrics = false; return nil } metrics.DefaultRegistry().Register(&metrics.PrometheusDefaultMetrics) @@ -152,7 +154,12 @@ func MakeHost(cfg config.Local, datadir string, pstore *pstore.PeerStore) (host. addrFactory = addressFilter } - rm, err := configureResourceManager(cfg) + connLimits := deriveConnLimits(cfg) + rm, err := configureResourceManager(connLimits) + if err != nil { + return nil, "", err + } + cm, err := configureConnManager(connLimits) if err != nil { return nil, "", err } @@ -167,21 +174,76 @@ func MakeHost(cfg config.Local, datadir string, pstore *pstore.PeerStore) (host. libp2p.Security(noise.ID, noise.New), enableMetrics, libp2p.ResourceManager(rm), + libp2p.ConnectionManager(cm), libp2p.AddrsFactory(addrFactory), ) return host, listenAddr, err } -func configureResourceManager(cfg config.Local) (network.ResourceManager, error) { +// connLimitConfig holds derived connection limits for both the connmgr and rcmgr. +type connLimitConfig struct { + connMgrLow int + connMgrHigh int + rcmgrConns int + rcmgrConnsInbound int + rcmgrConnsOutbound int +} + +// deriveConnLimits computes connection manager and resource manager limits +// from the node configuration. Listen servers use IncomingConnectionsLimit; +// client nodes use tighter limits based on GossipFanout. +func deriveConnLimits(cfg config.Local) connLimitConfig { + var low, high, rcmgrConns, rcmgrConnsInbound, rcmgrConnsOutbound int + rcmgrConnsOutbound = cfg.GossipFanout * 3 + if cfg.EnableDHTProviders { + rcmgrConnsOutbound += cfg.GossipFanout * 3 + } + if cfg.IsListenServer() { + if cfg.IncomingConnectionsLimit < 0 { + rcmgrConns = math.MaxInt + rcmgrConnsInbound = math.MaxInt + high = 0 + low = 0 + } else { + rcmgrConns = rcmgrConnsOutbound + cfg.IncomingConnectionsLimit + rcmgrConnsInbound = cfg.IncomingConnectionsLimit + high = rcmgrConns + low = high * 96 / 100 + } + } else { + rcmgrConns = rcmgrConnsOutbound + cfg.GossipFanout*3 + high = rcmgrConnsOutbound + low = cfg.GossipFanout * 2 + } + + return connLimitConfig{ + connMgrLow: low, + connMgrHigh: high, + rcmgrConns: rcmgrConns, + rcmgrConnsInbound: rcmgrConnsInbound, + rcmgrConnsOutbound: rcmgrConnsOutbound, + } +} + +func configureResourceManager(limits connLimitConfig) (network.ResourceManager, error) { // see https://github.com/libp2p/go-libp2p/tree/master/p2p/host/resource-manager for more details scalingLimits := rcmgr.DefaultLimits libp2p.SetDefaultServiceLimits(&scalingLimits) scaledDefaultLimits := scalingLimits.AutoScale() + systemLimits := rcmgr.ResourceLimits{} + if limits.rcmgrConns > 0 { + systemLimits.Conns = rcmgr.LimitVal(limits.rcmgrConns) + } + if limits.rcmgrConnsInbound > 0 { + systemLimits.ConnsInbound = rcmgr.LimitVal(limits.rcmgrConnsInbound) + } + if limits.rcmgrConnsOutbound > 0 { + systemLimits.ConnsOutbound = rcmgr.LimitVal(limits.rcmgrConnsOutbound) + } + limitConfig := rcmgr.PartialLimitConfig{ - System: rcmgr.ResourceLimits{ - Conns: rcmgr.LimitVal(cfg.IncomingConnectionsLimit), - }, + System: systemLimits, // Everything else is default. The exact values will come from `scaledDefaultLimits` above. } limiter := rcmgr.NewFixedLimiter(limitConfig.Build(scaledDefaultLimits)) @@ -189,6 +251,12 @@ func configureResourceManager(cfg config.Local) (network.ResourceManager, error) return rm, err } +func configureConnManager(limits connLimitConfig) (connmgrcore.ConnManager, error) { + // connMgrLow = 0 and connMgrHigh = 0 mean disabled conns trimming + return connmgr.NewConnManager(limits.connMgrLow, limits.connMgrHigh, + connmgr.WithGracePeriod(20*time.Second)) +} + // StreamHandlerPair is a struct that contains a protocol ID and a StreamHandler type StreamHandlerPair struct { ProtoID protocol.ID @@ -198,55 +266,22 @@ type StreamHandlerPair struct { // StreamHandlers is an ordered list of StreamHandlerPair type StreamHandlers []StreamHandlerPair -// PubSubOption is a function that modifies the pubsub options -type PubSubOption func(opts *[]pubsub.Option) - -// DisablePubSubPeerExchange disables PX (peer exchange) in pubsub -func DisablePubSubPeerExchange() PubSubOption { - return func(opts *[]pubsub.Option) { - *opts = append(*opts, pubsub.WithPeerExchange(false)) - } -} - -// SetPubSubMetricsTracer sets a pubsub.RawTracer for metrics collection -func SetPubSubMetricsTracer(metricsTracer pubsub.RawTracer) PubSubOption { - return func(opts *[]pubsub.Option) { - *opts = append(*opts, pubsub.WithRawTracer(metricsTracer)) - } -} - -// SetPubSubPeerFilter sets a pubsub.PeerFilter for peers filtering out -func SetPubSubPeerFilter(filter func(checker pstore.RoleChecker, pid peer.ID) bool, checker pstore.RoleChecker) PubSubOption { - return func(opts *[]pubsub.Option) { - f := func(pid peer.ID, topic string) bool { - return filter(checker, pid) - } - *opts = append(*opts, pubsub.WithPeerFilter(f)) - } -} - // MakeService creates a P2P service instance func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, h host.Host, listenAddr string, wsStreamHandlers StreamHandlers, pubsubOptions ...PubSubOption) (*serviceImpl, error) { - sm := makeStreamManager(ctx, log, cfg, h, wsStreamHandlers, cfg.EnableGossipService) + sm := makeStreamManager(ctx, log, h, wsStreamHandlers, cfg.EnableGossipService) h.Network().Notify(sm) for _, pair := range wsStreamHandlers { h.SetStreamHandler(pair.ProtoID, sm.streamHandler) } - pubsubOpts := []pubsub.Option{} - for _, opt := range pubsubOptions { - opt(&pubsubOpts) - } - - ps, err := makePubSub(ctx, h, cfg.GossipFanout, pubsubOpts...) + ps, err := makePubSub(ctx, h, cfg.GossipFanout, pubsubOptions...) if err != nil { return nil, err } return &serviceImpl{ log: log, - subcfg: cfg, listenAddr: listenAddr, host: h, streams: sm, @@ -296,13 +331,8 @@ func (s *serviceImpl) DialPeersUntilTargetCount(targetConnCount int) bool { var numOutgoingConns int for _, conn := range conns { if conn.Stat().Direction == network.DirOutbound { - if s.subcfg.IsHybridServer() { - remotePeer := conn.RemotePeer() - val, err := s.host.Peerstore().Get(remotePeer, psmdkDialed) - if err == nil && val != nil && val.(bool) { - numOutgoingConns++ - } - } else { + remotePeer := conn.RemotePeer() + if s.host.ConnManager().IsProtected(remotePeer, cnmgrTag) { numOutgoingConns++ } } @@ -313,8 +343,18 @@ func (s *serviceImpl) DialPeersUntilTargetCount(targetConnCount int) bool { if numOutgoingConns >= targetConnCount { return numOutgoingConns > preExistingConns } - // if we are already connected to this peer, skip it - if len(s.host.Network().ConnsToPeer(peerInfo.ID)) > 0 { + // if we are already connected to this peer, ensure it's properly handled + if conns := s.host.Network().ConnsToPeer(peerInfo.ID); len(conns) > 0 { + if !s.host.ConnManager().IsProtected(peerInfo.ID, cnmgrTag) { + // connection was established by DHT/pubsub before the mesh thread + // could protect it, so handleConnected skipped stream creation. + // protect and re-trigger stream setup now. + s.host.ConnManager().Protect(peerInfo.ID, cnmgrTag) + go s.streams.handleConnected(conns[0]) + if conns[0].Stat().Direction == network.DirOutbound { + numOutgoingConns++ + } + } continue } err := s.dialNode(context.Background(), peerInfo) // leaving the calls as blocking for now, to not over-connect beyond fanout @@ -335,12 +375,13 @@ func (s *serviceImpl) dialNode(ctx context.Context, peer *peer.AddrInfo) error { } ctx, cancel := context.WithTimeout(ctx, dialTimeout) defer cancel() - if s.subcfg.IsHybridServer() { - if err := s.host.Peerstore().Put(peer.ID, psmdkDialed, true); err != nil { // mark this peer as explicitly dialed - return err - } + // protect before attempting to connect so that checks in Connected handler have up-to-date information + s.host.ConnManager().Protect(peer.ID, cnmgrTag) + err := s.host.Connect(ctx, *peer) + if err != nil { + s.host.ConnManager().Unprotect(peer.ID, cnmgrTag) } - return s.host.Connect(ctx, *peer) + return err } // AddrInfo returns the peer.AddrInfo for self @@ -365,6 +406,12 @@ func (s *serviceImpl) ClosePeer(peer peer.ID) error { return s.host.Network().ClosePeer(peer) } +// UnprotectPeer removes the protection from a peer, allowing +// the connection manager to trim it if needed. +func (s *serviceImpl) UnprotectPeer(id peer.ID) { + s.host.ConnManager().Unprotect(id, cnmgrTag) +} + // netAddressToListenAddress converts a netAddress in "ip:port" format to a listen address // that can be passed in to libp2p.ListenAddrStrings func netAddressToListenAddress(netAddress string) (string, error) { diff --git a/network/p2p/p2p_test.go b/network/p2p/p2p_test.go index 5973b00ea6..9f15616b57 100644 --- a/network/p2p/p2p_test.go +++ b/network/p2p/p2p_test.go @@ -17,9 +17,12 @@ package p2p import ( + "context" "fmt" + "math" "net" "testing" + "time" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/peer" @@ -29,6 +32,7 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network/p2p/peerstore" "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-algorand/test/partitiontest" @@ -76,7 +80,7 @@ func TestNetAddressToListenAddress(t *testing.T) { t.Run(fmt.Sprintf("input: %s", test.input), func(t *testing.T) { res, err := netAddressToListenAddress(test.input) if test.err { - require.Error(t, err) + require.ErrorContains(t, err, `invalid netAddress`) } else { require.NoError(t, err) require.Equal(t, test.output, res) @@ -246,18 +250,41 @@ func TestP2PMakeHostAddressFilter(t *testing.T) { } } +func TestP2PServiceStartZeroIncomingDoesNotListen(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + cfg := config.GetDefaultLocal() + cfg.NetAddress = "127.0.0.1:0" + cfg.IncomingConnectionsLimit = 0 + + td := t.TempDir() + pstore, err := peerstore.NewPeerStore(nil, "test") + require.NoError(t, err) + + host, la, err := MakeHost(cfg, td, pstore) + require.NoError(t, err) + + svc, err := MakeService(context.Background(), logging.TestingLog(t), cfg, host, la, StreamHandlers{}) + require.NoError(t, err) + defer svc.Close() + + require.NoError(t, svc.Start()) + require.Empty(t, host.Network().ListenAddresses()) +} + func TestP2PPubSubOptions(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() var opts []pubsub.Option option := DisablePubSubPeerExchange() - option(&opts) + option(&opts, nil) require.Len(t, opts, 1) tracer := &mockRawTracer{} option = SetPubSubMetricsTracer(tracer) - option(&opts) + option(&opts, nil) require.Len(t, opts, 2) filterFunc := func(roleChecker peerstore.RoleChecker, pid peer.ID) bool { @@ -265,8 +292,14 @@ func TestP2PPubSubOptions(t *testing.T) { } checker := &mockRoleChecker{} option = SetPubSubPeerFilter(filterFunc, checker) - option(&opts) + option(&opts, nil) require.Len(t, opts, 3) + + option = SetPubSubHeartbeatInterval(100 * time.Millisecond) + params := &pubsub.GossipSubParams{} + option(&opts, params) + require.Len(t, opts, 3) // SetPubSubHeartbeatInterval does not add to opts but updates params + require.Equal(t, 100*time.Millisecond, params.HeartbeatInterval) } type mockRawTracer struct{} @@ -292,3 +325,122 @@ type mockRoleChecker struct{} func (m *mockRoleChecker) HasRole(pid peer.ID, role phonebook.Role) bool { return true } + +func TestDeriveConnLimits_Server(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + cfg := config.GetDefaultLocal() + cfg.NetAddress = ":4160" + cfg.IncomingConnectionsLimit = 2400 + cfg.GossipFanout = 4 + limits := deriveConnLimits(cfg) + require.Equal(t, 2400+12, limits.rcmgrConns) + require.Equal(t, 2400, limits.rcmgrConnsInbound) + require.Equal(t, 12, limits.rcmgrConnsOutbound) + require.Equal(t, 2412, limits.connMgrHigh) + require.Equal(t, 2315, limits.connMgrLow) // 2412 * 96 / 100 + require.LessOrEqual(t, limits.connMgrHigh, limits.rcmgrConns) +} + +func TestDeriveConnLimits_UnboundedServer(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + cfg := config.GetDefaultLocal() + cfg.NetAddress = ":4160" + cfg.IncomingConnectionsLimit = -1 + cfg.GossipFanout = 4 + limits := deriveConnLimits(cfg) + require.Equal(t, math.MaxInt, limits.rcmgrConns) + require.Equal(t, math.MaxInt, limits.rcmgrConnsInbound) + require.Equal(t, 12, limits.rcmgrConnsOutbound) + require.Equal(t, 0, limits.connMgrHigh) + require.Equal(t, 0, limits.connMgrLow) +} + +func TestDeriveConnLimits_DHTProviders(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + cfg := config.GetDefaultLocal() + cfg.NetAddress = ":4160" + cfg.IncomingConnectionsLimit = 2400 + cfg.GossipFanout = 4 + cfg.EnableDHTProviders = true + limits := deriveConnLimits(cfg) + require.Equal(t, 2400+12+12, limits.rcmgrConns) + require.Equal(t, 2400, limits.rcmgrConnsInbound) + require.Equal(t, 24, limits.rcmgrConnsOutbound) + require.Equal(t, 2424, limits.connMgrHigh) + require.Equal(t, 2327, limits.connMgrLow) // 2424 * 96 / 100 +} + +func TestDeriveConnLimits_Client(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + cfg := config.GetDefaultLocal() + cfg.GossipFanout = 4 + limits := deriveConnLimits(cfg) + require.Equal(t, 24, limits.rcmgrConns) // 4 * 6 + require.Equal(t, 0, limits.rcmgrConnsInbound) + require.Equal(t, 12, limits.rcmgrConnsOutbound) // 4 * 3 + require.Equal(t, 12, limits.connMgrHigh) // 4 * 3 + require.Equal(t, 8, limits.connMgrLow) // 4 * 2 + require.LessOrEqual(t, limits.connMgrHigh, limits.rcmgrConns) + + // ensure cfg.IncomingConnectionsLimit = -1 does not affect client limits + cfg.IncomingConnectionsLimit = -1 + newLimits := deriveConnLimits(cfg) + require.Equal(t, limits, newLimits) +} + +func TestDeriveConnLimits_HybridClient(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // Hybrid client: EnableP2PHybridMode but no listen addresses + cfg := config.GetDefaultLocal() + cfg.EnableP2PHybridMode = true + cfg.GossipFanout = 4 + limits := deriveConnLimits(cfg) + require.Equal(t, 24, limits.rcmgrConns) + require.Equal(t, 0, limits.rcmgrConnsInbound) + require.Equal(t, 12, limits.rcmgrConnsOutbound) + require.Equal(t, 12, limits.connMgrHigh) + require.Equal(t, 8, limits.connMgrLow) + require.LessOrEqual(t, limits.connMgrHigh, limits.rcmgrConns) +} + +func TestDeriveConnLimits_HybridServer(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + cfg := config.GetDefaultLocal() + cfg.EnableP2PHybridMode = true + cfg.NetAddress = ":4160" + cfg.P2PHybridNetAddress = ":4190" + cfg.IncomingConnectionsLimit = 2400 + limits := deriveConnLimits(cfg) + require.Equal(t, 2412, limits.rcmgrConns) + require.Equal(t, 2400, limits.rcmgrConnsInbound) + require.Equal(t, 12, limits.rcmgrConnsOutbound) + require.Equal(t, 2412, limits.connMgrHigh) + require.Equal(t, 2315, limits.connMgrLow) + require.LessOrEqual(t, limits.connMgrHigh, limits.rcmgrConns) +} + +func TestDeriveConnLimits_ZeroFanout(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + cfg := config.GetDefaultLocal() + cfg.GossipFanout = 0 + limits := deriveConnLimits(cfg) + require.GreaterOrEqual(t, limits.connMgrLow, 0) // zero means zero + require.GreaterOrEqual(t, limits.connMgrHigh, limits.connMgrLow) + require.GreaterOrEqual(t, limits.rcmgrConns, limits.connMgrHigh) + require.Equal(t, 0, limits.rcmgrConnsInbound) + require.Equal(t, 0, limits.rcmgrConnsOutbound) +} diff --git a/network/p2p/peerID.go b/network/p2p/peerID.go index 291de66425..84b535f8e5 100644 --- a/network/p2p/peerID.go +++ b/network/p2p/peerID.go @@ -27,12 +27,12 @@ import ( "os" "path" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/algorand/go-algorand/config" algocrypto "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/util" - - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" ) // DefaultPrivKeyPath is the default path inside the node's root directory at which the private key diff --git a/network/p2p/peerstore/peerstore.go b/network/p2p/peerstore/peerstore.go index 3d37334d5d..23a9737fad 100644 --- a/network/p2p/peerstore/peerstore.go +++ b/network/p2p/peerstore/peerstore.go @@ -27,8 +27,9 @@ import ( libp2p "github.com/libp2p/go-libp2p/core/peerstore" mempstore "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" - "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/network/phonebook" ) // when using GetAddresses with getAllAddresses, all the addresses will be retrieved, regardless diff --git a/network/p2p/pubsub.go b/network/p2p/pubsub.go index 8ff628a37b..33b021f1e8 100644 --- a/network/p2p/pubsub.go +++ b/network/p2p/pubsub.go @@ -25,20 +25,43 @@ import ( "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "golang.org/x/crypto/blake2b" + + pstore "github.com/algorand/go-algorand/network/p2p/peerstore" ) -func init() { - // configure larger overlay parameters - pubsub.GossipSubD = 8 - pubsub.GossipSubDscore = 6 - pubsub.GossipSubDout = 3 - pubsub.GossipSubDlo = 6 - pubsub.GossipSubDhi = 12 - pubsub.GossipSubDlazy = 12 - pubsub.GossipSubDirectConnectInitialDelay = 30 * time.Second - pubsub.GossipSubIWantFollowupTime = 5 * time.Second - pubsub.GossipSubHistoryLength = 10 - pubsub.GossipSubGossipFactor = 0.1 +// PubSubOption is a function that modifies the pubsub options +type PubSubOption func(opts *[]pubsub.Option, params *pubsub.GossipSubParams) + +// DisablePubSubPeerExchange disables PX (peer exchange) in pubsub +func DisablePubSubPeerExchange() PubSubOption { + return func(opts *[]pubsub.Option, _ *pubsub.GossipSubParams) { + *opts = append(*opts, pubsub.WithPeerExchange(false)) + } +} + +// SetPubSubMetricsTracer sets a pubsub.RawTracer for metrics collection +func SetPubSubMetricsTracer(metricsTracer pubsub.RawTracer) PubSubOption { + return func(opts *[]pubsub.Option, _ *pubsub.GossipSubParams) { + *opts = append(*opts, pubsub.WithRawTracer(metricsTracer)) + } +} + +// SetPubSubPeerFilter sets a pubsub.PeerFilter for peers filtering out +func SetPubSubPeerFilter(filter func(checker pstore.RoleChecker, pid peer.ID) bool, checker pstore.RoleChecker) PubSubOption { + return func(opts *[]pubsub.Option, _ *pubsub.GossipSubParams) { + f := func(pid peer.ID, topic string) bool { + return filter(checker, pid) + } + *opts = append(*opts, pubsub.WithPeerFilter(f)) + } +} + +// SetPubSubHeartbeatInterval sets the heartbeat interval in pubsub GossipSubParams +// Note: subsequent call of pubsub.WithGossipSubParams is needed. +func SetPubSubHeartbeatInterval(interval time.Duration) PubSubOption { + return func(opts *[]pubsub.Option, params *pubsub.GossipSubParams) { + params.HeartbeatInterval = interval + } } const ( @@ -57,24 +80,67 @@ const TXTopicName = "algotx01" const incomingThreads = 20 // matches to number wsNetwork workers -// deriveGossipSubParams derives the gossip sub parameters from the cfg.GossipFanout value +// deriveAlgorandGossipSubParams derives the gossip sub parameters from the cfg.GossipFanout value // by using the same proportions as pubsub defaults - see GossipSubD, GossipSubDlo, etc. -func deriveGossipSubParams(numOutgoingConns int) pubsub.GossipSubParams { +func deriveAlgorandGossipSubParams(numOutgoingConns int) pubsub.GossipSubParams { params := pubsub.DefaultGossipSubParams() - params.D = numOutgoingConns - params.Dlo = params.D - 1 - if params.Dlo <= 0 { - params.Dlo = params.D + + // configure larger overlay parameters + // despite the fact D-values are later overridden based on numOutgoingConns, + // we still want to give an idea what values might/should be by default. + params.D = 8 + params.Dscore = 6 + params.Dout = 3 + params.Dlo = 6 + params.Dhi = 12 + params.Dlazy = 12 + params.DirectConnectInitialDelay = 30 * time.Second + params.IWantFollowupTime = 5 * time.Second + params.HistoryLength = 10 + params.GossipFactor = 0.1 + + if numOutgoingConns >= 12 { + // large number of outgoing conns, cap to the defaults above + return params } - params.Dscore = params.D * 2 / 3 - params.Dout = params.D * 1 / 3 + if numOutgoingConns <= 0 { + // no outgoing connections + params.D = 0 + params.Dscore = 0 + params.Dout = 0 + params.Dlo = 0 + params.Dhi = 0 + params.Dlazy = 0 + return params + } + if numOutgoingConns <= 4 { + // use some minimal meaningful values satisfying + // go-libp2p-pubsub constraints like Dout < Dlo && Dout < D/2 + // note, Dout=1 requires D >= 4 so that numOutgoingConns <= 4 implies hardcoded values + params.D = 4 + params.Dscore = 1 + params.Dout = 1 + params.Dlo = 2 + params.Dhi = 4 + params.Dlazy = 4 + return params + } + + // for numOutgoingConns in (4, 12), scale the D parameters proportionally + // to the number of outgoing connections, keeping the same proportions as the defaults. + // + // ratios from the defaults: D/n ~= 2/3, Dlo/D = Dscore/D = 3/4, Dout/D = 3/8, and Dhi = Dlazy = n + params.D = numOutgoingConns - numOutgoingConns/3 + params.Dlo = params.D * 3 / 4 + params.Dscore = params.D * 3 / 4 + params.Dhi = numOutgoingConns + params.Dlazy = numOutgoingConns + params.Dout = params.D * 3 / 8 return params } -func makePubSub(ctx context.Context, host host.Host, numOutgoingConns int, opts ...pubsub.Option) (*pubsub.PubSub, error) { - gossipSubParams := deriveGossipSubParams(numOutgoingConns) +func makePubSub(ctx context.Context, host host.Host, numOutgoingConns int, pubsubOptions ...PubSubOption) (*pubsub.PubSub, error) { options := []pubsub.Option{ - pubsub.WithGossipSubParams(gossipSubParams), pubsub.WithPeerScore(&pubsub.PeerScoreParams{ DecayInterval: pubsub.DefaultDecayInterval, DecayToZero: pubsub.DefaultDecayToZero, @@ -116,7 +182,12 @@ func makePubSub(ctx context.Context, host host.Host, numOutgoingConns int, opts pubsub.WithValidateWorkers(incomingThreads), } - options = append(options, opts...) + gossipSubParams := deriveAlgorandGossipSubParams(numOutgoingConns) + for _, opt := range pubsubOptions { + opt(&options, &gossipSubParams) + } + options = append(options, pubsub.WithGossipSubParams(gossipSubParams)) + return pubsub.NewGossipSub(ctx, host, options...) } diff --git a/network/p2p/pubsub_test.go b/network/p2p/pubsub_test.go index b7cfab9716..18b97e1214 100644 --- a/network/p2p/pubsub_test.go +++ b/network/p2p/pubsub_test.go @@ -19,10 +19,8 @@ package p2p import ( "testing" - pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/stretchr/testify/require" - "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -30,22 +28,48 @@ func TestPubsub_GossipSubParamsBasic(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - cfg := config.GetDefaultLocal() - - for _, fanout := range []int{4, 8} { - cfg.GossipFanout = fanout + // expected values for n from 5 (calculated) to 12 (max default) + // n, D, Dlo, Dscore, Dout, Dhi, Dlazy + expected := []struct { + n, D, Dlo, Dscore, Dout, Dhi, Dlazy int + }{ + {5, 4, 3, 3, 1, 5, 5}, + {6, 4, 3, 3, 1, 6, 6}, + {7, 5, 3, 3, 1, 7, 7}, + {8, 6, 4, 4, 2, 8, 8}, + {9, 6, 4, 4, 2, 9, 9}, + {10, 7, 5, 5, 2, 10, 10}, + {11, 8, 6, 6, 3, 11, 11}, + {12, 8, 6, 6, 3, 12, 12}, + } - params := deriveGossipSubParams(cfg.GossipFanout) + for _, e := range expected { + p := deriveAlgorandGossipSubParams(e.n) + require.Equal(t, e.D, p.D, "n=%d D", e.n) + require.Equal(t, e.Dlo, p.Dlo, "n=%d Dlo", e.n) + require.Equal(t, e.Dscore, p.Dscore, "n=%d Dscore", e.n) + require.Equal(t, e.Dout, p.Dout, "n=%d Dout", e.n) + require.Equal(t, e.Dhi, p.Dhi, "n=%d Dhi", e.n) + require.Equal(t, e.Dlazy, p.Dlazy, "n=%d Dlazy", e.n) + } +} - require.Equal(t, fanout, params.D) - require.Equal(t, fanout-1, params.Dlo) - require.Equal(t, fanout*2/3, params.Dscore) - require.Equal(t, fanout*1/3, params.Dout) +// Verify libp2p gossipsub validate() constraints +// 1. Dlo <= D <= Dhi +// 2. Dscore <= Dhi +// 3. Dout < Dlo (strict) +// 4. Dout < D/2 (strict, integer division) +func TestPubsub_GossipSubParamsValidateConstraints(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() - // Sanity: other defaults are preserved (not zeroed). Avoid asserting exact values to reduce brittleness. - def := pubsub.DefaultGossipSubParams() - require.Equal(t, def.HeartbeatInitialDelay, params.HeartbeatInitialDelay) - require.Equal(t, def.HistoryLength, params.HistoryLength) + for n := 1; n <= 20; n++ { + p := deriveAlgorandGossipSubParams(n) + require.LessOrEqual(t, p.Dlo, p.D, "n=%d: Dlo <= D", n) + require.LessOrEqual(t, p.D, p.Dhi, "n=%d: D <= Dhi", n) + require.LessOrEqual(t, p.Dscore, p.Dhi, "n=%d: Dscore <= Dhi", n) + require.Less(t, p.Dout, p.Dlo, "n=%d: Dout < Dlo", n) + require.Less(t, p.Dout, p.D/2, "n=%d: Dout < D/2", n) } } @@ -53,21 +77,34 @@ func TestPubsub_GossipSubParamsEdgeCases(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - // D = 1 => Dlo must not drop below 1 - cfg := config.GetDefaultLocal() - cfg.GossipFanout = 1 - p := deriveGossipSubParams(cfg.GossipFanout) - require.Equal(t, 1, p.D) - require.Equal(t, 1, p.Dlo) - require.Equal(t, 0, p.Dscore) - require.Equal(t, 0, p.Dout) - - // D = 0 => keep Dlo = D (0) instead of negative - cfg = config.GetDefaultLocal() - cfg.GossipFanout = 0 - p = deriveGossipSubParams(cfg.GossipFanout) + // n = 0: all zeros + p := deriveAlgorandGossipSubParams(0) require.Equal(t, 0, p.D) require.Equal(t, 0, p.Dlo) require.Equal(t, 0, p.Dscore) require.Equal(t, 0, p.Dout) + require.Equal(t, 0, p.Dhi) + require.Equal(t, 0, p.Dlazy) + + // n = 1..4: low bound + for n := 1; n <= 4; n++ { + p = deriveAlgorandGossipSubParams(n) + require.Equal(t, 4, p.D) + require.Equal(t, 2, p.Dlo) + require.Equal(t, 1, p.Dscore) + require.Equal(t, 1, p.Dout) + require.Equal(t, 4, p.Dhi) + require.Equal(t, 4, p.Dlazy) + } + + // n >= 12: capped to defaults + for n := 12; n <= 20; n++ { + p = deriveAlgorandGossipSubParams(n) + require.Equal(t, 8, p.D) + require.Equal(t, 6, p.Dlo) + require.Equal(t, 6, p.Dscore) + require.Equal(t, 3, p.Dout) + require.Equal(t, 12, p.Dhi) + require.Equal(t, 12, p.Dlazy) + } } diff --git a/network/p2p/streams.go b/network/p2p/streams.go index 7f9cf0260f..5f505e60ed 100644 --- a/network/p2p/streams.go +++ b/network/p2p/streams.go @@ -19,90 +19,121 @@ package p2p import ( "context" "fmt" - "io" - "github.com/algorand/go-algorand/logging" - "github.com/algorand/go-deadlock" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/protocol" "github.com/multiformats/go-multiaddr" + + "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/logging" ) // streamManager implements network.Notifiee to create and manage streams for use with non-gossipsub protocols. type streamManager struct { ctx context.Context log logging.Logger - cfg nodeSubConfig host host.Host handlers StreamHandlers allowIncomingGossip bool streams map[peer.ID]network.Stream + inflight map[peer.ID]int streamsLock deadlock.Mutex } // StreamHandler is called when a new bidirectional stream for a given protocol and peer is opened. -type StreamHandler func(ctx context.Context, pid peer.ID, s network.Stream, incoming bool) +type StreamHandler func(ctx context.Context, pid peer.ID, s network.Stream, incoming bool) error -func makeStreamManager(ctx context.Context, log logging.Logger, cfg nodeSubConfig, h host.Host, handlers StreamHandlers, allowIncomingGossip bool) *streamManager { +func makeStreamManager(ctx context.Context, log logging.Logger, h host.Host, handlers StreamHandlers, allowIncomingGossip bool) *streamManager { return &streamManager{ ctx: ctx, log: log, - cfg: cfg, host: h, handlers: handlers, allowIncomingGossip: allowIncomingGossip, streams: make(map[peer.ID]network.Stream), + inflight: make(map[peer.ID]int), + } +} + +func (n *streamManager) beginPeerAttempt(remotePeer peer.ID) { + n.streamsLock.Lock() + n.inflight[remotePeer]++ + n.streamsLock.Unlock() +} + +func (n *streamManager) endPeerAttempt(remotePeer peer.ID) { + shouldUnprotect := false + + n.streamsLock.Lock() + if count := n.inflight[remotePeer]; count <= 1 { + delete(n.inflight, remotePeer) + } else { + n.inflight[remotePeer] = count - 1 + } + _, hasStream := n.streams[remotePeer] + _, hasInflight := n.inflight[remotePeer] + shouldUnprotect = !hasStream && !hasInflight + n.streamsLock.Unlock() + + if shouldUnprotect { + n.host.ConnManager().Unprotect(remotePeer, cnmgrTag) } } // streamHandler is called by libp2p when a new stream is accepted func (n *streamManager) streamHandler(stream network.Stream) { + remotePeer := stream.Conn().RemotePeer() + n.beginPeerAttempt(remotePeer) + defer n.endPeerAttempt(remotePeer) + if stream.Conn().Stat().Direction == network.DirInbound && !n.allowIncomingGossip { - n.log.Debugf("rejecting stream from incoming connection from %s", stream.Conn().RemotePeer().String()) + n.log.Debugf("rejecting stream from incoming connection from %s", remotePeer.String()) stream.Close() return } - - n.streamsLock.Lock() - defer n.streamsLock.Unlock() - - // could use stream.ID() for tracking; unique across all conns and peers - remotePeer := stream.Conn().RemotePeer() - - if oldStream, ok := n.streams[remotePeer]; ok { - // there's already a stream, for some reason, check if it's still open - buf := []byte{} // empty buffer for checking - _, err := oldStream.Read(buf) - if err != nil { - if err == io.EOF { - // old stream was closed by the peer - n.log.Infof("Old stream with %s was closed", remotePeer) - } else { - // an error occurred while checking the old stream - n.log.Infof("Failed to check old stream with %s: %v", remotePeer, err) - } - n.streams[stream.Conn().RemotePeer()] = stream - - incoming := stream.Conn().Stat().Direction == network.DirInbound - if err1 := n.dispatch(n.ctx, remotePeer, stream, incoming); err1 != nil { - n.log.Errorln(err1.Error()) - _ = stream.Reset() - } + // reject streams on connections not explicitly dialed by us + if stream.Conn().Stat().Direction == network.DirOutbound && stream.Stat().Direction == network.DirInbound { + if !n.host.ConnManager().IsProtected(remotePeer, cnmgrTag) { + n.log.Debugf("%s: ignoring incoming stream from non-dialed outgoing peer ID %s", stream.Conn().LocalPeer().String(), remotePeer.String()) + stream.Close() return } - // otherwise, the old stream is still open, so we can close the new one - stream.Close() - return } - // no old stream - n.streams[stream.Conn().RemotePeer()] = stream + + // Never do blocking I/O (like stream.Read) while holding streamsLock — + // that causes a deadlock with Disconnected which also needs the lock to + // close the old stream. + // + // Dispatch the new stream first (outside the lock), then swap the map + // entry only on success. This avoids dropping a healthy old stream when + // the replacement fails dispatch. incoming := stream.Conn().Stat().Direction == network.DirInbound if err := n.dispatch(n.ctx, remotePeer, stream, incoming); err != nil { n.log.Errorln(err.Error()) _ = stream.Reset() + return + } + + n.streamsLock.Lock() + // If the connection closed while we were dispatching, Disconnected has + // already fired (or will fire) and won't find this entry to clean up. + // Avoid adding a stale stream to the map. + if stream.Conn().IsClosed() { + n.streamsLock.Unlock() + _ = stream.Reset() + return + } + oldStream := n.streams[remotePeer] + n.streams[remotePeer] = stream + n.streamsLock.Unlock() + + if oldStream != nil { + n.log.Infof("Replacing old stream with %s", remotePeer) + oldStream.Close() } } @@ -110,8 +141,7 @@ func (n *streamManager) streamHandler(stream network.Stream) { func (n *streamManager) dispatch(ctx context.Context, remotePeer peer.ID, stream network.Stream, incoming bool) error { for _, pair := range n.handlers { if pair.ProtoID == stream.Protocol() { - pair.Handler(ctx, remotePeer, stream, incoming) - return nil + return pair.Handler(ctx, remotePeer, stream, incoming) } } n.log.Errorf("No handler for protocol %s, peer %s", stream.Protocol(), remotePeer) @@ -120,17 +150,22 @@ func (n *streamManager) dispatch(ctx context.Context, remotePeer peer.ID, stream // Connected is called when a connection is opened // for both incoming (listener -> addConn) and outgoing (dialer -> addConn) connections. +// This is invoked from libp2p's Swarm.notifyAll which holds a read lock on the notifiees list. +// We do some read/write operations in this handler for metadata exchange that creates a race condition +// with StopNotify on network shutdown. To avoid, run the handler as a goroutine. func (n *streamManager) Connected(net network.Network, conn network.Conn) { - remotePeer := conn.RemotePeer() localPeer := n.host.ID() if conn.Stat().Direction == network.DirInbound && !n.allowIncomingGossip { n.log.Debugf("%s: ignoring incoming connection from %s", localPeer.String(), remotePeer.String()) + n.host.ConnManager().Unprotect(conn.RemotePeer(), cnmgrTag) return } - // ensure that only one of the peers initiates the stream + // ensure that only one of the peers initiates the stream. + // the remote peer will open the stream and our streamHandler will handle it, + // so mark dispatched to preserve the cnmgr protection set by dialNode. if localPeer > remotePeer { n.log.Debugf("%s: ignoring a lesser peer ID %s", localPeer.String(), remotePeer.String()) return @@ -138,22 +173,26 @@ func (n *streamManager) Connected(net network.Network, conn network.Conn) { // check if this is outgoing connection but made not by us (serviceImpl.dialNode) // then it was made by some sub component like pubsub, ignore - if n.cfg.IsHybridServer() && conn.Stat().Direction == network.DirOutbound { - val, err := n.host.Peerstore().Get(remotePeer, psmdkDialed) - if err != nil || val != nil && !val.(bool) { - // not found or false value + if conn.Stat().Direction == network.DirOutbound { + if !n.host.ConnManager().IsProtected(remotePeer, cnmgrTag) { n.log.Debugf("%s: ignoring non-dialed outgoing peer ID %s", localPeer.String(), remotePeer.String()) return } - if val == nil { - n.log.Warnf("%s: failed to get dialed status for %s", localPeer.String(), remotePeer.String()) - } } + go n.handleConnected(conn) +} + +func (n *streamManager) handleConnected(conn network.Conn) { + remotePeer := conn.RemotePeer() + localPeer := n.host.ID() + n.beginPeerAttempt(remotePeer) + defer n.endPeerAttempt(remotePeer) + n.streamsLock.Lock() _, ok := n.streams[remotePeer] + n.streamsLock.Unlock() if ok { - n.streamsLock.Unlock() n.log.Debugf("%s: already have a stream to/from %s", localPeer.String(), remotePeer.String()) return // there's already an active stream with this peer for our protocol } @@ -165,20 +204,30 @@ func (n *streamManager) Connected(net network.Network, conn network.Conn) { stream, err := n.host.NewStream(n.ctx, remotePeer, protos...) if err != nil { n.log.Infof("%s: failed to open stream to %s (%s): %v", localPeer.String(), remotePeer, conn.RemoteMultiaddr().String(), err) - n.streamsLock.Unlock() return } - n.streams[remotePeer] = stream - n.streamsLock.Unlock() - n.log.Infof("%s: using protocol %s with peer %s", localPeer.String(), stream.Protocol(), remotePeer.String()) incoming := stream.Conn().Stat().Direction == network.DirInbound - err = n.dispatch(n.ctx, remotePeer, stream, incoming) - if err != nil { + if err = n.dispatch(n.ctx, remotePeer, stream, incoming); err != nil { n.log.Errorln(err.Error()) _ = stream.Reset() + return + } + + n.streamsLock.Lock() + defer n.streamsLock.Unlock() + if _, exists := n.streams[remotePeer]; exists { + // another stream was added in the meantime, close this one and keep the existing one + _ = stream.Reset() + return + } + // don't add disconnected / died conns, so Disconnect won't need to clean up + if stream.Conn().IsClosed() { + _ = stream.Reset() + return } + n.streams[remotePeer] = stream } // Disconnected is called when a connection is closed diff --git a/network/p2p/streams_stale_test.go b/network/p2p/streams_stale_test.go new file mode 100644 index 0000000000..ed2935e06a --- /dev/null +++ b/network/p2p/streams_stale_test.go @@ -0,0 +1,785 @@ +// Copyright (C) 2019-2026 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 p2p + +import ( + "context" + "errors" + "io" + "testing" + "time" + + connmgrcore "github.com/libp2p/go-libp2p/core/connmgr" + ic "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/event" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/libp2p/go-libp2p/core/protocol" + ma "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/test/partitiontest" +) + +// errDispatchFailed is the sentinel error returned by the failing test handler. +var errDispatchFailed = errors.New("dispatch failed") + +const testProto = protocol.ID("/algorand-test/1.0.0") + +// mockConnMgr implements connmgrcore.ConnManager for testing. +type mockConnMgr struct { + mu deadlock.Mutex + protected map[peer.ID]map[string]bool + unprotect map[peer.ID]int +} + +func newMockConnMgr() *mockConnMgr { + return &mockConnMgr{ + protected: make(map[peer.ID]map[string]bool), + unprotect: make(map[peer.ID]int), + } +} + +func (m *mockConnMgr) Protect(id peer.ID, tag string) { + m.mu.Lock() + defer m.mu.Unlock() + if m.protected[id] == nil { + m.protected[id] = make(map[string]bool) + } + m.protected[id][tag] = true +} + +func (m *mockConnMgr) Unprotect(id peer.ID, tag string) bool { + m.mu.Lock() + defer m.mu.Unlock() + m.unprotect[id]++ + if m.protected[id] != nil { + delete(m.protected[id], tag) + } + return len(m.protected[id]) > 0 +} + +func (m *mockConnMgr) IsProtected(id peer.ID, tag string) bool { + m.mu.Lock() + defer m.mu.Unlock() + return m.protected[id] != nil && m.protected[id][tag] +} + +func (m *mockConnMgr) UnprotectCalls(id peer.ID) int { + m.mu.Lock() + defer m.mu.Unlock() + return m.unprotect[id] +} + +func (m *mockConnMgr) TagPeer(peer.ID, string, int) {} +func (m *mockConnMgr) UntagPeer(peer.ID, string) {} +func (m *mockConnMgr) UpsertTag(peer.ID, string, func(int) int) {} +func (m *mockConnMgr) GetTagInfo(peer.ID) *connmgrcore.TagInfo { return nil } +func (m *mockConnMgr) TrimOpenConns(context.Context) {} +func (m *mockConnMgr) Notifee() network.Notifiee { return nil } +func (m *mockConnMgr) CheckLimit(connmgrcore.GetConnLimiter) error { return nil } +func (m *mockConnMgr) Close() error { return nil } + +// mockHost implements host.Host with only the methods used by streamManager. +type mockHost struct { + id peer.ID + cm *mockConnMgr + newStreamFn func(context.Context, peer.ID, ...protocol.ID) (network.Stream, error) +} + +func (h *mockHost) ID() peer.ID { return h.id } +func (h *mockHost) ConnManager() connmgrcore.ConnManager { return h.cm } +func (h *mockHost) NewStream(ctx context.Context, p peer.ID, pids ...protocol.ID) (network.Stream, error) { + return h.newStreamFn(ctx, p, pids...) +} + +func (h *mockHost) Peerstore() peerstore.Peerstore { panic("unused") } +func (h *mockHost) Addrs() []ma.Multiaddr { panic("unused") } +func (h *mockHost) Network() network.Network { panic("unused") } +func (h *mockHost) Mux() protocol.Switch { panic("unused") } +func (h *mockHost) Connect(context.Context, peer.AddrInfo) error { panic("unused") } +func (h *mockHost) SetStreamHandler(protocol.ID, network.StreamHandler) {} +func (h *mockHost) SetStreamHandlerMatch(protocol.ID, func(protocol.ID) bool, network.StreamHandler) { +} +func (h *mockHost) RemoveStreamHandler(protocol.ID) {} +func (h *mockHost) Close() error { return nil } +func (h *mockHost) EventBus() event.Bus { panic("unused") } + +// Verify interface satisfaction at compile time. +var _ host.Host = (*mockHost)(nil) + +// mockConn implements network.Conn with controllable direction and peer IDs. +type mockConn struct { + remotePeerID peer.ID + localPeerID peer.ID + dir network.Direction +} + +func newMockConn(local, remote peer.ID, dir network.Direction) *mockConn { + return &mockConn{localPeerID: local, remotePeerID: remote, dir: dir} +} + +func (c *mockConn) Close() error { return nil } +func (c *mockConn) LocalPeer() peer.ID { return c.localPeerID } +func (c *mockConn) RemotePeer() peer.ID { return c.remotePeerID } +func (c *mockConn) RemotePublicKey() ic.PubKey { return nil } +func (c *mockConn) ConnState() network.ConnectionState { return network.ConnectionState{} } +func (c *mockConn) LocalMultiaddr() ma.Multiaddr { return ma.StringCast("/ip4/127.0.0.1/tcp/4190") } +func (c *mockConn) RemoteMultiaddr() ma.Multiaddr { return ma.StringCast("/ip4/1.2.3.4/tcp/4190") } +func (c *mockConn) Stat() network.ConnStats { + return network.ConnStats{Stats: network.Stats{Direction: c.dir}} +} +func (c *mockConn) Scope() network.ConnScope { return nil } +func (c *mockConn) ID() string { return "mock-conn" } +func (c *mockConn) NewStream(context.Context) (network.Stream, error) { panic("unused") } +func (c *mockConn) GetStreams() []network.Stream { return nil } +func (c *mockConn) IsClosed() bool { return false } + +var _ network.Conn = (*mockConn)(nil) + +// mockStream implements network.Stream with controllable behavior. +type mockStream struct { + mu deadlock.Mutex + conn *mockConn + proto protocol.ID + dir network.Direction + readErr error // error returned by Read + resetCalled bool + closeCalled bool +} + +func newMockStream(conn *mockConn, proto protocol.ID, dir network.Direction) *mockStream { + return &mockStream{conn: conn, proto: proto, dir: dir} +} + +func (s *mockStream) Read(p []byte) (int, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.readErr != nil { + return 0, s.readErr + } + return 0, nil +} + +func (s *mockStream) Write(p []byte) (int, error) { return len(p), nil } + +func (s *mockStream) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + s.closeCalled = true + return nil +} + +func (s *mockStream) CloseRead() error { return nil } +func (s *mockStream) CloseWrite() error { return nil } + +func (s *mockStream) Reset() error { + s.mu.Lock() + defer s.mu.Unlock() + s.resetCalled = true + return nil +} + +func (s *mockStream) SetDeadline(time.Time) error { return nil } +func (s *mockStream) SetReadDeadline(time.Time) error { return nil } +func (s *mockStream) SetWriteDeadline(time.Time) error { return nil } +func (s *mockStream) Protocol() protocol.ID { return s.proto } +func (s *mockStream) SetProtocol(protocol.ID) error { return nil } +func (s *mockStream) Stat() network.Stats { return network.Stats{Direction: s.dir} } +func (s *mockStream) Conn() network.Conn { return s.conn } +func (s *mockStream) ID() string { return "mock-stream" } +func (s *mockStream) Scope() network.StreamScope { return nil } + +func (s *mockStream) wasReset() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.resetCalled +} + +var _ network.Stream = (*mockStream)(nil) + +// failingHandler is a StreamHandler that always returns errDispatchFailed. +func failingHandler(_ context.Context, _ peer.ID, _ network.Stream, _ bool) error { + return errDispatchFailed +} + +// newTestStreamManager creates a streamManager with a failing handler for testProto. +func newTestStreamManager(localID peer.ID, allowIncoming bool) (*streamManager, *mockHost) { + return newTestStreamManagerWithHandler(localID, allowIncoming, failingHandler) +} + +func newTestStreamManagerWithHandler(localID peer.ID, allowIncoming bool, handler StreamHandler) (*streamManager, *mockHost) { + cm := newMockConnMgr() + h := &mockHost{id: localID, cm: cm} + handlers := StreamHandlers{ + {ProtoID: testProto, Handler: handler}, + } + logger := logging.NewLogger() + logger.SetLevel(logging.Debug) + sm := makeStreamManager(context.Background(), logger, h, handlers, allowIncoming) + return sm, h +} + +// assertStreamMapEmpty checks that sm.streams has no entry for remotePeer. +func assertStreamMapEmpty(t *testing.T, sm *streamManager, remotePeer peer.ID) { + t.Helper() + sm.streamsLock.Lock() + defer sm.streamsLock.Unlock() + _, exists := sm.streams[remotePeer] + require.False(t, exists, "expected n.streams[%s] to be cleaned up after dispatch failure", remotePeer) +} + +// --- test cases --- + +// TestStream_MapCleanupOnDispatchFailure verifies that n.streams is cleaned up +// when dispatch (V22 handshake) fails, across all 8 combinations: +// +// directions (inbound/outbound) × +// peer ID orderings (local < remote / local > remote) × +// dial origins (dialNode / DHT-pubsub) +func TestStream_MapCleanupOnDispatchFailure(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // deterministic peer IDs + lowPeer := peer.ID("AAAA-low-peer") + highPeer := peer.ID("ZZZZ-high-peer") + require.True(t, lowPeer < highPeer) + + // handleConnected path — local node initiates the stream + + // case 1: outbound, localPeer < remotePeer, dialNode + // Connected, peer ID passes, protected, handleConnected, dispatch fails + t.Run("outbound_localLow_dialNode", func(t *testing.T) { + t.Parallel() + sm, h := newTestStreamManager(lowPeer, true) + conn := newMockConn(lowPeer, highPeer, network.DirOutbound) + stream := newMockStream(conn, testProto, network.DirOutbound) + h.newStreamFn = func(context.Context, peer.ID, ...protocol.ID) (network.Stream, error) { + return stream, nil + } + // dialNode would have protected the peer before dialing + h.cm.Protect(highPeer, cnmgrTag) + + sm.handleConnected(conn) + + assertStreamMapEmpty(t, sm, highPeer) + require.True(t, stream.wasReset()) + require.False(t, h.cm.IsProtected(highPeer, cnmgrTag)) + }) + + // case 2: outbound, localPeer < remotePeer, DHT dial + // Connected skips (unprotected). DialPeersUntilTargetCount, Protect, handleConnected, dispatch fails + t.Run("outbound_localLow_dhtDial", func(t *testing.T) { + t.Parallel() + sm, h := newTestStreamManager(lowPeer, true) + conn := newMockConn(lowPeer, highPeer, network.DirOutbound) + stream := newMockStream(conn, testProto, network.DirOutbound) + h.newStreamFn = func(context.Context, peer.ID, ...protocol.ID) (network.Stream, error) { + return stream, nil + } + // DialPeersUntilTargetCount protects then calls handleConnected + h.cm.Protect(highPeer, cnmgrTag) + + sm.handleConnected(conn) + + assertStreamMapEmpty(t, sm, highPeer) + require.True(t, stream.wasReset()) + require.False(t, h.cm.IsProtected(highPeer, cnmgrTag)) + }) + + // case 3: outbound, localPeer > remotePeer, DHT dial + // Connected defers (peer ID). Remote's stream rejected (unprotected outbound). + // DialPeersUntilTargetCount, Protect, handleConnected (new code skips peer ID check), dispatch fails + t.Run("outbound_localHigh_dhtDial", func(t *testing.T) { + t.Parallel() + sm, h := newTestStreamManager(highPeer, true) + conn := newMockConn(highPeer, lowPeer, network.DirOutbound) + stream := newMockStream(conn, testProto, network.DirOutbound) + h.newStreamFn = func(context.Context, peer.ID, ...protocol.ID) (network.Stream, error) { + return stream, nil + } + h.cm.Protect(lowPeer, cnmgrTag) + + sm.handleConnected(conn) + + assertStreamMapEmpty(t, sm, lowPeer) + require.True(t, stream.wasReset()) + require.False(t, h.cm.IsProtected(lowPeer, cnmgrTag)) + }) + + // case 4: inbound, localPeer < remotePeer, remote's dialNode dialed + // Connected, inbound gossip OK, peer ID passes, handleConnected, dispatch fails + t.Run("inbound_localLow_remoteDial", func(t *testing.T) { + t.Parallel() + sm, h := newTestStreamManager(lowPeer, true) + conn := newMockConn(lowPeer, highPeer, network.DirInbound) + stream := newMockStream(conn, testProto, network.DirInbound) + h.newStreamFn = func(context.Context, peer.ID, ...protocol.ID) (network.Stream, error) { + return stream, nil + } + + sm.handleConnected(conn) + + assertStreamMapEmpty(t, sm, highPeer) + require.True(t, stream.wasReset()) + }) + + // case 5: inbound, localPeer < remotePeer, remote's DHT dialed + t.Run("inbound_localLow_remoteDHT", func(t *testing.T) { + t.Parallel() + sm, h := newTestStreamManager(lowPeer, true) + conn := newMockConn(lowPeer, highPeer, network.DirInbound) + stream := newMockStream(conn, testProto, network.DirInbound) + h.newStreamFn = func(context.Context, peer.ID, ...protocol.ID) (network.Stream, error) { + return stream, nil + } + // No protection from our side (remote's inbound conn) + + sm.handleConnected(conn) + + assertStreamMapEmpty(t, sm, highPeer) + require.True(t, stream.wasReset()) + // Unprotect was called but was a no-op (nothing was protected) + require.False(t, h.cm.IsProtected(highPeer, cnmgrTag)) + }) + + // streamHandler path — remote peer creates the stream, our node handles it + + // case 6: outbound, localPeer > remotePeer, our dialNode + // Connected defers (peer ID). Remote opens stream, our streamHandler, dispatch fails + t.Run("outbound_localHigh_ourDial", func(t *testing.T) { + t.Parallel() + sm, h := newTestStreamManager(highPeer, true) + // Connection is outbound (we dialed), stream is inbound (remote initiated) + conn := newMockConn(highPeer, lowPeer, network.DirOutbound) + stream := newMockStream(conn, testProto, network.DirInbound) + // Our dialNode protected this peer + h.cm.Protect(lowPeer, cnmgrTag) + + sm.streamHandler(stream) + + assertStreamMapEmpty(t, sm, lowPeer) + require.True(t, stream.wasReset()) + // dispatched=false => Unprotect called + require.False(t, h.cm.IsProtected(lowPeer, cnmgrTag)) + }) + + // case 7: inbound, localPeer > remotePeer, remote's dialNode dialed us + // Connected defers (peer ID). Remote opens stream, our streamHandler, dispatch fails + t.Run("inbound_localHigh_remoteDial", func(t *testing.T) { + t.Parallel() + sm, h := newTestStreamManager(highPeer, true) + // Connection is inbound (remote dialed us), stream is inbound (remote initiated) + conn := newMockConn(highPeer, lowPeer, network.DirInbound) + stream := newMockStream(conn, testProto, network.DirInbound) + + sm.streamHandler(stream) + + assertStreamMapEmpty(t, sm, lowPeer) + require.True(t, stream.wasReset()) + // No protection was set, so Unprotect is a no-op + require.False(t, h.cm.IsProtected(lowPeer, cnmgrTag)) + }) + + // case 8: inbound, localPeer > remotePeer, remote's DHT dialed us + // Connected defers (peer ID). Remote's DHT connection; + // remote opens stream, our streamHandler, dispatch fails + t.Run("inbound_localHigh_remoteDHT", func(t *testing.T) { + t.Parallel() + sm, h := newTestStreamManager(highPeer, true) + conn := newMockConn(highPeer, lowPeer, network.DirInbound) + stream := newMockStream(conn, testProto, network.DirInbound) + + sm.streamHandler(stream) + + assertStreamMapEmpty(t, sm, lowPeer) + require.True(t, stream.wasReset()) + require.False(t, h.cm.IsProtected(lowPeer, cnmgrTag)) + }) +} + +// TestStream_HandlerKeepsOldStreamOnDispatchFailure verifies that when a new +// stream arrives but dispatch fails, the existing stream is preserved. +func TestStream_HandlerKeepsOldStreamOnDispatchFailure(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + localID := peer.ID("ZZZZ-high-peer") + remoteID := peer.ID("AAAA-low-peer") + sm, h := newTestStreamManager(localID, true) + + // Pre-populate n.streams with an existing stream + conn := newMockConn(localID, remoteID, network.DirInbound) + oldStream := newMockStream(conn, testProto, network.DirInbound) + sm.streams[remoteID] = oldStream + + // Protect so that Unprotect tracking works + h.cm.Protect(remoteID, cnmgrTag) + // New stream arrives from remote peer, dispatch will fail + newStream := newMockStream(conn, testProto, network.DirInbound) + sm.streamHandler(newStream) + + // Old stream is kept because new dispatch failed + sm.streamsLock.Lock() + current, exists := sm.streams[remoteID] + sm.streamsLock.Unlock() + require.True(t, exists, "old stream should still be in the map") + require.Equal(t, oldStream, current, "map should still reference the old stream") + require.False(t, oldStream.closeCalled, "old stream should not be closed") + require.True(t, newStream.wasReset(), "new stream should be reset on dispatch failure") + require.True(t, h.cm.IsProtected(remoteID, cnmgrTag), "peer should remain conn-manager protected after failed replacement") +} + +// blockingMockStream wraps mockStream but makes Read block until the stream is +// explicitly unblocked or closed, simulating a live yamux stream with no data. +type blockingMockStream struct { + mockStream + readStarted chan struct{} + unblockRead chan struct{} +} + +func newBlockingMockStream(conn *mockConn, proto protocol.ID, dir network.Direction) *blockingMockStream { + return &blockingMockStream{ + mockStream: mockStream{conn: conn, proto: proto, dir: dir}, + readStarted: make(chan struct{}), + unblockRead: make(chan struct{}), + } +} + +func (s *blockingMockStream) Read(p []byte) (int, error) { + select { + case <-s.readStarted: + default: + close(s.readStarted) + } + <-s.unblockRead + return 0, io.EOF +} + +func (s *blockingMockStream) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + s.closeCalled = true + // Unblock any pending Read. + select { + case <-s.unblockRead: + default: + close(s.unblockRead) + } + return nil +} + +func (s *blockingMockStream) readWasStarted() bool { + select { + case <-s.readStarted: + return true + default: + return false + } +} + +func closeSignal(ch chan struct{}) { + select { + case <-ch: + default: + close(ch) + } +} + +// TestStream_HandlerDispatchesBeforeTouchingOldStream verifies that +// streamHandler starts dispatch before interacting with any existing stream. +// The pre-fix code called oldStream.Read while holding streamsLock, so this +// test fails immediately if that regression returns. +func TestStream_HandlerDispatchesBeforeTouchingOldStream(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + localID := peer.ID("ZZZZ-high-peer") + remoteID := peer.ID("AAAA-low-peer") + dispatchStarted := make(chan struct{}) + dispatchRelease := make(chan struct{}) + handler := func(_ context.Context, _ peer.ID, _ network.Stream, _ bool) error { + closeSignal(dispatchStarted) + <-dispatchRelease + return nil + } + sm, h := newTestStreamManagerWithHandler(localID, true, handler) + conn := newMockConn(localID, remoteID, network.DirInbound) + + oldStream := newBlockingMockStream(conn, testProto, network.DirInbound) + sm.streams[remoteID] = oldStream + h.cm.Protect(remoteID, cnmgrTag) + t.Cleanup(func() { + closeSignal(dispatchRelease) + _ = oldStream.Close() + }) + + newStream := newMockStream(conn, testProto, network.DirInbound) + streamHandlerDone := make(chan struct{}) + go func() { + sm.streamHandler(newStream) + close(streamHandlerDone) + }() + + select { + case <-dispatchStarted: + case <-oldStream.readStarted: + t.Fatal("streamHandler tried to read the old stream before starting dispatch") + case <-time.After(2 * time.Second): + t.Fatal("streamHandler did not start dispatch") + } + + closeSignal(dispatchRelease) + + require.False(t, oldStream.readWasStarted(), "old stream should never be read") + select { + case <-streamHandlerDone: + case <-time.After(2 * time.Second): + t.Fatal("streamHandler did not complete after dispatch was released") + } + require.False(t, oldStream.readWasStarted(), "old stream should never be read") + + sm.streamsLock.Lock() + current, exists := sm.streams[remoteID] + sm.streamsLock.Unlock() + require.True(t, exists, "replacement stream should be tracked") + require.Equal(t, newStream, current, "replacement stream should be installed in the map") + require.True(t, oldStream.closeCalled, "old stream should be closed after replacement") +} + +// TestStream_DisconnectedCanRunWhileDispatchIsBlocked verifies that +// Disconnected is not blocked by streamHandler while the new stream's dispatch +// is in progress. The pre-fix implementation held streamsLock across a blocking +// Read on the old stream, which prevented Disconnected from making progress. +func TestStream_DisconnectedCanRunWhileDispatchIsBlocked(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + localID := peer.ID("ZZZZ-high-peer") + remoteID := peer.ID("AAAA-low-peer") + dispatchStarted := make(chan struct{}) + dispatchRelease := make(chan struct{}) + handler := func(_ context.Context, _ peer.ID, _ network.Stream, _ bool) error { + closeSignal(dispatchStarted) + <-dispatchRelease + return nil + } + sm, h := newTestStreamManagerWithHandler(localID, true, handler) + conn := newMockConn(localID, remoteID, network.DirInbound) + + oldStream := newBlockingMockStream(conn, testProto, network.DirInbound) + sm.streams[remoteID] = oldStream + h.cm.Protect(remoteID, cnmgrTag) + t.Cleanup(func() { + closeSignal(dispatchRelease) + _ = oldStream.Close() + }) + + newStream := newMockStream(conn, testProto, network.DirInbound) + streamHandlerDone := make(chan struct{}) + go func() { + sm.streamHandler(newStream) + close(streamHandlerDone) + }() + + select { + case <-dispatchStarted: + case <-oldStream.readStarted: + t.Fatal("streamHandler tried to read the old stream before starting dispatch") + case <-time.After(2 * time.Second): + t.Fatal("streamHandler did not start dispatch") + } + + disconnectedDone := make(chan struct{}) + go func() { + sm.Disconnected(nil, conn) + close(disconnectedDone) + }() + + select { + case <-disconnectedDone: + case <-time.After(2 * time.Second): + t.Fatal("Disconnected blocked while streamHandler was dispatching") + } + + require.False(t, oldStream.readWasStarted(), "old stream should never be read") + sm.streamsLock.Lock() + _, exists := sm.streams[remoteID] + sm.streamsLock.Unlock() + require.False(t, exists, "Disconnected should remove the old stream while dispatch is blocked") + + closeSignal(dispatchRelease) + + select { + case <-streamHandlerDone: + case <-time.After(2 * time.Second): + t.Fatal("streamHandler did not complete after dispatch was released") + } + + require.False(t, oldStream.readWasStarted(), "old stream should never be read") + sm.streamsLock.Lock() + current, exists := sm.streams[remoteID] + sm.streamsLock.Unlock() + require.True(t, exists, "replacement stream should be tracked after dispatch completes") + require.Equal(t, newStream, current, "replacement stream should be installed after disconnect cleanup") +} + +func TestStream_ConcurrentFailureDoesNotUnprotectWhileAnotherAttemptInFlight(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + localID := peer.ID("ZZZZ-high-peer") + remoteID := peer.ID("AAAA-low-peer") + conn := newMockConn(localID, remoteID, network.DirInbound) + + var successStream *mockStream + var failStream *mockStream + successStarted := make(chan struct{}) + releaseSuccess := make(chan struct{}) + handler := func(_ context.Context, _ peer.ID, s network.Stream, _ bool) error { + switch s { + case successStream: + closeSignal(successStarted) + <-releaseSuccess + return nil + case failStream: + return errDispatchFailed + default: + return nil + } + } + sm, h := newTestStreamManagerWithHandler(localID, true, handler) + h.cm.Protect(remoteID, cnmgrTag) + successStream = newMockStream(conn, testProto, network.DirInbound) + failStream = newMockStream(conn, testProto, network.DirInbound) + + successDone := make(chan struct{}) + go func() { + sm.streamHandler(successStream) + close(successDone) + }() + + select { + case <-successStarted: + case <-time.After(2 * time.Second): + t.Fatal("success dispatch did not start") + } + + failDone := make(chan struct{}) + go func() { + sm.streamHandler(failStream) + close(failDone) + }() + select { + case <-failDone: + case <-time.After(2 * time.Second): + t.Fatal("failed dispatch did not complete") + } + + require.True(t, h.cm.IsProtected(remoteID, cnmgrTag), "failed attempt must not unprotect while another attempt is in flight") + require.Equal(t, 0, h.cm.UnprotectCalls(remoteID), "no unprotect should happen before the in-flight attempt completes") + + closeSignal(releaseSuccess) + select { + case <-successDone: + case <-time.After(2 * time.Second): + t.Fatal("success dispatch did not complete") + } + + sm.streamsLock.Lock() + current, exists := sm.streams[remoteID] + sm.streamsLock.Unlock() + require.True(t, exists, "successful stream should be tracked") + require.Equal(t, successStream, current, "successful stream should be in the map") + require.True(t, h.cm.IsProtected(remoteID, cnmgrTag), "peer should remain protected after successful stream install") + require.Equal(t, 0, h.cm.UnprotectCalls(remoteID), "successful stream install should not unprotect") + require.True(t, failStream.wasReset(), "failed stream should be reset") +} + +func TestStream_ConcurrentFailedAttemptsUnprotectOnce(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + localID := peer.ID("ZZZZ-high-peer") + remoteID := peer.ID("AAAA-low-peer") + conn := newMockConn(localID, remoteID, network.DirInbound) + + var streamA *mockStream + var streamB *mockStream + started := make(chan struct{}, 2) + release := make(chan struct{}) + handler := func(_ context.Context, _ peer.ID, s network.Stream, _ bool) error { + switch s { + case streamA, streamB: + started <- struct{}{} + <-release + return errDispatchFailed + default: + return errDispatchFailed + } + } + sm, h := newTestStreamManagerWithHandler(localID, true, handler) + h.cm.Protect(remoteID, cnmgrTag) + streamA = newMockStream(conn, testProto, network.DirInbound) + streamB = newMockStream(conn, testProto, network.DirInbound) + + doneA := make(chan struct{}) + go func() { + sm.streamHandler(streamA) + close(doneA) + }() + doneB := make(chan struct{}) + go func() { + sm.streamHandler(streamB) + close(doneB) + }() + + for i := 0; i < 2; i++ { + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("expected both dispatch attempts to start") + } + } + closeSignal(release) + + select { + case <-doneA: + case <-time.After(2 * time.Second): + t.Fatal("first failed dispatch did not complete") + } + select { + case <-doneB: + case <-time.After(2 * time.Second): + t.Fatal("second failed dispatch did not complete") + } + + sm.streamsLock.Lock() + _, exists := sm.streams[remoteID] + sm.streamsLock.Unlock() + require.False(t, exists, "no stream should remain after two failed attempts") + require.False(t, h.cm.IsProtected(remoteID, cnmgrTag), "peer should be unprotected after all attempts failed") + require.Equal(t, 1, h.cm.UnprotectCalls(remoteID), "peer should be unprotected exactly once") + require.True(t, streamA.wasReset(), "first failed stream should be reset") + require.True(t, streamB.wasReset(), "second failed stream should be reset") +} diff --git a/network/p2p/streams_test.go b/network/p2p/streams_test.go index 6e71755d4f..17474ed6b2 100644 --- a/network/p2p/streams_test.go +++ b/network/p2p/streams_test.go @@ -17,30 +17,57 @@ package p2p import ( + "bytes" "context" "strings" "testing" "time" - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/logging" - "github.com/algorand/go-algorand/network/p2p/peerstore" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" + + "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/network/p2p/peerstore" + "github.com/algorand/go-algorand/test/partitiontest" ) +// syncBuffer is a thread-safe bytes.Buffer for use as a log output target. +type syncBuffer struct { + mu deadlock.Mutex + buf bytes.Buffer +} + +func (sb *syncBuffer) Write(p []byte) (int, error) { + sb.mu.Lock() + defer sb.mu.Unlock() + return sb.buf.Write(p) +} + +func (sb *syncBuffer) String() string { + sb.mu.Lock() + defer sb.mu.Unlock() + return sb.buf.String() +} + +func (sb *syncBuffer) Reset() { + sb.mu.Lock() + defer sb.mu.Unlock() + sb.buf.Reset() +} + // TestConnectedLogsNonDialedOutgoingConnection tests that the Connected function // exits early for non-dialed outgoing connections by checking the log output func TestStreamNonDialedOutgoingConnection(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - logBuffer := &strings.Builder{} + logBuffer := &syncBuffer{} logger := logging.NewLogger() logger.SetOutput(logBuffer) logger.SetLevel(logging.Debug) @@ -82,8 +109,8 @@ func TestStreamNonDialedOutgoingConnection(t *testing.T) { ctx := context.Background() handlers := StreamHandlers{} - dialerSM = makeStreamManager(ctx, logger, cfg, dialerHost, handlers, false) - listenerSM = makeStreamManager(ctx, logger, cfg, listenerHost, handlers, false) + dialerSM = makeStreamManager(ctx, logger, dialerHost, handlers, false) + listenerSM = makeStreamManager(ctx, logger, listenerHost, handlers, false) // Setup Connected notification dialerHost.Network().Notify(dialerSM) @@ -110,9 +137,9 @@ func TestStreamNonDialedOutgoingConnection(t *testing.T) { require.Len(t, conns, 1) require.Equal(t, network.DirOutbound, conns[0].Stat().Direction) - // Check that the log contains the expected message for non-dialed outgoing connection - logOutput := logBuffer.String() - expectedMsg := "ignoring non-dialed outgoing peer ID" - require.Contains(t, logOutput, expectedMsg) - require.Contains(t, logOutput, listenerHost.ID().String()) + const expectedMsg = "ignoring non-dialed outgoing peer ID" + require.Eventually(t, func() bool { + logOutput := logBuffer.String() + return strings.Contains(logOutput, expectedMsg) && strings.Contains(logOutput, listenerHost.ID().String()) + }, 5*time.Second, 50*time.Millisecond) } diff --git a/network/p2p/testing/httpNode.go b/network/p2p/testing/httpNode.go index b1e8555ce5..8a538173e4 100644 --- a/network/p2p/testing/httpNode.go +++ b/network/p2p/testing/httpNode.go @@ -23,13 +23,14 @@ import ( "net/http" "testing" - "github.com/algorand/go-algorand/components/mocks" - "github.com/algorand/go-algorand/network" - "github.com/algorand/go-algorand/network/p2p" "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/components/mocks" + "github.com/algorand/go-algorand/network" + "github.com/algorand/go-algorand/network/p2p" ) // HTTPNode is a mock network node that uses libp2p and http. @@ -66,7 +67,9 @@ func (p *HTTPNode) RegisterHandlers(dispatch []network.TaggedMessageHandler) {} func (p *HTTPNode) Start() error { go func() { err := p.httpServer.Serve() - require.NoError(p.tb, err) + if err != nil { + require.ErrorIs(p.tb, err, http.ErrServerClosed) + } }() return nil } diff --git a/network/p2pMetainfo.go b/network/p2pMetainfo.go index 0646e817f6..10f11faa8d 100644 --- a/network/p2pMetainfo.go +++ b/network/p2pMetainfo.go @@ -23,8 +23,9 @@ import ( "math" "net/http" - "github.com/algorand/go-algorand/data/basics" "github.com/libp2p/go-libp2p/core/peer" + + "github.com/algorand/go-algorand/data/basics" ) // peerMetaHeaders holds peer metadata headers similar to wsnet http.Header @@ -69,17 +70,17 @@ type peerMetaInfo struct { func readPeerMetaHeaders(stream io.ReadWriter, p2pPeer peer.ID, netProtoSupportedVersions []string) (peerMetaInfo, error) { var msgLenBytes [2]byte - rn, err := stream.Read(msgLenBytes[:]) - if rn != 2 || err != nil { + _, err := io.ReadFull(stream, msgLenBytes[:]) + if err != nil { err0 := fmt.Errorf("error reading response message length from peer %s: %w", p2pPeer, err) return peerMetaInfo{}, err0 } msgLen := binary.BigEndian.Uint16(msgLenBytes[:]) msgBytes := make([]byte, msgLen) - rn, err = stream.Read(msgBytes[:]) - if rn != int(msgLen) || err != nil { - err0 := fmt.Errorf("error reading response message from peer %s: %w, expected: %d, read: %d", p2pPeer, err, msgLen, rn) + _, err = io.ReadFull(stream, msgBytes) + if err != nil { + err0 := fmt.Errorf("error reading response message from peer %s: %w", p2pPeer, err) return peerMetaInfo{}, err0 } var responseHeaders peerMetaHeaders diff --git a/network/p2pMetainfo_test.go b/network/p2pMetainfo_test.go index 2d65ee2362..50aaf0f89a 100644 --- a/network/p2pMetainfo_test.go +++ b/network/p2pMetainfo_test.go @@ -22,11 +22,12 @@ import ( "net/http" "testing" - "github.com/algorand/go-algorand/logging" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/test/partitiontest" ) // MockStream is a io.ReaderWriter testing mock @@ -80,29 +81,28 @@ func TestReadPeerMetaHeaders(t *testing.T) { assert.Equal(t, "mockFeatures", metaInfo.features) mockStream.AssertExpectations(t) - // Error case: incomplete length read + // Error case: incomplete length read then EOF mockStream = new(MockStream) mockStream.On("Read", mock.Anything).Return([]byte{1}, nil).Once() + mockStream.On("Read", mock.Anything).Return([]byte{}, fmt.Errorf("EOF")).Once() _, err = readPeerMetaHeaders(mockStream, p2pPeer, n.supportedProtocolVersions) - assert.Error(t, err) - assert.Contains(t, err.Error(), "error reading response message length") + assert.ErrorContains(t, err, "error reading response message length") mockStream.AssertExpectations(t) // Error case: error reading length mockStream = new(MockStream) mockStream.On("Read", mock.Anything).Return([]byte{}, fmt.Errorf("read error")).Once() _, err = readPeerMetaHeaders(mockStream, p2pPeer, n.supportedProtocolVersions) - assert.Error(t, err) - assert.Contains(t, err.Error(), "error reading response message length") + assert.ErrorContains(t, err, "error reading response message length") mockStream.AssertExpectations(t) - // Error case: incomplete message read + // Error case: incomplete message read then EOF mockStream = new(MockStream) mockStream.On("Read", mock.Anything).Return(lengthBytes, nil).Once() - mockStream.On("Read", mock.Anything).Return(data[:len(data)/2], nil).Once() // Return only half the data + mockStream.On("Read", mock.Anything).Return(data[:len(data)/2], nil).Once() + mockStream.On("Read", mock.Anything).Return([]byte{}, fmt.Errorf("EOF")).Once() _, err = readPeerMetaHeaders(mockStream, p2pPeer, n.supportedProtocolVersions) - assert.Error(t, err) - assert.Contains(t, err.Error(), "error reading response message") + assert.ErrorContains(t, err, "error reading response message") mockStream.AssertExpectations(t) // Error case: error reading message @@ -110,8 +110,7 @@ func TestReadPeerMetaHeaders(t *testing.T) { mockStream.On("Read", mock.Anything).Return(lengthBytes, nil).Once() mockStream.On("Read", mock.Anything).Return([]byte{}, fmt.Errorf("read error")).Once() _, err = readPeerMetaHeaders(mockStream, p2pPeer, n.supportedProtocolVersions) - assert.Error(t, err) - assert.Contains(t, err.Error(), "error reading response message") + assert.ErrorContains(t, err, "error reading response message") mockStream.AssertExpectations(t) // Error case: invalid messagepack (unmarshaling error) @@ -121,8 +120,7 @@ func TestReadPeerMetaHeaders(t *testing.T) { mockStream.On("Read", mock.Anything).Return(corruptedMsgpLength, nil).Once() mockStream.On("Read", mock.Anything).Return([]byte{0x99, 0x01, 0x02}, nil).Once() _, err = readPeerMetaHeaders(mockStream, p2pPeer, n.supportedProtocolVersions) - assert.Error(t, err) - assert.Contains(t, err.Error(), "error unmarshaling response message") + assert.ErrorContains(t, err, "error unmarshaling response message") mockStream.AssertExpectations(t) // Error case: no matching protocol version @@ -138,8 +136,27 @@ func TestReadPeerMetaHeaders(t *testing.T) { mockStream.On("Read", mock.Anything).Return(incompatibleLengthBytes, nil).Once() mockStream.On("Read", mock.Anything).Return(incompatibleData, nil).Once() _, err = readPeerMetaHeaders(mockStream, p2pPeer, n.supportedProtocolVersions) - assert.Error(t, err) - assert.Contains(t, err.Error(), "does not support any of the supported protocol versions") + assert.ErrorContains(t, err, "does not support any of the supported protocol versions") + mockStream.AssertExpectations(t) + + // Verify short reads are handled: length arrives in two reads + mockStream = new(MockStream) + mockStream.On("Read", mock.Anything).Return(lengthBytes[:1], nil).Once() + mockStream.On("Read", mock.Anything).Return(lengthBytes[1:], nil).Once() + mockStream.On("Read", mock.Anything).Return(data, nil).Once() + metaInfo, err = readPeerMetaHeaders(mockStream, p2pPeer, n.supportedProtocolVersions) + assert.NoError(t, err) + assert.Equal(t, "1.0", metaInfo.version) + mockStream.AssertExpectations(t) + + // Verify short reads are handled: body arrives in two reads + mockStream = new(MockStream) + mockStream.On("Read", mock.Anything).Return(lengthBytes, nil).Once() + mockStream.On("Read", mock.Anything).Return(data[:len(data)/2], nil).Once() + mockStream.On("Read", mock.Anything).Return(data[len(data)/2:], nil).Once() + metaInfo, err = readPeerMetaHeaders(mockStream, p2pPeer, n.supportedProtocolVersions) + assert.NoError(t, err) + assert.Equal(t, "1.0", metaInfo.version) mockStream.AssertExpectations(t) } @@ -171,7 +188,6 @@ func TestWritePeerMetaHeaders(t *testing.T) { mockStream = new(MockStream) mockStream.On("Write", mock.Anything).Return(0, fmt.Errorf("write error")).Once() err = writePeerMetaHeaders(mockStream, p2pPeer, "1.0", n) - assert.Error(t, err) - assert.Contains(t, err.Error(), "error sending initial message") + assert.ErrorContains(t, err, "error sending initial message") mockStream.AssertExpectations(t) } diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 452b843369..a2e820f344 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -25,6 +25,14 @@ import ( "sync/atomic" "time" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" algocrypto "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/logging" @@ -35,13 +43,6 @@ import ( "github.com/algorand/go-algorand/network/p2p/peerstore" "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-deadlock" - - pubsub "github.com/libp2p/go-libp2p-pubsub" - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr/net" ) // some arbitrary number TODO: figure out a better value based on peerSelector/fetcher algorithm @@ -592,9 +593,9 @@ func (n *P2PNetwork) httpdThread() { defer n.wg.Done() err := n.httpServer.Serve() - if err != nil { + if err == http.ErrServerClosed { + } else if err != nil { n.log.Errorf("Error serving libp2phttp: %v", err) - return } } @@ -901,34 +902,37 @@ func (n *P2PNetwork) VoteCompressionEnabled() bool { // wsStreamHandlerV1 is a callback that the p2p package calls when a new peer connects and establishes a // stream for the websocket protocol. // TODO: remove after consensus v41 takes effect. -func (n *P2PNetwork) wsStreamHandlerV1(ctx context.Context, p2pPeer peer.ID, stream network.Stream, incoming bool) { +func (n *P2PNetwork) wsStreamHandlerV1(ctx context.Context, p2pPeer peer.ID, stream network.Stream, incoming bool) error { if stream.Protocol() != p2p.AlgorandWsProtocolV1 { - n.log.Warnf("unknown protocol %s from peer %s", stream.Protocol(), p2pPeer) - return + err := fmt.Errorf("unknown protocol %s from peer %s", stream.Protocol(), p2pPeer) + n.log.Warn(err.Error()) + return err } if incoming { var initMsg [1]byte rn, err := stream.Read(initMsg[:]) if rn == 0 || err != nil { - n.log.Warnf("wsStreamHandlerV1: error reading initial message from peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) - return + err1 := fmt.Errorf("wsStreamHandlerV1: error reading initial message from peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + n.log.Warn(err1.Error()) + return err1 } } else { - _, err := stream.Write([]byte("1")) - if err != nil { - n.log.Warnf("wsStreamHandlerV1: error sending initial message: %v", err) - return + if _, err := stream.Write([]byte("1")); err != nil { + err1 := fmt.Errorf("wsStreamHandlerV1: error sending initial message to peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + n.log.Warn(err1.Error()) + return err1 } } - n.baseWsStreamHandler(ctx, p2pPeer, stream, incoming, peerMetaInfo{}) + return n.baseWsStreamHandler(ctx, p2pPeer, stream, incoming, peerMetaInfo{}) } -func (n *P2PNetwork) wsStreamHandlerV22(ctx context.Context, p2pPeer peer.ID, stream network.Stream, incoming bool) { +func (n *P2PNetwork) wsStreamHandlerV22(ctx context.Context, p2pPeer peer.ID, stream network.Stream, incoming bool) error { if stream.Protocol() != p2p.AlgorandWsProtocolV22 { - n.log.Warnf("unknown protocol %s from peer%s", stream.Protocol(), p2pPeer) - return + err := fmt.Errorf("unknown protocol %s from peer %s", stream.Protocol(), p2pPeer) + n.log.Warn(err.Error()) + return err } var err error @@ -936,35 +940,39 @@ func (n *P2PNetwork) wsStreamHandlerV22(ctx context.Context, p2pPeer peer.ID, st if incoming { pmi, err = readPeerMetaHeaders(stream, p2pPeer, n.supportedProtocolVersions) if err != nil { - n.log.Warnf("wsStreamHandlerV22: error reading peer meta headers response from peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + err1 := fmt.Errorf("wsStreamHandlerV22: error reading peer meta headers response from peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + n.log.Warn(err1.Error()) _ = stream.Reset() - return + return err1 } err = writePeerMetaHeaders(stream, p2pPeer, pmi.version, n) if err != nil { - n.log.Warnf("wsStreamHandlerV22: error writing peer meta headers response to peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + err1 := fmt.Errorf("wsStreamHandlerV22: error writing peer meta headers response to peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + n.log.Warn(err1.Error()) _ = stream.Reset() - return + return err1 } } else { err = writePeerMetaHeaders(stream, p2pPeer, n.protocolVersion, n) if err != nil { - n.log.Warnf("wsStreamHandlerV22: error writing peer meta headers response to peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + err1 := fmt.Errorf("wsStreamHandlerV22: error writing peer meta headers response to peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + n.log.Warn(err1.Error()) _ = stream.Reset() - return + return err1 } // read the response pmi, err = readPeerMetaHeaders(stream, p2pPeer, n.supportedProtocolVersions) if err != nil { - n.log.Warnf("wsStreamHandlerV22: error reading peer meta headers response from peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + err1 := fmt.Errorf("wsStreamHandlerV22: error reading peer meta headers response from peer %s (%s): %v", p2pPeer, stream.Conn().RemoteMultiaddr().String(), err) + n.log.Warn(err1.Error()) _ = stream.Reset() - return + return err1 } } - n.baseWsStreamHandler(ctx, p2pPeer, stream, incoming, pmi) + return n.baseWsStreamHandler(ctx, p2pPeer, stream, incoming, pmi) } -func (n *P2PNetwork) baseWsStreamHandler(ctx context.Context, p2pPeer peer.ID, stream network.Stream, incoming bool, pmi peerMetaInfo) { +func (n *P2PNetwork) baseWsStreamHandler(ctx context.Context, p2pPeer peer.ID, stream network.Stream, incoming bool, pmi peerMetaInfo) error { // get address for peer ID ma := stream.Conn().RemoteMultiaddr() addr := ma.String() @@ -1023,18 +1031,20 @@ func (n *P2PNetwork) baseWsStreamHandler(ctx context.Context, p2pPeer peer.ID, s n.wsPeersLock.Unlock() if !ok { networkPeerIdentityDisconnect.Inc(nil) - n.log.With("remote", addr).With("local", localAddr).Warn("peer deduplicated before adding because the identity is already known") + err := fmt.Errorf("peer deduplicated before adding because the identity is already known: remote %s, local %s", addr, localAddr) + n.log.Warn(err.Error()) stream.Close() - return + return err } wsp.init(n.config, outgoingMessagesBufferSize) n.wsPeersLock.Lock() if wsp.didSignalClose.Load() == 1 { networkPeerAlreadyClosed.Inc(nil) - n.log.Debugf("peer closing %s", addr) + err := fmt.Errorf("peer closing %s", addr) + n.log.Debug(err.Error()) n.wsPeersLock.Unlock() - return + return err } n.wsPeers[p2pPeer] = wsp n.wsPeersToIDs[wsp] = p2pPeer @@ -1062,6 +1072,7 @@ func (n *P2PNetwork) baseWsStreamHandler(ctx context.Context, p2pPeer peer.ID, s Incoming: incoming, InstanceName: wsp.InstanceName, }) + return nil } // peerRemoteClose called from wsPeer to report that it has closed @@ -1071,11 +1082,36 @@ func (n *P2PNetwork) peerRemoteClose(peer *wsPeer, reason disconnectReason) { } func (n *P2PNetwork) removePeer(peer *wsPeer, remotePeerID peer.ID, reason disconnectReason) { + removed := false + n.wsPeersLock.Lock() - n.identityTracker.removeIdentity(peer) - delete(n.wsPeers, remotePeerID) - delete(n.wsPeersToIDs, peer) + n.identityTracker.removeIdentity(peer) // safe: removeIdentity only deletes if the stored identity matches this exact wsPeer + if cur, ok := n.wsPeers[remotePeerID]; ok && cur == peer { + delete(n.wsPeers, remotePeerID) + removed = true + } + _, knownPeer := n.wsPeersToIDs[peer] + delete(n.wsPeersToIDs, peer) // always delete reverse entry for this exact wsPeer + + // Unprotect while still holding wsPeersLock so we can't race with a new + // wsPeer insertion for the same remotePeerID between map deletion and + // unprotect. + if removed { + n.service.UnprotectPeer(remotePeerID) + } n.wsPeersLock.Unlock() + + // Throttle slots are per-wsPeer, not per map entry: release for any + // known wsPeer on its first cleanup, even if it was already replaced. + if knownPeer && peer.throttledOutgoingConnection { + n.throttledOutgoingConnections.Add(int32(1)) + } + + if !removed { + // stale close from an old stream/wsPeer that was already replaced; skip + // unprotect, disconnect telemetry, and counter updates. + return + } n.wsPeersChangeCounter.Add(1) eventDetails := telemetryspec.PeerEventDetails{ @@ -1098,9 +1134,6 @@ func (n *P2PNetwork) removePeer(peer *wsPeer, remotePeerID peer.ID, reason disco AVCount: peer.avMessageCount.Load(), PPCount: peer.ppMessageCount.Load(), }) - if peer.throttledOutgoingConnection { - n.throttledOutgoingConnections.Add(int32(1)) - } } func (n *P2PNetwork) peerSnapshot(dest []*wsPeer) ([]*wsPeer, int32) { diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 2e5f1617a0..33e75739a1 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -28,6 +28,17 @@ import ( "testing" "time" + pubsub "github.com/libp2p/go-libp2p-pubsub" + pb "github.com/libp2p/go-libp2p-pubsub/pb" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + ma "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" algocrypto "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/logging" @@ -39,16 +50,6 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/uuid" - "github.com/algorand/go-deadlock" - - pubsub "github.com/libp2p/go-libp2p-pubsub" - pb "github.com/libp2p/go-libp2p-pubsub/pb" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - ma "github.com/multiformats/go-multiaddr" - "github.com/stretchr/testify/require" ) func (n *P2PNetwork) hasPeers() bool { @@ -325,9 +326,12 @@ func TestP2PSubmitWS(t *testing.T) { } type mockService struct { - id peer.ID - addrs []ma.Multiaddr - peers map[peer.ID]peer.AddrInfo + id peer.ID + addrs []ma.Multiaddr + peers map[peer.ID]peer.AddrInfo + unprotectStarted chan struct{} + unprotectRelease <-chan struct{} + unprotectCalls atomic.Int32 } func (s *mockService) Start() error { @@ -362,6 +366,20 @@ func (s *mockService) ClosePeer(peer peer.ID) error { return nil } +func (s *mockService) UnprotectPeer(peer.ID) { + s.unprotectCalls.Add(1) + if s.unprotectStarted != nil { + select { + case <-s.unprotectStarted: + default: + close(s.unprotectStarted) + } + } + if s.unprotectRelease != nil { + <-s.unprotectRelease + } +} + func (s *mockService) Conns() []network.Conn { return nil } @@ -394,6 +412,78 @@ func makeMockService(id peer.ID, addrs []ma.Multiaddr) *mockService { } } +func TestP2PRemovePeerHoldsLockAcrossUnprotect(t *testing.T) { + partitiontest.PartitionTest(t) + + remotePeerID := peer.ID("12D3KooWRemotePeer") + oldPeer := &wsPeer{} + newPeer := &wsPeer{} + unprotectRelease := make(chan struct{}) + + mockSvc := &mockService{ + id: peer.ID("12D3KooWSelfPeer"), + unprotectStarted: make(chan struct{}), + unprotectRelease: unprotectRelease, + } + net := &P2PNetwork{ + log: logging.TestingLog(t), + service: mockSvc, + wsPeers: make(map[peer.ID]*wsPeer), + wsPeersToIDs: make(map[*wsPeer]peer.ID), + identityTracker: noopIdentityTracker{}, + } + net.wsPeers[remotePeerID] = oldPeer + net.wsPeersToIDs[oldPeer] = remotePeerID + + removeDone := make(chan struct{}) + go func() { + net.removePeer(oldPeer, remotePeerID, disconnectReasonNone) + close(removeDone) + }() + + select { + case <-mockSvc.unprotectStarted: + case <-time.After(2 * time.Second): + t.Fatal("removePeer did not enter UnprotectPeer") + } + + addDone := make(chan struct{}) + go func() { + net.wsPeersLock.Lock() + net.wsPeers[remotePeerID] = newPeer + net.wsPeersToIDs[newPeer] = remotePeerID + net.wsPeersLock.Unlock() + close(addDone) + }() + + select { + case <-addDone: + t.Fatal("wsPeersLock was released while UnprotectPeer was still running") + case <-time.After(100 * time.Millisecond): + // expected: add path stays blocked until UnprotectPeer returns + } + + close(unprotectRelease) + + select { + case <-removeDone: + case <-time.After(2 * time.Second): + t.Fatal("removePeer did not finish after UnprotectPeer was released") + } + select { + case <-addDone: + case <-time.After(2 * time.Second): + t.Fatal("add path did not complete after removePeer finished") + } + + require.Equal(t, int32(1), mockSvc.unprotectCalls.Load(), "expected exactly one unprotect call") + net.wsPeersLock.RLock() + current, ok := net.wsPeers[remotePeerID] + net.wsPeersLock.RUnlock() + require.True(t, ok, "replacement peer should be present") + require.Equal(t, newPeer, current, "replacement peer should remain mapped") +} + func TestP2PNetworkAddress(t *testing.T) { partitiontest.PartitionTest(t) @@ -853,6 +943,19 @@ func TestP2PHTTPHandlerAllInterfaces(t *testing.T) { } +// baseTestMeshCreator is a simple wrapper to baseMeshCreator for TestP2PRelay +// in order to provide custom GossipSubHeartbeatInterval option via makeConfig +// to speed up heartbeat to reduce test flakiness of TestP2PRelay from pubsub mesh establishment timing +type baseTestMeshCreator struct { + baseMeshCreator +} + +func (c baseTestMeshCreator) makeConfig(wsnet *WebsocketNetwork, p2pnet *P2PNetwork) networkConfig { + return networkConfig{ + pubsubOpts: []p2p.PubSubOption{p2p.SetPubSubHeartbeatInterval(200 * time.Millisecond)}, + } +} + // TestP2PRelay checks p2p nodes can properly relay messages: // netA and netB are started with ForceFetchTransactions so it subscribes to the txn topic, // both of them are connected and do not relay messages. @@ -862,11 +965,6 @@ func TestP2PHTTPHandlerAllInterfaces(t *testing.T) { func TestP2PRelay(t *testing.T) { partitiontest.PartitionTest(t) - // Speed up heartbeat to reduce test flakiness from mesh establishment timing - oldHeartbeatInterval := pubsub.GossipSubHeartbeatInterval - pubsub.GossipSubHeartbeatInterval = 200 * time.Millisecond - defer func() { pubsub.GossipSubHeartbeatInterval = oldHeartbeatInterval }() - cfg := config.GetDefaultLocal() cfg.DNSBootstrapID = "" // disable DNS lookups since the test uses phonebook addresses cfg.ForceFetchTransactions = true @@ -875,7 +973,7 @@ func TestP2PRelay(t *testing.T) { log := logging.TestingLog(t) genesisInfo := GenesisInfo{genesisID, config.Devtestnet} log.Debugln("Starting netA") - netA, err := NewP2PNetwork(log.With("net", "netA"), cfg, "", nil, genesisInfo, &nopeNodeInfo{}, nil, nil) + netA, err := NewP2PNetwork(log.With("net", "netA"), cfg, "", nil, genesisInfo, &nopeNodeInfo{}, nil, baseTestMeshCreator{}) require.NoError(t, err) err = netA.Start() diff --git a/network/p2pPeer.go b/network/p2pPeer.go index fae62f5379..9eff715488 100644 --- a/network/p2pPeer.go +++ b/network/p2pPeer.go @@ -23,12 +23,13 @@ import ( "net" "time" - "github.com/algorand/go-algorand/logging" - "github.com/algorand/websocket" - "github.com/libp2p/go-libp2p/core/network" yamux "github.com/libp2p/go-yamux/v4" mnet "github.com/multiformats/go-multiaddr/net" + + "github.com/algorand/websocket" + + "github.com/algorand/go-algorand/logging" ) type wsPeerConnP2P struct { diff --git a/network/phonebook/phonebook_test.go b/network/phonebook/phonebook_test.go index ed2da2dbaf..a2f3a5ba95 100644 --- a/network/phonebook/phonebook_test.go +++ b/network/phonebook/phonebook_test.go @@ -21,8 +21,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func testPhonebookAll(t *testing.T, set []string, ph Phonebook) { diff --git a/network/topics_test.go b/network/topics_test.go index 0c830bc0a5..35a50f6c0b 100644 --- a/network/topics_test.go +++ b/network/topics_test.go @@ -21,8 +21,9 @@ import ( "fmt" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) // Test the marshall/unmarshall of Topics diff --git a/network/vpack/dynamic_vpack_test.go b/network/vpack/dynamic_vpack_test.go index 612c9dc404..7820b25526 100644 --- a/network/vpack/dynamic_vpack_test.go +++ b/network/vpack/dynamic_vpack_test.go @@ -23,11 +23,12 @@ import ( "testing" "unsafe" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TestStatefulEncoderDecoderSequence verifies that a StatefulEncoder/StatefulDecoder diff --git a/network/vpack/lru_table_test.go b/network/vpack/lru_table_test.go index 3e5d0a9c53..4bc96ec9c6 100644 --- a/network/vpack/lru_table_test.go +++ b/network/vpack/lru_table_test.go @@ -22,9 +22,10 @@ import ( "testing" "testing/quick" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestLRUTableSizeValidation(t *testing.T) { @@ -32,13 +33,11 @@ func TestLRUTableSizeValidation(t *testing.T) { // Test invalid size (not power of 2) _, err := NewStatefulEncoder(100) - require.Error(t, err) - require.Contains(t, err.Error(), "must be a power of 2") + require.ErrorContains(t, err, "must be a power of 2") // Test invalid size (too small) _, err = NewStatefulEncoder(8) - require.Error(t, err) - require.Contains(t, err.Error(), "at least 16") + require.ErrorContains(t, err, "at least 16") // Test valid sizes for _, size := range []uint{16, 32, 64, 128, 256, 512, 1024, 2048} { diff --git a/network/vpack/parse_test.go b/network/vpack/parse_test.go index f69c577953..def4d5858c 100644 --- a/network/vpack/parse_test.go +++ b/network/vpack/parse_test.go @@ -19,10 +19,11 @@ package vpack import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/assert" ) // a string that is greater than the max 5-bit fixmap size @@ -187,3 +188,66 @@ func TestParseVoteTrailingDataErr(t *testing.T) { _, err := se.CompressVote(nil, buf) assert.ErrorContains(t, err, "unexpected trailing data") } + +// TestUncompressedMsgpackDetection tests that decompression errors on uncompressed +// msgpack data get wrapped with ErrLikelyUncompressed, while corrupted vpack data does not. +func TestUncompressedMsgpackDetection(t *testing.T) { + partitiontest.PartitionTest(t) + + // Create a vote structure shared by both subtests + vote := map[string]any{ + "cred": map[string]any{"pf": crypto.VrfProof{7, 8, 9}}, + "r": map[string]any{ + "rnd": uint64(1000), + "per": uint64(5), + "step": uint64(2), + "snd": [32]byte{1, 2, 3}, + "prop": map[string]any{ + "dig": [32]byte{4, 5, 6}, + }, + }, + "sig": map[string]any{ + "s": [64]byte{10, 11, 12}, + "p": [32]byte{13, 14, 15}, + "p2": [32]byte{16, 17, 18}, + "p1s": [64]byte{22, 23, 24}, + "p2s": [64]byte{25, 26, 27}, + "ps": [64]byte{}, + }, + } + + t.Run("uncompressed_detected", func(t *testing.T) { + msgpackData := protocol.EncodeReflect(vote) + + // Verify it starts with the uncompressed msgpack pattern + assert.GreaterOrEqual(t, len(msgpackData), 6) + assert.Equal(t, byte(0x83), msgpackData[0], "uncompressed vote should start with fixmap(3)") + assert.Equal(t, byte(0xa4), msgpackData[1], "followed by fixstr(4)") + assert.Equal(t, "cred", string(msgpackData[2:6]), "field name should be 'cred'") + + // Try to decompress as vpack - should fail with ErrLikelyUncompressed + var dec StatelessDecoder + _, err := dec.DecompressVote(nil, msgpackData) + + assert.Error(t, err) + assert.ErrorIs(t, err, ErrLikelyUncompressed, "should detect uncompressed msgpack pattern") + assert.ErrorContains(t, err, "data appears to be uncompressed msgpack") + }) + + t.Run("corrupted_vpack_not_detected", func(t *testing.T) { + msgpackData := protocol.EncodeReflect(vote) + var enc StatelessEncoder + compressed, err := enc.CompressVote(nil, msgpackData) + assert.NoError(t, err) + + // Corrupt the compressed data by truncating it + corrupted := compressed[:len(compressed)/2] + + // Try to decompress - should fail but NOT with ErrLikelyUncompressed + var dec StatelessDecoder + _, err = dec.DecompressVote(nil, corrupted) + + assert.Error(t, err) + assert.NotErrorIs(t, err, ErrLikelyUncompressed, "corrupted vpack should not be detected as uncompressed msgpack") + }) +} diff --git a/network/vpack/proposal_window_test.go b/network/vpack/proposal_window_test.go index 0161107240..a8738ba8d3 100644 --- a/network/vpack/proposal_window_test.go +++ b/network/vpack/proposal_window_test.go @@ -19,8 +19,9 @@ package vpack import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func makeTestPropBundle(seed byte) proposalEntry { diff --git a/network/vpack/rapid_test.go b/network/vpack/rapid_test.go index fc58c57528..32c63a0bd3 100644 --- a/network/vpack/rapid_test.go +++ b/network/vpack/rapid_test.go @@ -23,12 +23,13 @@ import ( "testing" "unsafe" + "github.com/stretchr/testify/require" + "pgregory.net/rapid" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" - "pgregory.net/rapid" ) // TestCheckStatelessEncoder tests the StatelessEncoder/Decoder using randomly generated votes diff --git a/network/vpack/vpack.go b/network/vpack/vpack.go index 4d20275b97..5bafe269df 100644 --- a/network/vpack/vpack.go +++ b/network/vpack/vpack.go @@ -215,9 +215,31 @@ func (d *StatelessDecoder) proposalValueMapSize(mask uint8) uint8 { return uint8(bits.OnesCount8(mask & (bitDig | bitEncDig | bitOper | bitOprop))) } +// ErrLikelyUncompressed is returned when vpack decompression detects what appears +// to be uncompressed msgpack data from a peer claiming vpack support. +var ErrLikelyUncompressed = fmt.Errorf("data appears to be uncompressed msgpack") + +// isLikelyUncompressedMsgpack checks if the source data looks like an uncompressed +// msgpack vote that was mistakenly treated as vpack-compressed. +func isLikelyUncompressedMsgpack(src []byte) bool { + // uncompressed msgpack votes start with 0x83 (fixmap marker with 3 elements: cred, r, sig), + // followed by 0xa4 (fixstr marker of length 4), then "cred" + return len(src) > 5 && src[0] == 0x83 && src[1] == 0xa4 && + src[2] == 'c' && src[3] == 'r' && src[4] == 'e' && src[5] == 'd' +} + // DecompressVote decodes a compressed vote in src and appends it to dst. // To re-use dst, run like: dst = dec.DecompressVote(dst[:0], src) func (d *StatelessDecoder) DecompressVote(dst, src []byte) ([]byte, error) { + result, err := d.decompressVote(dst, src) + if err != nil && isLikelyUncompressedMsgpack(src) { + return nil, fmt.Errorf("%w: %v", ErrLikelyUncompressed, err) + } + return result, err +} + +// decompressVote performs the actual decompression logic. +func (d *StatelessDecoder) decompressVote(dst, src []byte) ([]byte, error) { if len(src) < 2 { return nil, fmt.Errorf("header missing") } diff --git a/network/vpack/vpack_test.go b/network/vpack/vpack_test.go index 3b6bc5643c..78fe3691aa 100644 --- a/network/vpack/vpack_test.go +++ b/network/vpack/vpack_test.go @@ -23,11 +23,12 @@ import ( "testing" "unsafe" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // checkVoteValid analyzes a vote to determine if it would cause compression errors and what kind. diff --git a/network/websocketProxy_test.go b/network/websocketProxy_test.go index a08c18db1e..f08f397b38 100644 --- a/network/websocketProxy_test.go +++ b/network/websocketProxy_test.go @@ -28,10 +28,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + + "github.com/algorand/websocket" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/algorand/websocket" - "github.com/stretchr/testify/require" ) var testProxyUpgrader = websocket.Upgrader{ diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 765bdd93dd..9a9694828e 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -38,9 +38,10 @@ import ( "sync/atomic" "time" + "github.com/gorilla/mux" + "github.com/algorand/go-deadlock" "github.com/algorand/websocket" - "github.com/gorilla/mux" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -671,7 +672,7 @@ func (wn *WebsocketNetwork) Start() error { wn.messagesOfInterestEnc = marshallMessageOfInterestMap(wn.messagesOfInterest) } - if wn.relayMessages { + if wn.relayMessages && wn.config.IncomingConnectionsLimit != 0 { listener, err := net.Listen("tcp", wn.config.NetAddress) if err != nil { wn.log.Errorf("network could not listen %v: %s", wn.config.NetAddress, err) @@ -683,6 +684,8 @@ func (wn *WebsocketNetwork) Start() error { // wrap the limited connection listener with a requests tracker listener wn.listener = wn.requestsTracker.Listener(listener) wn.log.Debugf("listening on %s", wn.listener.Addr().String()) + } + if wn.relayMessages { wn.throttledOutgoingConnections.Store(int32(wn.config.GossipFanout / 2)) } else { // on non-relay, all the outgoing connections are throttled. diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index 8603fbf4ff..11c11a6d14 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -40,20 +40,19 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/internal/rapidgen" - "github.com/algorand/go-algorand/network/phonebook" - "pgregory.net/rapid" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "pgregory.net/rapid" "github.com/algorand/go-deadlock" "github.com/algorand/websocket" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/internal/rapidgen" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" + "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util" @@ -285,6 +284,20 @@ func TestWebsocketNetworkStartStop(t *testing.T) { netA.Stop() } +func TestWebsocketNetworkStartZeroIncomingDoesNotListen(t *testing.T) { + partitiontest.PartitionTest(t) + + netA := makeTestWebsocketNode(t) + netA.config.IncomingConnectionsLimit = 0 + + require.NoError(t, netA.Start()) + defer netA.Stop() + + require.Nil(t, netA.listener) + _, connected := netA.Address() + require.False(t, connected) +} + func waitReady(t testing.TB, wn *WebsocketNetwork, timeout <-chan time.Time) bool { select { case <-wn.Ready(): @@ -341,6 +354,7 @@ func setupWebsocketNetworkABwithLogger(t *testing.T, countTarget int, log loggin readyTimeout := time.NewTimer(5 * time.Second) waitReady(t, netA, readyTimeout.C) t.Log("a ready") + readyTimeout.Reset(5 * time.Second) waitReady(t, netB, readyTimeout.C) t.Log("b ready") @@ -4735,7 +4749,7 @@ func TestWebsocketNetworkHTTPClient(t *testing.T) { require.Equal(t, http.StatusPreconditionFailed, resp.StatusCode) // not enough ws peer headers _, err = netB.GetHTTPClient("invalid") - require.Error(t, err) + require.ErrorContains(t, err, `could not parse a host from url`) } // TestPeerComparisonInBroadcast tests that the peer comparison in the broadcast function works as expected diff --git a/network/wsPeer_test.go b/network/wsPeer_test.go index d3e3cd3fdb..04e7ff91d7 100644 --- a/network/wsPeer_test.go +++ b/network/wsPeer_test.go @@ -27,16 +27,18 @@ import ( "path/filepath" "slices" "sort" + "strconv" "strings" "sync/atomic" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/metrics" - "github.com/stretchr/testify/require" ) func TestCheckSlowWritingPeer(t *testing.T) { @@ -133,17 +135,18 @@ func TestVersionToMajorMinor(t *testing.T) { require.Equal(t, int64(2), mi) ma, mi, err = versionToMajorMinor("1.2.3") - require.Error(t, err) + require.ErrorContains(t, err, `version 1.2.3 does not have two components`) require.Zero(t, ma) require.Zero(t, mi) ma, mi, err = versionToMajorMinor("1") - require.Error(t, err) + require.ErrorContains(t, err, `version 1 does not have two components`) require.Zero(t, ma) require.Zero(t, mi) ma, mi, err = versionToMajorMinor("a.b") - require.Error(t, err) + var numErr *strconv.NumError + require.ErrorAs(t, err, &numErr) require.Zero(t, ma) require.Zero(t, mi) } diff --git a/node/netprio_test.go b/node/netprio_test.go index 1073c88f75..c4e99f5dc8 100644 --- a/node/netprio_test.go +++ b/node/netprio_test.go @@ -20,9 +20,10 @@ import ( "encoding/base64" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TestBase64AllocboundSize tests that the base64 encoded size of the Nonce is the same as the allocbound diff --git a/node/node.go b/node/node.go index 23aae5cd06..200322dfe9 100644 --- a/node/node.go +++ b/node/node.go @@ -1247,7 +1247,7 @@ func (node *AlgorandFullNode) AbortCatchup(catchpoint string) error { // channel which contains the updated node context. This function need to work asynchronously so that the caller could // detect and handle the use case where the node is being shut down while we're switching to/from catchup mode without // deadlocking on the shared node mutex. -func (node *AlgorandFullNode) SetCatchpointCatchupMode(catchpointCatchupMode bool) (outCtxCh <-chan context.Context) { +func (node *AlgorandFullNode) SetCatchpointCatchupMode(enable bool) (outCtxCh <-chan context.Context) { // create a non-buffered channel to return the newly created context. The fact that it's non-buffered here // is important, as it allows us to synchronize the "receiving" of the new context before canceling of the previous // one. @@ -1262,7 +1262,7 @@ func (node *AlgorandFullNode) SetCatchpointCatchupMode(catchpointCatchupMode boo node.mu.Unlock() return } - if catchpointCatchupMode { + if enable { // stop.. defer func() { node.mu.Unlock() diff --git a/node/node_test.go b/node/node_test.go index 7026b363e4..2450632506 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -1181,6 +1181,10 @@ func TestNodeSetCatchpointCatchupMode(t *testing.T) { // "start" catchpoint catchup => close services outCh := n.SetCatchpointCatchupMode(true) <-outCh + // make sure SetCatchpointCatchupMode' goroutine has completely finished + // to prevent data race on stop/start services + n.waitMonitoringRoutines() + // "stop" catchpoint catchup => resume services outCh = n.SetCatchpointCatchupMode(false) <-outCh diff --git a/nodecontrol/NodeController.go b/nodecontrol/NodeController.go index 957c9dc39d..1c8ba7f508 100644 --- a/nodecontrol/NodeController.go +++ b/nodecontrol/NodeController.go @@ -17,7 +17,11 @@ package nodecontrol import ( + "context" + "fmt" + "os" "path/filepath" + "strings" "syscall" "time" @@ -111,7 +115,7 @@ func (nc NodeController) stopProcesses() (kmdAlreadyStopped bool, err error) { return } -func killPID(pid int) (killed bool, err error) { +func killPID(pid int, beforeKill func()) (killed bool, err error) { process, err := util.FindProcess(pid) if process == nil || err != nil { return false, err @@ -130,8 +134,42 @@ func killPID(pid int) (killed bool, err error) { } select { case <-waitLong: + if beforeKill != nil { + beforeKill() + } return true, util.KillProcess(pid, syscall.SIGKILL) case <-time.After(time.Millisecond * 100): } } } + +// collectGoroutineStacks fetches goroutine stacks from the node's pprof endpoint +// and saves them to a file in the data directory. +func (nc *NodeController) collectGoroutineStacks() { + algodClient, err := nc.AlgodClient() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create algod client for goroutine dump: %v\n", err) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + goRoutines, err := algodClient.GetGoRoutines(ctx) + if err != nil { + msg := err.Error() + if strings.Contains(msg, "404 Not Found") { + msg = "pprof most likely disabled, consider setting EnableProfiler=true for further debugging" + } + fmt.Fprintf(os.Stderr, "cannot fetch goroutine stacks: %s\n", msg) + return + } + + dumpFile := filepath.Join(nc.algodDataDir, fmt.Sprintf("goroutine-dump-%s.txt", time.Now().Format("20060102-150405"))) + err = os.WriteFile(dumpFile, []byte(goRoutines), 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to write goroutine dump to %s: %v\n", dumpFile, err) + return + } + fmt.Fprintf(os.Stderr, "goroutine dump saved to %s\n", dumpFile) +} diff --git a/nodecontrol/algodControl.go b/nodecontrol/algodControl.go index e5f3ef06bf..625a4b763e 100644 --- a/nodecontrol/algodControl.go +++ b/nodecontrol/algodControl.go @@ -175,7 +175,7 @@ func (nc *NodeController) StopAlgod() (err error) { algodPID, err := nc.GetAlgodPID() if err == nil { // Kill algod by PID - killed, killErr := killPID(int(algodPID)) + killed, killErr := killPID(int(algodPID), nc.collectGoroutineStacks) if killErr != nil { return killErr } diff --git a/nodecontrol/algodControl_test.go b/nodecontrol/algodControl_test.go index 1cd5849830..92268d50d5 100644 --- a/nodecontrol/algodControl_test.go +++ b/nodecontrol/algodControl_test.go @@ -20,8 +20,9 @@ import ( "errors" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestStopAlgodErrorNotRunning(t *testing.T) { diff --git a/nodecontrol/kmdControl.go b/nodecontrol/kmdControl.go index 2acdd78e80..17c4b0f3f6 100644 --- a/nodecontrol/kmdControl.go +++ b/nodecontrol/kmdControl.go @@ -121,7 +121,7 @@ func (kc *KMDController) StopKMD() (alreadyStopped bool, err error) { kmdPID, err := kc.GetKMDPID() if err == nil { // Kill kmd by PID - killed, killErr := killPID(int(kmdPID)) + killed, killErr := killPID(int(kmdPID), nil) if killErr != nil { return false, killErr } diff --git a/protocol/codec_test.go b/protocol/codec_test.go index 91a83932ab..12c89e2564 100644 --- a/protocol/codec_test.go +++ b/protocol/codec_test.go @@ -23,9 +23,11 @@ import ( "reflect" "testing" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/algorand/go-codec/codec" "github.com/stretchr/testify/require" + + "github.com/algorand/go-codec/codec" + + "github.com/algorand/go-algorand/test/partitiontest" ) type TestArray [4]uint64 diff --git a/protocol/codec_tester.go b/protocol/codec_tester.go index 27654bb120..06e62b8151 100644 --- a/protocol/codec_tester.go +++ b/protocol/codec_tester.go @@ -27,12 +27,12 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" "github.com/algorand/go-deadlock" - "github.com/algorand/msgp/msgp" - "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) const debugCodecTester = false diff --git a/protocol/hash_test.go b/protocol/hash_test.go index 459a0160e9..a30b46f477 100644 --- a/protocol/hash_test.go +++ b/protocol/hash_test.go @@ -20,8 +20,9 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" + + "github.com/algorand/go-algorand/test/partitiontest" ) // TestHashIDPrefix checks if any HashID const declared in hash.go is a prefix of another. diff --git a/protocol/tags_test.go b/protocol/tags_test.go index e1a42bb85a..cab4ba1b13 100644 --- a/protocol/tags_test.go +++ b/protocol/tags_test.go @@ -24,8 +24,9 @@ import ( "strconv" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) // getConstValues uses the AST to get a list of the values of declared const diff --git a/rpcs/blockService.go b/rpcs/blockService.go index 97f0a43af7..29a35c3560 100644 --- a/rpcs/blockService.go +++ b/rpcs/blockService.go @@ -31,7 +31,6 @@ import ( "github.com/gorilla/mux" "github.com/algorand/go-codec/codec" - "github.com/algorand/go-deadlock" "github.com/algorand/go-algorand/agreement" diff --git a/rpcs/healthService_test.go b/rpcs/healthService_test.go index 3b5284c5dd..f42ce3df0a 100644 --- a/rpcs/healthService_test.go +++ b/rpcs/healthService_test.go @@ -22,10 +22,11 @@ import ( "path" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestHealthService_ServeHTTP(t *testing.T) { diff --git a/shared/pingpong/accounts.go b/shared/pingpong/accounts.go index 39ca5ead00..648894c6f0 100644 --- a/shared/pingpong/accounts.go +++ b/shared/pingpong/accounts.go @@ -254,7 +254,9 @@ func (pps *WorkerState) integrateAccountInfo(addr string, ppa *pingPongAccount, for _, ap := range *ai.CreatedAssets { assetID := ap.Index pps.cinfo.OptIns[assetID] = uniqueAppend(pps.cinfo.OptIns[assetID], addr) - pps.cinfo.AssetParams[assetID] = ap.Params + if ap.Params != nil { + pps.cinfo.AssetParams[assetID] = *ap.Params + } } } // assets held @@ -273,7 +275,9 @@ func (pps *WorkerState) integrateAccountInfo(addr string, ppa *pingPongAccount, for _, ap := range *ai.CreatedApps { appID := ap.Id pps.cinfo.OptIns[appID] = uniqueAppend(pps.cinfo.OptIns[appID], addr) - pps.cinfo.AppParams[appID] = ap.Params + if ap.Params != nil { + pps.cinfo.AppParams[appID] = *ap.Params + } } } // apps opted into @@ -436,7 +440,9 @@ func (pps *WorkerState) makeNewAssets(client *libgoal.Client) (err error) { pps.cinfo.OptIns[assetID] = uniqueAppend(pps.cinfo.OptIns[assetID], addr) _, has := pps.cinfo.AssetParams[assetID] if !has { - newAssets[assetID] = ap.Params + if ap.Params != nil { + newAssets[assetID] = *ap.Params + } } } } @@ -807,7 +813,9 @@ func (pps *WorkerState) prepareApps(client *libgoal.Client) (err error) { for _, ap := range *ai.CreatedApps { appID := ap.Id pps.cinfo.OptIns[appID] = uniqueAppend(pps.cinfo.OptIns[appID], addr) - pps.cinfo.AppParams[appID] = ap.Params + if ap.Params != nil { + pps.cinfo.AppParams[appID] = *ap.Params + } } } diff --git a/shared/pingpong/accounts_test.go b/shared/pingpong/accounts_test.go index c35c032aec..c243c8c207 100644 --- a/shared/pingpong/accounts_test.go +++ b/shared/pingpong/accounts_test.go @@ -20,11 +20,11 @@ import ( "encoding/binary" "testing" + "github.com/stretchr/testify/assert" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/assert" ) func makeKeyFromSeed(i uint64) *crypto.SignatureSecrets { diff --git a/stateproof/verify/stateproof_test.go b/stateproof/verify/stateproof_test.go index 07e3d9d79c..832f8fd903 100644 --- a/stateproof/verify/stateproof_test.go +++ b/stateproof/verify/stateproof_test.go @@ -17,6 +17,10 @@ package verify import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto/stateproof" "github.com/algorand/go-algorand/data/basics" @@ -26,8 +30,6 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" - "testing" ) func invokeValidateStateProof(latestRoundInIntervalHdr *bookkeeping.BlockHeader, diff --git a/stateproof/worker_test.go b/stateproof/worker_test.go index de1de63c91..7a08c71fdc 100644 --- a/stateproof/worker_test.go +++ b/stateproof/worker_test.go @@ -30,6 +30,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklearray" @@ -46,7 +48,6 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/db" - "github.com/algorand/go-deadlock" ) type testWorkerStubs struct { diff --git a/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp b/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp index 7ba924f99d..9d0f3a8d8b 100644 --- a/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp +++ b/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp @@ -180,7 +180,7 @@ if { [catch { ::AlgorandGoal::StartNode $TEST_ROOT_DIR/Node False $WEBPROXY_LISTEN_ADDRESS # once the node is started we can clear the ::GLOBAL_TEST_ALGO_DIR, so that shutdown would be done as a network. - set ::GLOBAL_TEST_ALGO_DIR "" + unset ::GLOBAL_TEST_ALGO_DIR ::AlgorandGoal::WaitForRound $CATCHPOINT_ROUND $TEST_ROOT_DIR/Node diff --git a/test/e2e-go/cli/tealdbg/cdtmock/main.go b/test/e2e-go/cli/tealdbg/cdtmock/main.go index 77a889661d..261b88e13a 100644 --- a/test/e2e-go/cli/tealdbg/cdtmock/main.go +++ b/test/e2e-go/cli/tealdbg/cdtmock/main.go @@ -18,7 +18,6 @@ package main import ( "fmt" - "net/http" "os" "strings" diff --git a/test/e2e-go/features/accountPerf/sixMillion_test.go b/test/e2e-go/features/accountPerf/sixMillion_test.go index 1ed8e1cb4e..03989a987a 100644 --- a/test/e2e-go/features/accountPerf/sixMillion_test.go +++ b/test/e2e-go/features/accountPerf/sixMillion_test.go @@ -29,9 +29,10 @@ import ( "testing" "time" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" clientApi "github.com/algorand/go-algorand/daemon/algod/api/client" @@ -966,7 +967,7 @@ func scenarioD( } pass := checkApplicationParams( appCallFields[(*app.Params.GlobalState)[0].Value.Uint], - app.Params, + *app.Params, baseAcct.pk.String(), &globalStateCheck, &globalStateCheckMu) diff --git a/test/e2e-go/features/incentives/suspension_test.go b/test/e2e-go/features/incentives/suspension_test.go index 965b837c98..cef9ef432d 100644 --- a/test/e2e-go/features/incentives/suspension_test.go +++ b/test/e2e-go/features/incentives/suspension_test.go @@ -22,9 +22,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/config" "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/libgoal" diff --git a/test/e2e-go/features/incentives/whalejoin_test.go b/test/e2e-go/features/incentives/whalejoin_test.go index 082b803f83..bad75c7536 100644 --- a/test/e2e-go/features/incentives/whalejoin_test.go +++ b/test/e2e-go/features/incentives/whalejoin_test.go @@ -21,9 +21,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/config" "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" "github.com/algorand/go-algorand/data/basics" diff --git a/test/e2e-go/features/multisig/multisig_test.go b/test/e2e-go/features/multisig/multisig_test.go index 87d94adebb..5ffe729f7c 100644 --- a/test/e2e-go/features/multisig/multisig_test.go +++ b/test/e2e-go/features/multisig/multisig_test.go @@ -20,11 +20,12 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // create a 2 out of 3 multisig address diff --git a/test/e2e-go/features/p2p/p2p_basic_test.go b/test/e2e-go/features/p2p/p2p_basic_test.go index 2f5614ee5d..24de2254f6 100644 --- a/test/e2e-go/features/p2p/p2p_basic_test.go +++ b/test/e2e-go/features/p2p/p2p_basic_test.go @@ -22,11 +22,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func testP2PWithConfig(t *testing.T, templateName string) *fixtures.RestClientFixture { diff --git a/test/e2e-go/features/privatenet/privatenet_test.go b/test/e2e-go/features/privatenet/privatenet_test.go index a5bbe3a542..13b71eebc4 100644 --- a/test/e2e-go/features/privatenet/privatenet_test.go +++ b/test/e2e-go/features/privatenet/privatenet_test.go @@ -20,9 +20,10 @@ package privatenet import ( "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TestPrivateNetworkImportKeys tests that part keys can be exported and diff --git a/test/e2e-go/features/transactions/asset_test.go b/test/e2e-go/features/transactions/asset_test.go index 0f193a3916..ceba57c879 100644 --- a/test/e2e-go/features/transactions/asset_test.go +++ b/test/e2e-go/features/transactions/asset_test.go @@ -291,7 +291,10 @@ func TestAssetConfig(t *testing.T) { for _, asset := range *info.CreatedAssets { idx := asset.Index cp := asset.Params - assets = append(assets, assetIDParams{idx, cp}) + if cp == nil { + continue + } + assets = append(assets, assetIDParams{idx, *cp}) a.Equal(derefString(cp.UnitName), fmt.Sprintf("test%d", cp.Total-1)) a.Equal(derefString(cp.Name), fmt.Sprintf("testname%d", cp.Total-1)) a.Equal(derefString(cp.Manager), manager) @@ -978,7 +981,9 @@ func TestAssetCreateWaitRestartDelete(t *testing.T) { var asset model.AssetParams var assetIndex basics.AssetIndex for _, cp := range *info.CreatedAssets { - asset = cp.Params + if cp.Params != nil { + asset = *cp.Params + } assetIndex = cp.Index } @@ -1000,7 +1005,9 @@ func TestAssetCreateWaitRestartDelete(t *testing.T) { a.NotNil(info.CreatedAssets) a.Equal(len(*info.CreatedAssets), 1) for _, cp := range *info.CreatedAssets { - asset = cp.Params + if cp.Params != nil { + asset = *cp.Params + } assetIndex = cp.Index } verifyAssetParameters(asset, "test", "testunit", manager, reserve, freeze, clawback, @@ -1079,7 +1086,9 @@ func TestAssetCreateWaitBalLookbackDelete(t *testing.T) { var asset model.AssetParams var assetIndex basics.AssetIndex for _, cp := range *info.CreatedAssets { - asset = cp.Params + if cp.Params != nil { + asset = *cp.Params + } assetIndex = cp.Index } @@ -1103,7 +1112,9 @@ func TestAssetCreateWaitBalLookbackDelete(t *testing.T) { a.NotNil(info.CreatedAssets) a.Equal(len(*info.CreatedAssets), 1) for _, cp := range *info.CreatedAssets { - asset = cp.Params + if cp.Params != nil { + asset = *cp.Params + } assetIndex = cp.Index } verifyAssetParameters(asset, "test", "testunit", manager, reserve, freeze, clawback, diff --git a/test/e2e-go/features/transactions/logicsig_test.go b/test/e2e-go/features/transactions/logicsig_test.go index 4a03e928d4..fd6ac53b17 100644 --- a/test/e2e-go/features/transactions/logicsig_test.go +++ b/test/e2e-go/features/transactions/logicsig_test.go @@ -20,11 +20,12 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func TestLogicSigSizeBeforePooling(t *testing.T) { diff --git a/test/e2e-go/features/transactions/proof_test.go b/test/e2e-go/features/transactions/proof_test.go index ad95f8ef35..ff8a86c358 100644 --- a/test/e2e-go/features/transactions/proof_test.go +++ b/test/e2e-go/features/transactions/proof_test.go @@ -20,13 +20,14 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklearray" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TxnMerkleElemRaw this struct helps creates a hashable struct from the bytes diff --git a/test/e2e-go/features/transactions/sendReceive_test.go b/test/e2e-go/features/transactions/sendReceive_test.go index 7c38c12a88..d6e51d4715 100644 --- a/test/e2e-go/features/transactions/sendReceive_test.go +++ b/test/e2e-go/features/transactions/sendReceive_test.go @@ -24,7 +24,6 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" - "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" ) diff --git a/test/e2e-go/restAPI/helpers.go b/test/e2e-go/restAPI/helpers.go index 7dec672cf2..e143be2c24 100644 --- a/test/e2e-go/restAPI/helpers.go +++ b/test/e2e-go/restAPI/helpers.go @@ -22,10 +22,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" "github.com/algorand/go-algorand/libgoal" "github.com/algorand/go-algorand/test/framework/fixtures" - "github.com/stretchr/testify/require" ) // helper generates a random Uppercase Alphabetic ASCII char diff --git a/test/e2e-go/restAPI/other/appsRestAPI_test.go b/test/e2e-go/restAPI/other/appsRestAPI_test.go index 7cb209d9ee..cd9077a87d 100644 --- a/test/e2e-go/restAPI/other/appsRestAPI_test.go +++ b/test/e2e-go/restAPI/other/appsRestAPI_test.go @@ -26,6 +26,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod/api/client" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" @@ -33,11 +35,9 @@ import ( "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/libgoal" + helper "github.com/algorand/go-algorand/test/e2e-go/restAPI" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" - - helper "github.com/algorand/go-algorand/test/e2e-go/restAPI" ) func TestPendingTransactionInfoInnerTxnAssetCreate(t *testing.T) { diff --git a/test/e2e-go/restAPI/other/misc_test.go b/test/e2e-go/restAPI/other/misc_test.go index aa4e72750f..6ff1e25822 100644 --- a/test/e2e-go/restAPI/other/misc_test.go +++ b/test/e2e-go/restAPI/other/misc_test.go @@ -22,12 +22,13 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/daemon/algod/api/client" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/tokens" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestDisabledAPIConfig(t *testing.T) { diff --git a/test/e2e-go/restAPI/simulate/simulateRestAPI_test.go b/test/e2e-go/restAPI/simulate/simulateRestAPI_test.go index 47903fa7b8..a752638f50 100644 --- a/test/e2e-go/restAPI/simulate/simulateRestAPI_test.go +++ b/test/e2e-go/restAPI/simulate/simulateRestAPI_test.go @@ -25,6 +25,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod/api/client" @@ -36,11 +38,9 @@ import ( "github.com/algorand/go-algorand/ledger/simulation" "github.com/algorand/go-algorand/libgoal" "github.com/algorand/go-algorand/protocol" + helper "github.com/algorand/go-algorand/test/e2e-go/restAPI" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" - - helper "github.com/algorand/go-algorand/test/e2e-go/restAPI" ) func TestSimulateTxnTracerDevMode(t *testing.T) { diff --git a/test/e2e-go/restAPI/stateproof/stateproofRestAPI_test.go b/test/e2e-go/restAPI/stateproof/stateproofRestAPI_test.go index f86230dc16..ffc1c1c0ca 100644 --- a/test/e2e-go/restAPI/stateproof/stateproofRestAPI_test.go +++ b/test/e2e-go/restAPI/stateproof/stateproofRestAPI_test.go @@ -21,6 +21,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklesignature" @@ -28,12 +30,10 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" + helper "github.com/algorand/go-algorand/test/e2e-go/restAPI" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/db" - "github.com/stretchr/testify/require" - - helper "github.com/algorand/go-algorand/test/e2e-go/restAPI" ) func TestStateProofInParticipationInfo(t *testing.T) { diff --git a/test/e2e-go/upgrades/stateproof_participation_test.go b/test/e2e-go/upgrades/stateproof_participation_test.go index 0049a0bdd8..f0ec77fd50 100644 --- a/test/e2e-go/upgrades/stateproof_participation_test.go +++ b/test/e2e-go/upgrades/stateproof_participation_test.go @@ -21,6 +21,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" @@ -28,7 +30,6 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/framework/fixtures" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) func waitUntilProtocolUpgrades(a *require.Assertions, fixture *fixtures.RestClientFixture, nodeClient libgoal.Client) { diff --git a/test/framework/fixtures/expectFixture.go b/test/framework/fixtures/expectFixture.go index 218d82ca86..8a7f946d83 100644 --- a/test/framework/fixtures/expectFixture.go +++ b/test/framework/fixtures/expectFixture.go @@ -26,8 +26,9 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) // ExpectFixture is a wrapper for running expect tests diff --git a/test/framework/fixtures/fixture.go b/test/framework/fixtures/fixture.go index 025719a7f9..87d67526bb 100644 --- a/test/framework/fixtures/fixture.go +++ b/test/framework/fixtures/fixture.go @@ -19,8 +19,9 @@ package fixtures import ( "testing" - "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/protocol" ) // TestingTB is identical to testing.TB, beside the private method. diff --git a/test/framework/fixtures/libgoalFixture.go b/test/framework/fixtures/libgoalFixture.go index 38d3b3ec4d..d0aec4625d 100644 --- a/test/framework/fixtures/libgoalFixture.go +++ b/test/framework/fixtures/libgoalFixture.go @@ -28,9 +28,10 @@ import ( "testing" "time" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/crypto/merklearray" diff --git a/test/framework/fixtures/restClientFixture.go b/test/framework/fixtures/restClientFixture.go index 01ac30c8f6..8c24642f01 100644 --- a/test/framework/fixtures/restClientFixture.go +++ b/test/framework/fixtures/restClientFixture.go @@ -24,15 +24,13 @@ import ( "github.com/stretchr/testify/require" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/data/bookkeeping" - "github.com/algorand/go-algorand/netdeploy" - "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" - + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/libgoal" + "github.com/algorand/go-algorand/netdeploy" "github.com/algorand/go-algorand/nodecontrol" "github.com/algorand/go-algorand/util/tokens" ) diff --git a/tools/block-generator/core/commands.go b/tools/block-generator/core/commands.go index a67fc80e95..a3be48c9d1 100644 --- a/tools/block-generator/core/commands.go +++ b/tools/block-generator/core/commands.go @@ -17,9 +17,10 @@ package core import ( + "github.com/spf13/cobra" + "github.com/algorand/go-algorand/tools/block-generator/generator" "github.com/algorand/go-algorand/tools/block-generator/runner" - "github.com/spf13/cobra" ) // BlockGenerator related cobra commands, ready to be executed or included as subcommands. diff --git a/tools/block-generator/generator/config_test.go b/tools/block-generator/generator/config_test.go index dd8ba9919a..4c71df5971 100644 --- a/tools/block-generator/generator/config_test.go +++ b/tools/block-generator/generator/config_test.go @@ -21,8 +21,9 @@ import ( "os" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestInitConfigFile(t *testing.T) { diff --git a/tools/block-generator/generator/generate.go b/tools/block-generator/generator/generate.go index 42665efd11..3fda9f5ffc 100644 --- a/tools/block-generator/generator/generate.go +++ b/tools/block-generator/generator/generate.go @@ -411,7 +411,7 @@ func (g *generator) WriteAccount(output io.Writer, accountString string) error { nameBytes := []byte(a.name) asset := model.Asset{ Index: a.assetID, - Params: model.AssetParams{ + Params: &model.AssetParams{ Creator: accountString, Decimals: 0, Clawback: &accountString, diff --git a/tools/block-generator/generator/generator_ledger.go b/tools/block-generator/generator/generator_ledger.go index b9e08f9554..14b7edc4ad 100644 --- a/tools/block-generator/generator/generator_ledger.go +++ b/tools/block-generator/generator/generator_ledger.go @@ -25,6 +25,7 @@ import ( "time" "github.com/algorand/avm-abi/apps" + cconfig "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" diff --git a/tools/block-generator/generator/server_test.go b/tools/block-generator/generator/server_test.go index 2c318e0e96..ec1eb1dd8c 100644 --- a/tools/block-generator/generator/server_test.go +++ b/tools/block-generator/generator/server_test.go @@ -21,9 +21,10 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestParseURL(t *testing.T) { diff --git a/tools/block-generator/generator/utils.go b/tools/block-generator/generator/utils.go index e40365a47c..01932a1772 100644 --- a/tools/block-generator/generator/utils.go +++ b/tools/block-generator/generator/utils.go @@ -21,8 +21,9 @@ import ( "fmt" "math/rand" - "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-codec/codec" + + "github.com/algorand/go-algorand/data/basics" ) func weightedSelection(weights []float32, options []interface{}, defaultOption interface{}) (selection interface{}, err error) { diff --git a/tools/block-generator/generator/utils_test.go b/tools/block-generator/generator/utils_test.go index 73148fcb4b..14be5beffd 100644 --- a/tools/block-generator/generator/utils_test.go +++ b/tools/block-generator/generator/utils_test.go @@ -20,9 +20,10 @@ import ( "fmt" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestWeightedSelectionInternalBadInput(t *testing.T) { diff --git a/tools/block-generator/runner/run.go b/tools/block-generator/runner/run.go index 529629a386..0176b0e1bd 100644 --- a/tools/block-generator/runner/run.go +++ b/tools/block-generator/runner/run.go @@ -19,18 +19,16 @@ package runner import ( "bytes" "context" + _ "embed" // embed conduit template config file "encoding/json" - "io" - "sort" - - // embed conduit template config file - _ "embed" "fmt" + "io" "net/http" "os" "os/exec" "path" "path/filepath" + "sort" "strconv" "strings" "text/template" diff --git a/tools/block-generator/runner/runner.go b/tools/block-generator/runner/runner.go index 06252006a9..e1d2d29f0a 100644 --- a/tools/block-generator/runner/runner.go +++ b/tools/block-generator/runner/runner.go @@ -35,7 +35,7 @@ func init() { Use: "runner", Short: "Run test suite and collect results.", Long: "Run an automated test suite using the block-generator daemon and a provided conduit binary. Results are captured to a specified output directory.", - RunE: func(cmd *cobra.Command, args []string) error{ + RunE: func(cmd *cobra.Command, args []string) error { fmt.Printf("starting block-generator runner with args: %+v\n", runnerArgs) if runnerArgs.Template == "postgres-exporter" && runnerArgs.PostgresConnectionString == "" { diff --git a/tools/block-generator/util/util.go b/tools/block-generator/util/util.go index c447730194..3f909afdf6 100644 --- a/tools/block-generator/util/util.go +++ b/tools/block-generator/util/util.go @@ -24,10 +24,10 @@ import ( "os" "strings" - "github.com/algorand/go-algorand/data/basics" - // import postgres driver _ "github.com/lib/pq" + + "github.com/algorand/go-algorand/data/basics" ) // ErrorNotInitialized is returned when the database is not initialized. diff --git a/tools/debug/algodump/compress.go b/tools/debug/algodump/compress.go index a0aed9566a..3fbebc7b90 100644 --- a/tools/debug/algodump/compress.go +++ b/tools/debug/algodump/compress.go @@ -31,15 +31,17 @@ import ( "time" "github.com/DataDog/zstd" + kzstd "github.com/klauspost/compress/zstd" + "github.com/valyala/gozstd" + + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/network/vpack" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-deadlock" - kzstd "github.com/klauspost/compress/zstd" - "github.com/valyala/gozstd" ) // Compression-specific flags diff --git a/tools/debug/algodump/compress_bench_test.go b/tools/debug/algodump/compress_bench_test.go index f1ce7c5ece..858d12303b 100644 --- a/tools/debug/algodump/compress_bench_test.go +++ b/tools/debug/algodump/compress_bench_test.go @@ -32,10 +32,11 @@ import ( "testing" "github.com/DataDog/zstd" - "github.com/algorand/go-algorand/network/vpack" - "github.com/algorand/go-algorand/protocol" kzstd "github.com/klauspost/compress/zstd" "github.com/valyala/gozstd" + + "github.com/algorand/go-algorand/network/vpack" + "github.com/algorand/go-algorand/protocol" ) // testCorpus holds all the test data loaded from message files diff --git a/tools/debug/logfilter/main_test.go b/tools/debug/logfilter/main_test.go index 416351e07c..888fd2d620 100644 --- a/tools/debug/logfilter/main_test.go +++ b/tools/debug/logfilter/main_test.go @@ -24,8 +24,9 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestLogFilterExamples(t *testing.T) { diff --git a/tools/debug/transplanter/main.go b/tools/debug/transplanter/main.go index 76782d262c..e6ec587e56 100644 --- a/tools/debug/transplanter/main.go +++ b/tools/debug/transplanter/main.go @@ -34,6 +34,8 @@ import ( "github.com/golang/snappy" _ "github.com/mattn/go-sqlite3" + "github.com/algorand/go-codec/codec" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/config/bounds" @@ -46,7 +48,6 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/node" "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-codec/codec" ) var dataDir = flag.String("d", "", "Data directory to track to get files from") diff --git a/tools/network/bootstrap_test.go b/tools/network/bootstrap_test.go index dee9607a7e..c027bb9aff 100644 --- a/tools/network/bootstrap_test.go +++ b/tools/network/bootstrap_test.go @@ -20,8 +20,9 @@ import ( "context" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestReadFromSRVPriority(t *testing.T) { diff --git a/tools/network/dnssec/anchor_test.go b/tools/network/dnssec/anchor_test.go index 03b01a20a0..60ea899064 100644 --- a/tools/network/dnssec/anchor_test.go +++ b/tools/network/dnssec/anchor_test.go @@ -19,8 +19,9 @@ package dnssec import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestParseRootTrustAnchor(t *testing.T) { diff --git a/tools/network/dnssec/client_test.go b/tools/network/dnssec/client_test.go index 0a785ba06b..3dee975126 100644 --- a/tools/network/dnssec/client_test.go +++ b/tools/network/dnssec/client_test.go @@ -21,9 +21,10 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/miekg/dns" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestEmptyClient(t *testing.T) { diff --git a/tools/network/dnssec/config_test.go b/tools/network/dnssec/config_test.go index f18886a3cb..c97060a458 100644 --- a/tools/network/dnssec/config_test.go +++ b/tools/network/dnssec/config_test.go @@ -20,8 +20,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestConfigSystem(t *testing.T) { diff --git a/tools/network/dnssec/config_unix_test.go b/tools/network/dnssec/config_unix_test.go index 5481766e4f..9c5960c913 100644 --- a/tools/network/dnssec/config_unix_test.go +++ b/tools/network/dnssec/config_unix_test.go @@ -23,8 +23,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestConfigEmpty(t *testing.T) { diff --git a/tools/network/dnssec/dnssec_test.go b/tools/network/dnssec/dnssec_test.go index bd5b913603..d99fb0e93b 100644 --- a/tools/network/dnssec/dnssec_test.go +++ b/tools/network/dnssec/dnssec_test.go @@ -22,10 +22,11 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/logging" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/miekg/dns" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/test/partitiontest" ) func TestLookup(t *testing.T) { diff --git a/tools/network/dnssec/sort_test.go b/tools/network/dnssec/sort_test.go index a3ef3bfee8..8938d75670 100644 --- a/tools/network/dnssec/sort_test.go +++ b/tools/network/dnssec/sort_test.go @@ -20,8 +20,9 @@ import ( "net" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestSrvSort(t *testing.T) { diff --git a/tools/network/dnssec/trustchain.go b/tools/network/dnssec/trustchain.go index 8daa1a933c..e952a1c5a1 100644 --- a/tools/network/dnssec/trustchain.go +++ b/tools/network/dnssec/trustchain.go @@ -22,8 +22,9 @@ import ( "strings" "time" - "github.com/algorand/go-deadlock" "github.com/miekg/dns" + + "github.com/algorand/go-deadlock" ) // TrustQuerier wraps Querier and trusted root anchor retrieval for better testability diff --git a/tools/network/dnssec/trustedchain_test.go b/tools/network/dnssec/trustedchain_test.go index 4d7e1b7dfe..3c1987883e 100644 --- a/tools/network/dnssec/trustedchain_test.go +++ b/tools/network/dnssec/trustedchain_test.go @@ -22,9 +22,10 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/miekg/dns" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestTrustChainBasic(t *testing.T) { diff --git a/tools/network/dnssec/trustedzone_test.go b/tools/network/dnssec/trustedzone_test.go index 9e78fa512c..c47bb1e80b 100644 --- a/tools/network/dnssec/trustedzone_test.go +++ b/tools/network/dnssec/trustedzone_test.go @@ -21,9 +21,10 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/miekg/dns" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestTrustedZone(t *testing.T) { diff --git a/tools/network/dnssec/util_test.go b/tools/network/dnssec/util_test.go index 3c14cc993e..2f0e688cef 100644 --- a/tools/network/dnssec/util_test.go +++ b/tools/network/dnssec/util_test.go @@ -19,8 +19,9 @@ package dnssec import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestSplitZone(t *testing.T) { diff --git a/tools/x-repo-types/typeAnalyzer/main.go b/tools/x-repo-types/typeAnalyzer/main.go index a2eec918e1..92bb39a462 100644 --- a/tools/x-repo-types/typeAnalyzer/main.go +++ b/tools/x-repo-types/typeAnalyzer/main.go @@ -24,9 +24,8 @@ package main import ( "fmt" - "os" - xpkg "net/http" + "os" ypkg "time" ) diff --git a/tools/x-repo-types/typeAnalyzer/typeAnalyzer_test.go b/tools/x-repo-types/typeAnalyzer/typeAnalyzer_test.go index c51f2709f3..238ffa4c40 100644 --- a/tools/x-repo-types/typeAnalyzer/typeAnalyzer_test.go +++ b/tools/x-repo-types/typeAnalyzer/typeAnalyzer_test.go @@ -20,8 +20,9 @@ import ( "fmt" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestEdgeFromLabel(t *testing.T) { diff --git a/tools/x-repo-types/xrt.go b/tools/x-repo-types/xrt.go index 51b5577afa..1ea4f8bdb2 100644 --- a/tools/x-repo-types/xrt.go +++ b/tools/x-repo-types/xrt.go @@ -18,6 +18,7 @@ package main import ( "bytes" + _ "embed" "errors" "fmt" "log" @@ -28,8 +29,6 @@ import ( "text/template" "github.com/spf13/cobra" - - _ "embed" ) //go:embed typeAnalyzer/main.tmpl diff --git a/tools/x-repo-types/xrt_test.go b/tools/x-repo-types/xrt_test.go index 19a3c74c7b..2d28fbb311 100644 --- a/tools/x-repo-types/xrt_test.go +++ b/tools/x-repo-types/xrt_test.go @@ -19,8 +19,9 @@ package main import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) type testCase struct { diff --git a/util/condvar/timedwait_test.go b/util/condvar/timedwait_test.go index e0d9393fa5..32be7c254d 100644 --- a/util/condvar/timedwait_test.go +++ b/util/condvar/timedwait_test.go @@ -23,8 +23,9 @@ import ( "github.com/stretchr/testify/require" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestTimedWaitSignal(t *testing.T) { diff --git a/util/db/initialize_test.go b/util/db/initialize_test.go index c368aef57d..61307621ff 100644 --- a/util/db/initialize_test.go +++ b/util/db/initialize_test.go @@ -22,10 +22,10 @@ import ( "errors" "testing" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) // A few migrations functions to mix and match in tests. diff --git a/util/db/versioning_test.go b/util/db/versioning_test.go index aa3670fdab..a8baee5f7f 100644 --- a/util/db/versioning_test.go +++ b/util/db/versioning_test.go @@ -22,8 +22,9 @@ import ( "os" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func testVersioning(t *testing.T, inMemory bool) { diff --git a/util/execpool/stream_test.go b/util/execpool/stream_test.go index 3c2c1cfb7f..00a7d35673 100644 --- a/util/execpool/stream_test.go +++ b/util/execpool/stream_test.go @@ -23,8 +23,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) // implements BatchProcessor interface for testing purposes @@ -121,8 +122,8 @@ func testStreamToBatchCore(wg *sync.WaitGroup, mockJobs <-chan *mockJob, done <- sv.WaitForStop() } -// TestStreamToBatchBasic tests the basic functionality -func TestStreamToBatchBasic(t *testing.T) { +// TestStream_ToBatchBasic tests the basic functionality +func TestStream_ToBatchBasic(t *testing.T) { partitiontest.PartitionTest(t) numJobs := 400 @@ -196,8 +197,8 @@ func TestStreamToBatchBasic(t *testing.T) { } } -// TestNoInputYet let the service start and get to the timeout without any inputs -func TestNoInputYet(t *testing.T) { +// TestStream_NoInputYet let the service start and get to the timeout without any inputs +func TestStream_NoInputYet(t *testing.T) { partitiontest.PartitionTest(t) numJobs := 1 @@ -227,8 +228,8 @@ func TestNoInputYet(t *testing.T) { wg.Wait() } -// TestMutipleBatchAttempts tests the behavior when multiple batch attempts will fail and the stream blocks -func TestMutipleBatchAttempts(t *testing.T) { +// TestStream_MultipleBatchAttempts tests the behavior when multiple batch attempts will fail and the stream blocks +func TestStream_MultipleBatchAttempts(t *testing.T) { partitiontest.PartitionTest(t) mp := mockPool{ @@ -296,9 +297,9 @@ func TestMutipleBatchAttempts(t *testing.T) { sv.WaitForStop() } -// TestErrors tests all the cases where exec pool returned error is handled +// TestStream_Errors tests all the cases where exec pool returned error is handled // by ending the stream processing -func TestErrors(t *testing.T) { +func TestStream_Errors(t *testing.T) { partitiontest.PartitionTest(t) mp := mockPool{ @@ -370,9 +371,9 @@ func TestErrors(t *testing.T) { sv.WaitForStop() } -// TestPendingJobOnRestart makes sure a pending job in the exec pool is cancled -// when the Stream ctx is cancled, and a now one started with a new ctx -func TestPendingJobOnRestart(t *testing.T) { +// TestStream_PendingJobOnRestart makes sure a pending job in the exec pool is canceled +// when the Stream ctx is canceled, and a new one started with a new ctx +func TestStream_PendingJobOnRestart(t *testing.T) { partitiontest.PartitionTest(t) mp := mockPool{ @@ -428,7 +429,7 @@ func TestPendingJobOnRestart(t *testing.T) { <-mp.asyncDelay <-mp.asyncDelay - // wait for the notifiation from cleanup before checking the TestPendingJobOnRestart + // wait for the notifiation from cleanup before checking the TestStream_PendingJobOnRestart <-mbp.notify require.Error(t, mj.returnError) require.False(t, mj.processed) diff --git a/util/metrics/counter_test.go b/util/metrics/counter_test.go index 9e28a49bd1..03ec665b17 100644 --- a/util/metrics/counter_test.go +++ b/util/metrics/counter_test.go @@ -23,8 +23,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) type CounterTest struct { diff --git a/util/metrics/gauge_test.go b/util/metrics/gauge_test.go index 8410e1f101..0aed9f8b69 100644 --- a/util/metrics/gauge_test.go +++ b/util/metrics/gauge_test.go @@ -23,8 +23,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) type GaugeTest struct { diff --git a/util/metrics/metrics_test.go b/util/metrics/metrics_test.go index c382b8e8cf..431e071c87 100644 --- a/util/metrics/metrics_test.go +++ b/util/metrics/metrics_test.go @@ -25,9 +25,11 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" + + "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/test/partitiontest" ) type MetricTest struct { diff --git a/util/metrics/opencensus_test.go b/util/metrics/opencensus_test.go index 849aa5f076..6b8b794f21 100644 --- a/util/metrics/opencensus_test.go +++ b/util/metrics/opencensus_test.go @@ -23,12 +23,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" "go.opencensus.io/stats" "go.opencensus.io/stats/view" "go.opencensus.io/tag" "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" ) // TestDHTOpenCensusMetrics ensures both count and distribution stats are properly converted to our metrics diff --git a/util/metrics/prometheus_test.go b/util/metrics/prometheus_test.go index 1eded61598..203c6d6965 100644 --- a/util/metrics/prometheus_test.go +++ b/util/metrics/prometheus_test.go @@ -24,9 +24,10 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestPrometheusMetrics(t *testing.T) { diff --git a/util/metrics/registry_test.go b/util/metrics/registry_test.go index b8f91a67ed..9d4cc4fe5f 100644 --- a/util/metrics/registry_test.go +++ b/util/metrics/registry_test.go @@ -20,8 +20,9 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestWriteAdd(t *testing.T) { diff --git a/util/metrics/reporter.go b/util/metrics/reporter.go index 65423691e0..851d54918f 100644 --- a/util/metrics/reporter.go +++ b/util/metrics/reporter.go @@ -26,8 +26,6 @@ import ( "regexp" "strings" "time" - // logging imports metrics so that we can have metrics about logging, which is more important than the four Debug lines we had here logging about metrics. TODO: find a more clever cycle resolution - //"github.com/algorand/go-algorand/logging" ) const ( diff --git a/util/metrics/reporter_test.go b/util/metrics/reporter_test.go index add4e1e781..846e90ce72 100755 --- a/util/metrics/reporter_test.go +++ b/util/metrics/reporter_test.go @@ -19,8 +19,9 @@ package metrics import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestParseNodeExporterArgs(t *testing.T) { diff --git a/util/metrics/runtime_test.go b/util/metrics/runtime_test.go index 5949de2853..b04d789972 100644 --- a/util/metrics/runtime_test.go +++ b/util/metrics/runtime_test.go @@ -21,8 +21,9 @@ import ( "strings" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestRuntimeMetrics(t *testing.T) { diff --git a/util/metrics/tagcounter_test.go b/util/metrics/tagcounter_test.go index 12dec38933..9cca2e966c 100644 --- a/util/metrics/tagcounter_test.go +++ b/util/metrics/tagcounter_test.go @@ -22,8 +22,9 @@ import ( "sync" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestTagCounter(t *testing.T) { diff --git a/util/pagedqueue.go b/util/pagedqueue.go new file mode 100644 index 0000000000..51c31f387e --- /dev/null +++ b/util/pagedqueue.go @@ -0,0 +1,166 @@ +// Copyright (C) 2019-2026 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 util + +import "iter" + +// PagedQueue is a linked list of "pages" of entries. It is unsynchronized, so +// `append` must be used single threaded, or externally synchronized. Access +// with `get` can be from multiple threads, after all appends have been +// completed. These are roughly the same rules that a growing slice would have, +// but PagedQueue avoids copying to grow by using a paged approach. Random +// access would pay a penalty to access entries by following the links, but +// processing in FIFO order has negligible extra cost. +// +// The zero value is a valid empty queue; NewPagedQueue is only needed to +// pre-allocate capacity for the first page. +type PagedQueue[T any] struct { + next *PagedQueue[T] + entries []T + baseIdx int +} + +// NewPagedQueue constructs a new PagedQueue that can hold at least count items +// without allocating a new page. +func NewPagedQueue[T any](count int) *PagedQueue[T] { + count = max(count, 4) + return &PagedQueue[T]{ + entries: make([]T, 0, count), + } +} + +// Len returns the number of entries in the queue. +func (pq *PagedQueue[T]) Len() int { + if pq.next != nil { + return pq.next.Len() + } + return pq.baseIdx + len(pq.entries) +} + +// Append places v on the queue, allocating a new page if the current one is +// full, and returning the active page. +func (pq *PagedQueue[T]) Append(v T) *PagedQueue[T] { + // We are at capacity, add a page rather than allow append() to grow the + // slice and perform copies. + if len(pq.entries) == cap(pq.entries) { + if pq.entries == nil { + // We must have a zero value of PagedQueue, no need for a page, just + // allocate an initial slice. + pq.entries = make([]T, 0, 8) + } else { + pq.next = &PagedQueue[T]{ + entries: make([]T, 0, cap(pq.entries)*2), + baseIdx: pq.baseIdx + len(pq.entries), + } + pq = pq.next + } + } + pq.entries = append(pq.entries, v) + return pq +} + +// Get returns an entry at an index. Callers must have a pointer to the page +// that idx is on, or a previous page. It is most efficient to call Get with +// ascending values, constantly updating your local pointer to the returned +// PagedQueue. If idx is too low, Get() panics. If it is too high, the zero +// value is returned. The asymmetry reflects the fact that a low index is +// certainly a programmer error, a high index is a natural result of scanning +// forward. +func (pq *PagedQueue[T]) Get(idx int) (*PagedQueue[T], T) { + localIdx := idx - pq.baseIdx + if len(pq.entries) > localIdx { + return pq, pq.entries[localIdx] + } + if pq.next != nil { + return pq.next.Get(idx) + } + var zero T + return pq, zero +} + +// All returns an iterator over all entries in the queue in insertion order. +func (pq *PagedQueue[T]) All() iter.Seq[T] { + return func(yield func(T) bool) { + for page := pq; page != nil; page = page.next { + for _, t := range page.entries { + if !yield(t) { + return + } + } + } + } +} + +// All2 returns an iterator over all entries in the queue in insertion order, +// yielding the queue-wide index and value of each entry. +func (pq *PagedQueue[T]) All2() iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + for page := pq; page != nil; page = page.next { + for i, t := range page.entries { + if !yield(page.baseIdx+i, t) { + return + } + } + } + } +} + +// Ptr returns a pointer to the entry at idx, along with the page it lives on +// for efficient subsequent calls. Because PagedQueue never relocates entries, +// the pointer remains valid for the lifetime of the queue. If idx is beyond +// the end, nil is returned. If idx is too low, Ptr panics. +func (pq *PagedQueue[T]) Ptr(idx int) (*PagedQueue[T], *T) { + localIdx := idx - pq.baseIdx + if localIdx < len(pq.entries) { + return pq, &pq.entries[localIdx] + } + if pq.next != nil { + return pq.next.Ptr(idx) + } + return pq, nil +} + +// AllPtrs returns an iterator over pointers to all entries in the queue in +// insertion order. Because PagedQueue never relocates entries, the pointers +// remain valid for the lifetime of the queue. +func (pq *PagedQueue[T]) AllPtrs() iter.Seq[*T] { + return func(yield func(*T) bool) { + for page := pq; page != nil; page = page.next { + for i := range page.entries { + if !yield(&page.entries[i]) { + return + } + } + } + } +} + +// AllPtrs2 returns an iterator over all entries in the queue in insertion +// order, yielding the queue-wide index and a pointer to each entry. Because +// PagedQueue never relocates entries, the pointers remain valid for the +// lifetime of the queue. +func (pq *PagedQueue[T]) AllPtrs2() iter.Seq2[int, *T] { + return func(yield func(int, *T) bool) { + for page := pq; page != nil; page = page.next { + for i := range page.entries { + if !yield(page.baseIdx+i, &page.entries[i]) { + return + } + } + } + } +} diff --git a/util/pagedqueue_test.go b/util/pagedqueue_test.go new file mode 100644 index 0000000000..7e92dfaa12 --- /dev/null +++ b/util/pagedqueue_test.go @@ -0,0 +1,360 @@ +// Copyright (C) 2019-2026 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 util + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" +) + +// TestPagedQueueNew checks that the constructor enforces a minimum page size of 4. +func TestPagedQueueNew(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + pq := NewPagedQueue[int](10) + require.NotNil(t, pq) + require.Zero(t, pq.Len()) + require.Equal(t, 10, cap(pq.entries)) + + // sizes below 4 are promoted to 4 + small := NewPagedQueue[int](2) + require.Equal(t, 4, cap(small.entries)) + require.Empty(t, small.entries) +} + +// TestPagedQueueAppendAndLen checks that Len tracks the count correctly as items are appended across pages. +func TestPagedQueueAppendAndLen(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + pq := NewPagedQueue[int](4) + for i := 0; i < 10; i++ { + pq = pq.Append(i) + require.Equal(t, i+1, pq.Len()) + } +} + +// TestPagedQueueGet checks retrieval by index starting from the head page. +func TestPagedQueueGet(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 10; i++ { + cur = cur.Append(i) + } + + // retrieve every element using the head page as the starting point + for i := 0; i < 10; i++ { + page, val := head.Get(i) + require.NotNil(t, page) + require.Equal(t, i, val) + } +} + +// TestPagedQueueGetAdvancingPage verifies that callers can advance their page pointer to avoid re-traversal from the head. +func TestPagedQueueGetAdvancingPage(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // Demonstrate that callers can advance their page pointer to avoid + // traversing from the head on every Get. + head := NewPagedQueue[string](4) + cur := head + words := []string{"a", "b", "c", "d", "e", "f", "g", "h"} + for _, w := range words { + cur = cur.Append(w) + } + + page := head + for i, want := range words { + var got string + page, got = page.Get(i) + require.Equal(t, want, got) + } +} + +// TestPagedQueueGetZeroValueBeyondEnd checks that an index past the last entry returns the zero value. +func TestPagedQueueGetZeroValueBeyondEnd(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 3; i++ { + cur = cur.Append(i + 1) + } + + // index past the end returns the zero value + _, val := head.Get(100) + require.Equal(t, 0, val) +} + +// TestPagedQueueGetPanicOnLowIndex checks that asking a non-head page for an index that belongs to an earlier page panics. +func TestPagedQueueGetPanicOnLowIndex(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // Fill two pages so the second page has baseIdx > 0. + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 8; i++ { + cur = cur.Append(i) + } + + // Asking a non-head page for an index that belongs to an earlier page panics + // because localIdx goes negative and entries[-n] is out of bounds. + require.Panics(t, func() { + cur.Get(0) + }) +} + +// TestPagedQueuePtr checks that Ptr returns a stable pointer to the correct entry across page boundaries. +func TestPagedQueuePtr(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 10; i++ { + cur = cur.Append(i) + } + + // collect pointers to all entries, then mutate through them + ptrs := make([]*int, 10) + page := head + for i := range 10 { + page, ptrs[i] = page.Ptr(i) + require.NotNil(t, ptrs[i]) + require.Equal(t, i, *ptrs[i]) + } + + for i, p := range ptrs { + *p = i * 100 + } + + // verify mutations are visible via Get + for i := range 10 { + _, val := head.Get(i) + require.Equal(t, i*100, val) + } +} + +// TestPagedQueuePtrBeyondEnd checks that Ptr returns nil for an out-of-bounds index. +func TestPagedQueuePtrBeyondEnd(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 3; i++ { + cur = cur.Append(i) + } + + _, p := head.Ptr(100) + require.Nil(t, p) +} + +// TestPagedQueuePtrPanicOnLowIndex checks that Ptr panics when the index is below the page's baseIdx. +func TestPagedQueuePtrPanicOnLowIndex(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 8; i++ { + cur = cur.Append(i) + } + + require.Panics(t, func() { cur.Ptr(0) }) +} + +// TestPagedQueueAllPtrs checks that AllPtrs yields stable pointers in insertion order. +func TestPagedQueueAllPtrs(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + const n = 15 + for i := 0; i < n; i++ { + cur = cur.Append(i) + } + + // collect pointers then mutate through them + var ptrs []*int + for p := range head.AllPtrs() { + ptrs = append(ptrs, p) + } + require.Len(t, ptrs, n) + for i, p := range ptrs { + *p = i * 10 + } + for idx, v := range head.All2() { + require.Equal(t, idx*10, v) + } +} + +// TestPagedQueueAllPtrs2 checks that AllPtrs2 yields correct indices and stable pointers across page boundaries. +func TestPagedQueueAllPtrs2(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + const n = 15 + for i := 0; i < n; i++ { + cur = cur.Append(i) + } + + for idx, p := range head.AllPtrs2() { + require.Equal(t, idx, *p) + *p = idx * 10 + } + for idx, v := range head.All2() { + require.Equal(t, idx*10, v) + } +} + +// TestPagedQueueAll checks that the iterator yields all entries in insertion order. +func TestPagedQueueAll(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + const n = 15 + for i := 0; i < n; i++ { + cur = cur.Append(i) + } + + var collected []int + for v := range head.All() { + collected = append(collected, v) + } + require.Len(t, collected, n) + for i := 0; i < n; i++ { + require.Equal(t, i, collected[i]) + } +} + +// TestPagedQueueAllEarlyReturn checks that breaking early from All() works without panicking. +func TestPagedQueueAllEarlyReturn(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 12; i++ { + cur = cur.Append(i) + } + + // stopping early should not panic and should return only the first few items + var collected []int + for v := range head.All() { + collected = append(collected, v) + if len(collected) == 5 { + break + } + } + require.Len(t, collected, 5) + for i := 0; i < 5; i++ { + require.Equal(t, i, collected[i]) + } +} + +// TestPagedQueueAll2 checks that All2 yields correct queue-wide indices and values across page boundaries. +func TestPagedQueueAll2(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + const n = 15 + for i := 0; i < n; i++ { + cur = cur.Append(i * 10) + } + + for idx, v := range head.All2() { + require.Equal(t, idx*10, v) + } +} + +// TestPagedQueueAll2EarlyReturn checks that breaking early from All2() works without panicking. +func TestPagedQueueAll2EarlyReturn(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 12; i++ { + cur = cur.Append(i) + } + + var collected []int + for idx, v := range head.All2() { + require.Equal(t, idx, v) + collected = append(collected, v) + if len(collected) == 5 { + break + } + } + require.Len(t, collected, 5) +} + +// TestPagedQueuePageGrowth checks that pages double in size and all values survive across many page boundaries. +func TestPagedQueuePageGrowth(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // Each new page doubles in size relative to the previous one. + head := NewPagedQueue[int](4) + cur := head + for i := 0; i < 100; i++ { + cur = cur.Append(i) + } + + require.Equal(t, 100, head.Len()) + + // All values must be retrievable in order. + var collected []int + for v := range head.All() { + collected = append(collected, v) + } + require.Len(t, collected, 100) + for i, v := range collected { + require.Equal(t, i, v) + } +} + +// TestPagedQueueEmptyAll checks that iterating an empty queue produces no values. +func TestPagedQueueEmptyAll(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + head := NewPagedQueue[int](8) + var count int + for range head.All() { + count++ + } + require.Equal(t, 0, count) +} diff --git a/util/rateLimit.go b/util/rateLimit.go index 2bb13af97c..ec7775daed 100644 --- a/util/rateLimit.go +++ b/util/rateLimit.go @@ -26,8 +26,9 @@ import ( "sync" "time" - "github.com/algorand/go-algorand/util/metrics" "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/util/metrics" ) var errConManDropped = errors.New("congestionManager prevented client from consuming capacity") diff --git a/util/rateLimit_test.go b/util/rateLimit_test.go index 16d28ff020..c544781af0 100644 --- a/util/rateLimit_test.go +++ b/util/rateLimit_test.go @@ -20,8 +20,9 @@ import ( "testing" "time" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/assert" + + "github.com/algorand/go-algorand/test/partitiontest" ) type mockClient string diff --git a/util/s3/s3Helper_test.go b/util/s3/s3Helper_test.go index 5303f6628b..b29bddfef1 100644 --- a/util/s3/s3Helper_test.go +++ b/util/s3/s3Helper_test.go @@ -22,8 +22,9 @@ import ( "reflect" "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestGetS3UploadBucket(t *testing.T) { diff --git a/util/set_test.go b/util/set_test.go index 03da2c912e..c33df9241b 100644 --- a/util/set_test.go +++ b/util/set_test.go @@ -19,8 +19,9 @@ package util import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestMakeSet(t *testing.T) { diff --git a/util/util_test.go b/util/util_test.go index 1ad4cbcd67..bfb596f5e3 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -19,8 +19,9 @@ package util import ( "testing" - "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) func TestGetTotalMemory(t *testing.T) {