From 175a7e1754426ab27fdd0e5050740bdd006ba0b7 Mon Sep 17 00:00:00 2001 From: Eric Warehime Date: Thu, 2 Nov 2023 12:26:17 -0700 Subject: [PATCH 01/38] p2p: Capabilities Advertisement for Archival/Catchpoints (#5702) --- catchup/universalFetcher_test.go | 1 - cmd/goal/network.go | 98 +- config/config.go | 15 + config/config_test.go | 126 ++ config/localTemplate.go | 38 +- config/local_defaults.go | 3 +- crypto/merklearray/proof.go | 5 +- crypto/merklearray/proof_test.go | 11 +- daemon/algod/api/algod.oas2.json | 79 + daemon/algod/api/algod.oas3.yml | 88 + .../api/server/v2/generated/data/routes.go | 403 ++-- .../v2/generated/experimental/routes.go | 404 ++-- .../api/server/v2/generated/model/types.go | 41 + .../nonparticipating/private/routes.go | 403 ++-- .../nonparticipating/public/routes.go | 557 ++--- .../generated/participating/private/routes.go | 410 ++-- .../generated/participating/public/routes.go | 317 +-- daemon/algod/api/server/v2/handlers.go | 1 + .../algod/api/server/v2/test/handlers_test.go | 4 +- daemon/algod/api/server/v2/utils.go | 66 +- data/transactions/logic/assembler.go | 8 +- data/transactions/logic/opcodeExplain.go | 82 +- data/transactions/logic/opcodes.go | 60 +- docker/README.md | 2 +- go.mod | 25 +- go.sum | 381 +--- installer/config.json.example | 3 +- ledger/acctupdates.go | 2 +- ledger/catchpointtracker.go | 38 +- ledger/simulation/initialStates.go | 179 ++ ledger/simulation/simulation_eval_test.go | 1889 +++++++++++++++-- ledger/simulation/trace.go | 2 + ledger/simulation/tracer.go | 20 +- ...merkle_commiter.go => merkle_committer.go} | 0 netdeploy/network.go | 4 +- netdeploy/networkTemplate.go | 7 +- netdeploy/networkTemplates_test.go | 3 +- network/p2p/capabilities.go | 172 ++ network/p2p/capabilities_test.go | 289 +++ network/p2p/dht/dht.go | 100 + network/p2p/dht/dht_test.go | 106 + network/p2p/p2p.go | 42 +- network/p2p/p2p_test.go | 76 + network/p2p/peerstore/peerstore.go | 327 ++- network/p2p/peerstore/peerstore_test.go | 351 ++- network/p2p/peerstore/utils.go | 18 + network/p2p/peerstore/utils_test.go | 3 +- network/p2pNetwork.go | 21 +- network/p2pNetwork_test.go | 127 ++ network/phonebook.go | 2 +- network/wsNetwork_test.go | 133 +- node/node.go | 34 +- rpcs/blockService_test.go | 8 + scripts/configure_dev.sh | 44 +- .../features/privatenet/privatenet_test.go | 62 + test/e2e-go/restAPI/helpers.go | 2 +- test/e2e-go/restAPI/other/appsRestAPI_test.go | 14 +- test/e2e-go/restAPI/restClient_test.go | 8 +- .../restAPI/simulate/simulateRestAPI_test.go | 362 +++- .../stateproof/stateproofRestAPI_test.go | 4 +- test/framework/fixtures/goalFixture.go | 76 +- test/framework/fixtures/libgoalFixture.go | 4 +- test/scripts/test_private_network.sh | 4 +- test/testdata/configs/config-v30.json | 1 + test/testdata/configs/config-v31.json | 3 +- tools/block-generator/go.mod | 25 +- tools/block-generator/go.sum | 387 +--- util/set.go | 42 + 68 files changed, 6375 insertions(+), 2247 deletions(-) create mode 100644 ledger/simulation/initialStates.go rename ledger/store/trackerdb/sqlitedriver/{merkle_commiter.go => merkle_committer.go} (100%) create mode 100644 network/p2p/capabilities.go create mode 100644 network/p2p/capabilities_test.go create mode 100644 network/p2p/dht/dht.go create mode 100644 network/p2p/dht/dht_test.go create mode 100644 network/p2p/p2p_test.go create mode 100644 test/e2e-go/features/privatenet/privatenet_test.go create mode 100644 util/set.go diff --git a/catchup/universalFetcher_test.go b/catchup/universalFetcher_test.go index bf74c95702..836360139f 100644 --- a/catchup/universalFetcher_test.go +++ b/catchup/universalFetcher_test.go @@ -94,7 +94,6 @@ func TestUGetBlockHTTP(t *testing.T) { blockServiceConfig := config.GetDefaultLocal() blockServiceConfig.EnableBlockService = true - blockServiceConfig.EnableBlockServiceFallbackToArchiver = false net := &httpTestPeerSource{} ls := rpcs.MakeBlockService(logging.Base(), blockServiceConfig, ledger, net, "test genesisID") diff --git a/cmd/goal/network.go b/cmd/goal/network.go index 5c32b68e36..efc7117edb 100644 --- a/cmd/goal/network.go +++ b/cmd/goal/network.go @@ -28,6 +28,7 @@ import ( "github.com/algorand/go-algorand/cmd/util/datadir" "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/gen" "github.com/algorand/go-algorand/netdeploy" "github.com/algorand/go-algorand/util" ) @@ -40,11 +41,11 @@ var noImportKeys bool var noClean bool var devModeOverride bool var startOnCreation bool +var pregenDir string func init() { networkCmd.AddCommand(networkCreateCmd) networkCmd.PersistentFlags().StringVarP(&networkRootDir, "rootdir", "r", "", "Root directory for the private network directories") - networkCmd.MarkPersistentFlagRequired("rootdir") networkCreateCmd.Flags().StringVarP(&networkName, "network", "n", "", "Specify the name to use for the private network") networkCreateCmd.Flags().StringVarP(&networkTemplateFile, "template", "t", "", "Specify the path to the template file for the network") @@ -52,14 +53,34 @@ func init() { networkCreateCmd.Flags().BoolVar(&noClean, "noclean", false, "Prevents auto-cleanup on error - for diagnosing problems") networkCreateCmd.Flags().BoolVar(&devModeOverride, "devMode", false, "Forces the configuration to enable DevMode, returns an error if the template is not compatible with DevMode.") networkCreateCmd.Flags().BoolVarP(&startOnCreation, "start", "s", false, "Automatically start the network after creating it.") + networkCreateCmd.Flags().StringVarP(&pregenDir, "pregendir", "p", "", "Specify the path to the directory with pregenerated genesis.json, root and partkeys to import into the network directory. By default, the genesis.json and keys will be generated on start. This should only be used on private networks.") + networkCreateCmd.MarkFlagRequired("rootdir") + networkCmd.AddCommand(networkStartCmd) networkStartCmd.Flags().StringVarP(&startNode, "node", "n", "", "Specify the name of a specific node to start") + networkStartCmd.MarkFlagRequired("rootdir") - networkCmd.AddCommand(networkStartCmd) networkCmd.AddCommand(networkRestartCmd) + networkRestartCmd.MarkFlagRequired("rootdir") + networkCmd.AddCommand(networkStopCmd) + networkStopCmd.MarkFlagRequired("rootdir") + networkCmd.AddCommand(networkStatusCmd) + networkStatusCmd.MarkFlagRequired("rootdir") + networkCmd.AddCommand(networkDeleteCmd) + networkDeleteCmd.MarkFlagRequired("rootdir") + + networkCmd.AddCommand(networkPregenCmd) + networkPregenCmd.Flags().StringVarP(&networkTemplateFile, "template", "t", "", "Specify the path to the template file for the network") + networkPregenCmd.Flags().StringVarP(&pregenDir, "pregendir", "p", "", "Specify the path to the directory to export genesis.json, root and partkey files. This should only be used on private networks.") + networkPregenCmd.MarkFlagRequired("pregendir") + // Hide rootdir flag as it is unused and will error if used with this command. + networkPregenCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { + _ = command.Flags().MarkHidden("rootdir") + command.Parent().HelpFunc()(command, strings) + }) } var networkCmd = &cobra.Command{ @@ -112,6 +133,18 @@ var networkCreateCmd = &cobra.Command{ reportErrorf(infoNetworkAlreadyExists, networkRootDir) } + // If pregendir is specified, copy files over + if pregenDir != "" { + pregenDir, err = filepath.Abs(pregenDir) + if err != nil { + panic(err) + } + err = util.CopyFolder(pregenDir, networkRootDir) + if err != nil { + panic(err) + } + } + binDir, err := util.ExeDir() if err != nil { panic(err) @@ -246,3 +279,64 @@ var networkDeleteCmd = &cobra.Command{ reportInfof(infoNetworkDeleted, networkRootDir) }, } + +var networkPregenCmd = &cobra.Command{ + Use: "pregen", + Short: "Pregenerates the genesis.json, root and participation keys for a wallet", + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + var err error + if networkRootDir != "" { + reportErrorf("This command does not take a network directory as an argument. Use --pregendir flag instead.") + } + + pregenDir, err = filepath.Abs(pregenDir) + if err != nil { + panic(err) + } + + var templateReader io.Reader + + if networkTemplateFile == "" { + templateReader = strings.NewReader(defaultNetworkTemplate) + } else { + networkTemplateFile, err = filepath.Abs(networkTemplateFile) + if err != nil { + panic(err) + } + file, osErr := os.Open(networkTemplateFile) + if osErr != nil { + reportErrorf(errorCreateNetwork, osErr) + } + + defer file.Close() + templateReader = file + } + + // Make sure target directory does not exist or is empty + if util.FileExists(pregenDir) && !util.IsEmpty(pregenDir) { + reportErrorf(infoNetworkAlreadyExists, pregenDir) + } + + var template netdeploy.NetworkTemplate + err = netdeploy.LoadTemplateFromReader(templateReader, &template) + if err != nil { + reportErrorf("Error in loading template: %v\n", err) + } + + dataDir := datadir.MaybeSingleDataDir() + var consensus config.ConsensusProtocols + if dataDir != "" { + // try to load the consensus from there. If there is none, we can just use the built in one. + consensus, _ = config.PreloadConfigurableConsensusProtocols(dataDir) + } + if err = template.Validate(); err != nil { + reportErrorf("Error in template validation: %v\n", err) + } + + err = gen.GenerateGenesisFiles(template.Genesis, config.Consensus.Merge(consensus), pregenDir, os.Stdout) + if err != nil { + reportErrorf("Cannot write genesis files: %s", err) + } + }, +} diff --git a/config/config.go b/config/config.go index fc2dd30050..ad363f3644 100644 --- a/config/config.go +++ b/config/config.go @@ -89,6 +89,21 @@ const MaxGenesisIDLen = 128 // MaxEvalDeltaTotalLogSize is the maximum size of the sum of all log sizes in a single eval delta. const MaxEvalDeltaTotalLogSize = 1024 +// CatchpointTrackingModeUntracked defines the CatchpointTracking mode that does _not_ track catchpoints +const CatchpointTrackingModeUntracked = -1 + +// CatchpointTrackingModeAutomatic defines the CatchpointTracking mode that automatically determines catchpoint tracking +// and storage based on the Archival property and CatchpointInterval. +const CatchpointTrackingModeAutomatic = 0 + +// CatchpointTrackingModeTracked defines the CatchpointTracking mode that tracks catchpoint +// as long as CatchpointInterval > 0 +const CatchpointTrackingModeTracked = 1 + +// CatchpointTrackingModeStored defines the CatchpointTracking mode that tracks and stores catchpoints +// as long as CatchpointInterval > 0 +const CatchpointTrackingModeStored = 2 + // LoadConfigFromDisk returns a Local config structure based on merging the defaults // with settings loaded from the config file from the custom dir. If the custom file // cannot be loaded, the default config is returned (with the error from loading the diff --git a/config/config_test.go b/config/config_test.go index c88b53834e..87b4cc4d43 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -803,3 +803,129 @@ func TestResolveLogPaths(t *testing.T) { require.Equal(t, "mycoolLogDir/node.log", log) require.Equal(t, "myCoolLogArchive/node.archive.log", archive) } + +func TestStoresCatchpoints(t *testing.T) { + partitiontest.PartitionTest(t) + + var tests = []struct { + name string + catchpointTracking int64 + catchpointInterval uint64 + archival bool + expected bool + }{ + { + name: "-1 w/ no catchpoint interval expects false", + catchpointTracking: CatchpointTrackingModeUntracked, + catchpointInterval: 0, + expected: false, + }, + { + name: "-1 expects false", + catchpointTracking: CatchpointTrackingModeUntracked, + catchpointInterval: GetDefaultLocal().CatchpointInterval, + archival: GetDefaultLocal().Archival, + expected: false, + }, + { + name: "0 expects false", + catchpointTracking: CatchpointTrackingModeAutomatic, + catchpointInterval: GetDefaultLocal().CatchpointInterval, + archival: GetDefaultLocal().Archival, + expected: false, + }, + { + name: "0 w/ archival expects true", + catchpointTracking: CatchpointTrackingModeAutomatic, + catchpointInterval: GetDefaultLocal().CatchpointInterval, + archival: true, + expected: true, + }, + { + name: "0 w/ archival & catchpointInterval=0 expects false", + catchpointTracking: CatchpointTrackingModeAutomatic, + catchpointInterval: 0, + archival: true, + expected: false, + }, + { + name: "1 expects false", + catchpointTracking: CatchpointTrackingModeTracked, + catchpointInterval: GetDefaultLocal().CatchpointInterval, + archival: GetDefaultLocal().Archival, + expected: false, + }, + { + name: "1 w/ archival expects true", + catchpointTracking: CatchpointTrackingModeTracked, + catchpointInterval: GetDefaultLocal().CatchpointInterval, + archival: true, + expected: true, + }, + { + name: "1 w/ archival & catchpointInterval=0 expects false", + catchpointTracking: CatchpointTrackingModeTracked, + catchpointInterval: 0, + archival: true, + expected: false, + }, + { + name: "2 w/ catchpointInterval=0 expects false", + catchpointTracking: CatchpointTrackingModeStored, + catchpointInterval: 0, + archival: GetDefaultLocal().Archival, + expected: false, + }, + { + name: "2 expects true", + catchpointTracking: CatchpointTrackingModeStored, + catchpointInterval: GetDefaultLocal().CatchpointInterval, + archival: GetDefaultLocal().Archival, + expected: true, + }, + { + name: "99 expects false", + catchpointTracking: 99, + catchpointInterval: GetDefaultLocal().CatchpointInterval, + archival: GetDefaultLocal().Archival, + expected: false, + }, + { + name: "99 w/ catchpointInterval=0 expects false", + catchpointTracking: 99, + catchpointInterval: 0, + archival: GetDefaultLocal().Archival, + expected: false, + }, + { + name: "27 expects false", + catchpointTracking: 27, + catchpointInterval: GetDefaultLocal().CatchpointInterval, + archival: GetDefaultLocal().Archival, + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cfg := GetDefaultLocal() + cfg.CatchpointTracking = test.catchpointTracking + cfg.CatchpointInterval = test.catchpointInterval + cfg.Archival = test.archival + require.Equal(t, test.expected, cfg.StoresCatchpoints()) + if cfg.StoresCatchpoints() { + require.Equal(t, true, cfg.TracksCatchpoints()) + } + }) + } +} + +func TestTracksCatchpointsWithoutStoring(t *testing.T) { + partitiontest.PartitionTest(t) + + cfg := GetDefaultLocal() + cfg.CatchpointTracking = CatchpointTrackingModeTracked + cfg.CatchpointInterval = 10000 + cfg.Archival = false + require.Equal(t, true, cfg.TracksCatchpoints()) + require.Equal(t, false, cfg.StoresCatchpoints()) +} diff --git a/config/localTemplate.go b/config/localTemplate.go index 4202c2648a..9036745565 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -388,7 +388,7 @@ type Local struct { // A value of 1 means "track catchpoints as long as CatchpointInterval > 0". // A value of 2 means "track catchpoints and always generate catchpoint files as long as CatchpointInterval > 0". // A value of 0 means automatic, which is the default value. In this mode, a non archival node would not track the catchpoints, and an archival node would track the catchpoints as long as CatchpointInterval > 0. - // Other values of CatchpointTracking would give a warning in the log file, and would behave as if the default value was provided. + // Other values of CatchpointTracking would behave as if the default value was provided. CatchpointTracking int64 `version[11]:"0"` // LedgerSynchronousMode defines the synchronous mode used by the ledger database. The supported options are: @@ -440,7 +440,7 @@ type Local struct { // an archiver or return StatusNotFound (404) when in does not have the requested round, and // BlockServiceCustomFallbackEndpoints is empty. // The archiver is randomly selected, if none is available, will return StatusNotFound (404). - EnableBlockServiceFallbackToArchiver bool `version[16]:"true"` + EnableBlockServiceFallbackToArchiver bool `version[16]:"true" version[31]:"false"` // CatchupBlockValidateMode is a development and testing configuration used by the catchup service. // It can be used to omit certain validations to speed up the catchup process, or to apply extra validations which are redundant in normal operation. @@ -581,6 +581,9 @@ type Local struct { // DisableAPIAuth turns off authentication for public (non-admin) API endpoints. DisableAPIAuth bool `version[30]:"false"` + + // EnableDHT will turn on the hash table for use with capabilities advertisement + EnableDHTProviders bool `version[30]:"false"` } // DNSBootstrapArray returns an array of one or more DNS Bootstrap identifiers @@ -862,3 +865,34 @@ func (cfg *Local) AdjustConnectionLimits(requiredFDs, maxFDs uint64) bool { return true } + +// StoresCatchpoints returns true if the node is configured to store catchpoints +func (cfg *Local) StoresCatchpoints() bool { + if cfg.CatchpointInterval <= 0 { + return false + } + switch cfg.CatchpointTracking { + case CatchpointTrackingModeUntracked: + // No catchpoints. + default: + fallthrough + case CatchpointTrackingModeAutomatic, CatchpointTrackingModeTracked: + if cfg.Archival { + return true + } + case CatchpointTrackingModeStored: + return true + } + return false +} + +// TracksCatchpoints returns true if the node is configured to track catchpoints +func (cfg *Local) TracksCatchpoints() bool { + if cfg.StoresCatchpoints() { + return true + } + if cfg.CatchpointTracking == CatchpointTrackingModeTracked && cfg.CatchpointInterval > 0 { + return true + } + return false +} diff --git a/config/local_defaults.go b/config/local_defaults.go index fd0aa20521..348e8eb704 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -64,8 +64,9 @@ var defaultLocal = Local{ EnableAgreementTimeMetrics: false, EnableAssembleStats: false, EnableBlockService: false, - EnableBlockServiceFallbackToArchiver: true, + EnableBlockServiceFallbackToArchiver: false, EnableCatchupFromArchiveServers: false, + EnableDHTProviders: false, EnableDeveloperAPI: false, EnableExperimentalAPI: false, EnableFollowMode: false, diff --git a/crypto/merklearray/proof.go b/crypto/merklearray/proof.go index 0489312035..3455baa08f 100644 --- a/crypto/merklearray/proof.go +++ b/crypto/merklearray/proof.go @@ -149,7 +149,7 @@ func (p *SingleLeafProof) GetConcatenatedProof() []byte { } // ProofDataToSingleLeafProof receives serialized proof data and uses it to construct a proof object. -func ProofDataToSingleLeafProof(hashTypeData string, treeDepth uint64, proofBytes []byte) (SingleLeafProof, error) { +func ProofDataToSingleLeafProof(hashTypeData string, proofBytes []byte) (SingleLeafProof, error) { hashType, err := crypto.UnmarshalHashType(hashTypeData) if err != nil { return SingleLeafProof{}, err @@ -158,7 +158,7 @@ func ProofDataToSingleLeafProof(hashTypeData string, treeDepth uint64, proofByte var proof SingleLeafProof proof.HashFactory = crypto.HashFactory{HashType: hashType} - proof.TreeDepth = uint8(treeDepth) + proof.TreeDepth = 0 digestSize := proof.HashFactory.NewHash().Size() if len(proofBytes)%digestSize != 0 { @@ -172,6 +172,7 @@ func ProofDataToSingleLeafProof(hashTypeData string, treeDepth uint64, proofByte copy(d[:], proofBytes) proofPath = append(proofPath, d[:]) proofBytes = proofBytes[len(d):] + proof.TreeDepth++ } proof.Path = proofPath diff --git a/crypto/merklearray/proof_test.go b/crypto/merklearray/proof_test.go index 9591d6ee41..a160371550 100644 --- a/crypto/merklearray/proof_test.go +++ b/crypto/merklearray/proof_test.go @@ -157,8 +157,9 @@ func TestConcatenatedProofsMissingChild(t *testing.T) { err = Verify(tree.Root(), map[uint64]crypto.Hashable{6: array[6]}, newP.ToProof()) a.NoError(err) - recomputedProof, err := ProofDataToSingleLeafProof(p.HashFactory.HashType.String(), uint64(p.TreeDepth), concatenatedProof) + recomputedProof, err := ProofDataToSingleLeafProof(p.HashFactory.HashType.String(), concatenatedProof) a.NoError(err) + a.Equal(recomputedProof.TreeDepth, p.TreeDepth) // verify that we can reconstruct the original singleLeafProof from the concatenated proof err = Verify(tree.Root(), map[uint64]crypto.Hashable{6: array[6]}, recomputedProof.ToProof()) @@ -189,8 +190,9 @@ func TestConcatenatedProofsFullTree(t *testing.T) { err = Verify(tree.Root(), map[uint64]crypto.Hashable{6: array[6]}, newP.ToProof()) a.NoError(err) - recomputedProof, err := ProofDataToSingleLeafProof(p.HashFactory.HashType.String(), uint64(p.TreeDepth), concatenatedProof) + recomputedProof, err := ProofDataToSingleLeafProof(p.HashFactory.HashType.String(), concatenatedProof) a.NoError(err) + a.Equal(recomputedProof.TreeDepth, p.TreeDepth) // verify that we can reconstruct the original singleLeafProof from the concatenated proof err = Verify(tree.Root(), map[uint64]crypto.Hashable{6: array[6]}, recomputedProof.ToProof()) @@ -218,8 +220,9 @@ func TestConcatenatedProofsOneLeaf(t *testing.T) { err = Verify(tree.Root(), map[uint64]crypto.Hashable{0: array[0]}, newP.ToProof()) a.NoError(err) - recomputedProof, err := ProofDataToSingleLeafProof(p.HashFactory.HashType.String(), uint64(p.TreeDepth), concatenatedProof) + recomputedProof, err := ProofDataToSingleLeafProof(p.HashFactory.HashType.String(), concatenatedProof) a.NoError(err) + a.Equal(recomputedProof.TreeDepth, p.TreeDepth) // verify that we can reconstruct the original singleLeafProof from the concatenated proof err = Verify(tree.Root(), map[uint64]crypto.Hashable{0: array[0]}, recomputedProof.ToProof()) @@ -230,7 +233,7 @@ func TestProofDeserializationError(t *testing.T) { partitiontest.PartitionTest(t) a := require.New(t) - _, err := ProofDataToSingleLeafProof(crypto.Sha256.String(), 1, []byte{1}) + _, err := ProofDataToSingleLeafProof(crypto.Sha256.String(), []byte{1}) a.ErrorIs(err, ErrProofLengthDigestSizeMismatch) } diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index ed75263e03..f8b85e2c36 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -3289,6 +3289,23 @@ } } }, + "AvmKeyValue": { + "description": "Represents an AVM key-value pair in an application store.", + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string", + "format": "byte" + }, + "value": { + "$ref": "#/definitions/AvmValue" + } + } + }, "StateDelta": { "description": "Application state delta.", "type": "array", @@ -4131,6 +4148,52 @@ } } }, + "ApplicationKVStorage": { + "description": "An application's global/local/box state.", + "required": [ + "kvs" + ], + "properties": { + "kvs": { + "description": "Key-Value pairs representing application states.", + "type": "array", + "items": { + "$ref": "#/definitions/AvmKeyValue" + } + }, + "account": { + "description": "The address of the account associated with the local state.", + "type": "string", + "x-algorand-format": "Address" + } + } + }, + "ApplicationInitialStates": { + "description": "An application's initial global/local/box states that were accessed during simulation.", + "required": [ + "id" + ], + "properties": { + "id": { + "description": "Application index.", + "type": "integer", + "x-algorand-format": "uint64" + }, + "app-locals": { + "description": "An application's initial local states tied to different accounts.", + "type": "array", + "items": { + "$ref": "#/definitions/ApplicationKVStorage" + } + }, + "app-globals": { + "$ref": "#/definitions/ApplicationKVStorage" + }, + "app-boxes": { + "$ref": "#/definitions/ApplicationKVStorage" + } + } + }, "SimulationOpcodeTraceUnit": { "description": "The set of trace information and effect from evaluating a single opcode.", "type": "object", @@ -4279,6 +4342,19 @@ } } } + }, + "SimulateInitialStates": { + "description": "Initial states of resources that were accessed during simulation.", + "type": "object", + "properties": { + "app-initial-states": { + "description": "The initial states of accessed application before simulation. The order of this array is arbitrary.", + "type": "array", + "items": { + "$ref": "#/definitions/ApplicationInitialStates" + } + } + } } }, "parameters": { @@ -4929,6 +5005,9 @@ }, "exec-trace-config": { "$ref": "#/definitions/SimulateTraceConfig" + }, + "initial-states": { + "$ref": "#/definitions/SimulateInitialStates" } } } diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index da57568a60..b690d06958 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -815,6 +815,9 @@ "exec-trace-config": { "$ref": "#/components/schemas/SimulateTraceConfig" }, + "initial-states": { + "$ref": "#/components/schemas/SimulateInitialStates" + }, "last-round": { "description": "The round immediately preceding this simulation. State changes through this round were used to run this simulation.", "type": "integer" @@ -1217,6 +1220,54 @@ ], "type": "object" }, + "ApplicationInitialStates": { + "description": "An application's initial global/local/box states that were accessed during simulation.", + "properties": { + "app-boxes": { + "$ref": "#/components/schemas/ApplicationKVStorage" + }, + "app-globals": { + "$ref": "#/components/schemas/ApplicationKVStorage" + }, + "app-locals": { + "description": "An application's initial local states tied to different accounts.", + "items": { + "$ref": "#/components/schemas/ApplicationKVStorage" + }, + "type": "array" + }, + "id": { + "description": "Application index.", + "type": "integer", + "x-algorand-format": "uint64" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "ApplicationKVStorage": { + "description": "An application's global/local/box state.", + "properties": { + "account": { + "description": "The address of the account associated with the local state.", + "type": "string", + "x-algorand-format": "Address" + }, + "kvs": { + "description": "Key-Value pairs representing application states.", + "items": { + "$ref": "#/components/schemas/AvmKeyValue" + }, + "type": "array" + } + }, + "required": [ + "kvs" + ], + "type": "object" + }, "ApplicationLocalReference": { "description": "References an account's local state for an application.", "properties": { @@ -1495,6 +1546,24 @@ ], "type": "object" }, + "AvmKeyValue": { + "description": "Represents an AVM key-value pair in an application store.", + "properties": { + "key": { + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + }, + "value": { + "$ref": "#/components/schemas/AvmValue" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, "AvmValue": { "description": "Represents an AVM value.", "properties": { @@ -2066,6 +2135,19 @@ ], "type": "object" }, + "SimulateInitialStates": { + "description": "Initial states of resources that were accessed during simulation.", + "properties": { + "app-initial-states": { + "description": "The initial states of accessed application before simulation. The order of this array is arbitrary.", + "items": { + "$ref": "#/components/schemas/ApplicationInitialStates" + }, + "type": "array" + } + }, + "type": "object" + }, "SimulateRequest": { "description": "Request type for simulation endpoint.", "properties": { @@ -6820,6 +6902,9 @@ "exec-trace-config": { "$ref": "#/components/schemas/SimulateTraceConfig" }, + "initial-states": { + "$ref": "#/components/schemas/SimulateInitialStates" + }, "last-round": { "description": "The round immediately preceding this simulation. State changes through this round were used to run this simulation.", "type": "integer" @@ -6853,6 +6938,9 @@ "exec-trace-config": { "$ref": "#/components/schemas/SimulateTraceConfig" }, + "initial-states": { + "$ref": "#/components/schemas/SimulateInitialStates" + }, "last-round": { "description": "The round immediately preceding this simulation. State changes through this round were used to run this simulation.", "type": "integer" diff --git a/daemon/algod/api/server/v2/generated/data/routes.go b/daemon/algod/api/server/v2/generated/data/routes.go index 4b1d09d4a9..a17b5f1ca8 100644 --- a/daemon/algod/api/server/v2/generated/data/routes.go +++ b/daemon/algod/api/server/v2/generated/data/routes.go @@ -114,206 +114,209 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9/ZPbNrLgv4LSe1WOfeKMv5Ld+Grr3cROsnNxEpdnkr33bF8CkS0JOxTABcAZKT7/", - "71foBkiQBCXOR+zsq/3JHpEEGo1Go7/7/SxXm0pJkNbMnr2fVVzzDVjQ+BfPc1VLm4nC/VWAybWorFBy", - "9iw8Y8ZqIVez+Uy4Xytu17P5TPINtO+47+czDf+ohYZi9szqGuYzk69hw93Adle5t5uRttlKZX6IExri", - "9MXsw54HvCg0GDOE8kdZ7piQeVkXwKzm0vDcPTLsStg1s2thmP+YCcmUBKaWzK47L7OlgLIwR2GR/6hB", - "76JV+snHl/ShBTHTqoQhnM/VZiEkBKigAarZEGYVK2CJL625ZW4GB2t40SpmgOt8zZZKHwCVgIjhBVlv", - "Zs/ezAzIAjTuVg7iEv+71AC/QWa5XoGdvZunFre0oDMrNomlnXrsazB1aQ3Dd3GNK3EJkrmvjtj3tbFs", - "AYxL9vqb5+zJkydfuoVsuLVQeCIbXVU7e7wm+nz2bFZwC+HxkNZ4uVKayyJr3n/9zXOc/8wvcOpb3BhI", - "H5YT94SdvhhbQPgwQUJCWljhPnSo332ROBTtzwtYKg0T94RevtNNief/pLuSc5uvKyWkTewLw6eMHid5", - "WPT5Ph7WANB5v3KY0m7QNw+zL9+9fzR/9PDDv705yf7L//n5kw8Tl/+8GfcABpIv5rXWIPNdttLA8bSs", - "uRzi47WnB7NWdVmwNb/EzecbZPX+W+a+JdZ5ycva0YnItTopV8ow7smogCWvS8vCxKyWpWNTbjRP7UwY", - "Vml1KQoo5o77Xq1FvmY5NzQEvseuRFk6GqwNFGO0ll7dnsP0IUaJg+tG+MAF/XGR0a7rACZgi9wgy0tl", - "ILPqwPUUbhwuCxZfKO1dZa53WbHzNTCc3D2gyxZxJx1Nl+WOWdzXgnHDOAtX05yJJdupml3h5pTiAr/3", - "q3FY2zCHNNyczj3qDu8Y+gbISCBvoVQJXCLywrkbokwuxarWYNjVGuza33kaTKWkAaYWf4fcum3/32c/", - "/sCUZt+DMXwFr3h+wUDmqoDiiJ0umVQ2Ig1PS4hD9+XYOjxcqUv+70Y5mtiYVcXzi/SNXoqNSKzqe74V", - "m3rDZL1ZgHZbGq4Qq5gGW2s5BhCNeIAUN3w7nPRc1zLH/W+n7chyjtqEqUq+Q4Rt+PYvD+ceHMN4WbIK", - "ZCHkitmtHJXj3NyHwcu0qmUxQcyxbk+ji9VUkIulgII1o+yBxE9zCB4hrwdPK3xF4IRBRsFpZjkAjoRt", - "gmbc6XZPWMVXEJHMEfvJMzd8atUFyIbQ2WKHjyoNl0LVpvloBEacer8ELpWFrNKwFAkaO/PocAyG3vEc", - "eONloFxJy4WEwjFnBFpZIGY1ClM04X59Z3iLL7iBL56O3fHt04m7v1T9Xd+745N2G1/K6Egmrk731B/Y", - "tGTV+X6CfhjPbcQqo58HGylW5+62WYoSb6K/u/0LaKgNMoEOIsLdZMRKcltrePZWPnB/sYydWS4Lrgv3", - "y4Z++r4urTgTK/dTST+9VCuRn4nVCDIbWJMKF362oX/ceGl2bLdJveKlUhd1FS8o7yiuix07fTG2yTTm", - "dQnzpNF2Y8XjfBuUket+YbfNRo4AOYq7irsXL2CnwUHL8yX+s10iPfGl/s39U1Wl+9pWyxRqHR37KxnN", - "B96scFJVpci5Q+Jr/9g9dUwASJHg7RvHeKE+ex+BWGlVgbaCBuVVlZUq52VmLLc40r9rWM6ezf7tuLW/", - "HNPn5jia/KX76gw/ciIriUEZr6prjPHKiT5mD7NwDBofIZsgtodCk5C0iY6UhGPBJVxyaY9alaXDD5oD", - "/MbP1OKbpB3Cd08FG0U4oxcXYEgCphfvGRahniFaGaIVBdJVqRbND5+dVFWLQXx+UlWED5QeQaBgBlth", - "rLmPy+ftSYrnOX1xxL6Nx0ZRXMly5y4HEjXc3bD0t5a/xRrbkl9DO+I9w3A7lT5yWxPQ4MT8u6A4VCvW", - "qnRSz0FacS//1b8bk5n7fdLH/xwkFuN2nLhQ0fKYIx0Hf4mUm896lDMkHG/uOWIn/W9vRjZulDTB3IhW", - "9u4njbsHjw0KrzSvCED/hO5SIVFJo5cI1lty04mMLglzdIYjWkOobnzWDp6HJCRICj0YvipVfvFXbtZ3", - "cOYXYazh8cNp2Bp4AZqtuVkfzVJSRny82tGmHDH3Iir4bBFNddQs8a6Wd2BpBbc8WpqHNy2WEOrxO2R6", - "oBO6y4/4H14y99idbcf6adgjdo4MzNBx9k6Gwmn7pCDQTO4FtEIotiEFnzmt+1pQPm8nT+/TpD36mmwK", - "fof8IpodOt+KwtzVNuFgY3sVC6inL0ijs7AxCa2tWRXXmu/Sa6e5piDgXFWshEso+yAQy8LRCCFqe+d8", - "4Su1TcH0ldoOeILawp3shBsH5eqA3QPwvfCQKX0Y8zj2FKS7BTpZ3iB7kLEI5GZprdUnC6Vvxo57fFay", - "1gbPuBs1uo3mPSThq3WV+bOZsOPRC72BWrfnfi7aHz6FsQ4Wziz/HbBg3Kh3gYXuQHeNBbWpRAl3QPrr", - "5C244AaePGZnfz35/NHjXx5//oUjyUqrleYbtthZMOwzr6wyY3cl3B+uDNXFurTp0b94Giy33XFT4xhV", - "6xw2vBoORRZhkgnpNebeG2Kti2ZcdQPgJI4I7mojtDNydjjQXgjjRM7N4k42YwxhRTtLwTwkBRwkpusu", - "r51mFy9R73R9F7o9aK108uqqtLIqV2V2CdoIlXAvvfJvMP9GkPer/u8ELbvihrm50RZeS5SwEpRlt3I6", - "36ehz7eyxc1ezk/rTazOzztlX7rID6ZVwyrQmd1KVsCiXnVUw6VWG8ZZgR/iHf0tWJJbxAbOLN9UPy6X", - "d6M7KxwoocOKDRg3E6M3nNRgIFeSQkMOqKt+1Cno6SMm2CztOAAeI2c7maPh9S6O7bgmvxESvUBmJ/NI", - "rXcwllCsOmR5e/V9DB001T2TAMeh4yU+RsvPCygt/0bp81bs+1arurpzIa8/59TlcL8Yb1sq3LfBqCDk", - "quyGI60c7EepNX6SBT0Px9evAaFHinwpVmsb6VmvtFLLu4cxNUsKUHxAWmrpvhnqqj+owjETW5s7EMHa", - "wVoO5+g25mt8oWrLOJOqANz82qSFs5EAFvSco8PfxvKeXZPiuQBHXTmv3WrriqE7e3BftB9mPKcTmiFq", - "zIgzr/HC0ls0HQVHlBp4sWMLAMnUwnvMvC8PF8nRF2+DeONFwwS/6MBVaZWDMVBk3lJ3ELTwHl0ddg+e", - "EHAEuJmFGcWWXN8a2IvLg3BewC7DyBHDPvvuZ3P/E8BrleXlAcTiOyn0NnYP7xYdQj1t+n0E1588Jjuu", - "gYV7hVmF0mwJFsZQeC2cjO5fH6LBLt4eLZeg0UH5u1J8mOR2BNSA+jvT+22hrauReEiv3joJz22Y5FIF", - "wSo1WMmNzQ6xZfdSRwd3K4g4YYoT48AjgtdLbiw51YUs0BZI1wnOQ0KYm2Ic4FE1xI38c9BAhmPn7h6U", - "pjaNOmLqqlLaQpFag4Ttnrl+gG0zl1pGYzc6j1WsNnBo5DEsReN7ZNFKCEHcNr4nH3UyXBx6aNw9v0ui", - "sgNEi4h9gJyFtyLsxjFhI4AI0yKaCEeYHuU0gWjzmbGqqhy3sFktm+/G0HRGb5/Yn9p3h8TFbXtvFwoM", - "hqL59z3kV4RZigZcc8M8HGzDL5zsgWYQ8v4PYXaHMTNC5pDto3xU8dxb8RE4eEjraqV5AVkBJd8NB/2J", - "HjN6vG8A3PFW3VUWMgrrSm96S8khimbP0ArHMynhkeETlrsj6FSBlkD81wdGLgDHTjEnT0f3mqFwruQW", - "hfFw2bTViRHxNrxU1u24pwcE2XP0KQCP4KEZ+uaowI+zVvfsT/GfYPwEjRxx/Ul2YMaW0I5/rQWM2FB9", - "xHx0XnrsvceBk2xzlI0d4CNjR3bEoPuKaytyUaGu8x3s7lz160+Q9LuyAiwXJRQsekBqYBV/zyggqT/m", - "zVTBSba3IfgD41tiOaUwKPJ0gb+AHercryjSNTJ13IUumxjV3U9cMgQ0xM85ETx+BbY8t+XOCWp2DTt2", - "BRqYqRcbYS1FsHdVXauqLB4g6dfYM6P3aiZ9invdrGc4VLS84VbMZ6QT7IfvvKcYdNDhdYFKqXKChWyA", - "jCQEkwJgWKXcrgsfTB/CqQMldYD0TBtd2s31f8900IwrYP+papZziSpXbaGRaZRGQQEFSDeDE8GaOX2o", - "S4shKGEDpEnikwcP+gt/8MDvuTBsCVchA8W92EfHgwdox3mljO0crjuwh7rjdpq4PtDh4y4+r4X0ecrh", - "UAs/8pSdfNUbvPESuTNljCdct/xbM4DeydxOWXtMI9PCTHDcSb6cjst+uG7c9zOxqUtu78JrBZe8zNQl", - "aC0KOMjJ/cRCya8veflj8xlm10DuaDSHLMeckIljwbn7htJIDumGbXid2GygENxCuWOVhhwo7cGJfKaB", - "8YhRQGS+5nKFkr5W9cpH5NE4yKlrQzYVXcvBEElpyG5lhtbpFOf2Udgh88XJQcCdLtY3bZPmccWb+Xyy", - "05QrNUJe39Sf9G7NZ6OqqkPqZauqEnK66TsTuHhHUIvw00480QeCqHNCyxBf8ba4U+A29/extbdDp6Ac", - "ThzFCLYPx8IEnZ5c7u5AWqGBmIZKg8G7JbYvGXqqlnGqnr98zM5Y2AxN8PTpLyPH7/WooqdkKSRkGyVh", - "l8xOFxK+x4fJ44T328jHKGmMfdtXHjrw98DqzjOFGm+LX9zt/gntu5rMN0rflS+TBpwsl09wHR70k/sp", - "b+rg5GWZ8An6RJ4+AzDzpnCA0Iwbo3KBwtZpYeZ00Lwb0Wf9dNH/qglPvoOz1x+35/yKc0TRuAtlxTjL", - "S4GmXyWN1XVu30qOxqVoqYmopaBFj5sbn4dX0vbNhPnRD/VWcoxYa0xOyUiLJSTsK98ABKujqVcrMLan", - "pCwB3kr/lpCslsLiXBt3XDI6LxVoDB06ojc3fMeWjiasYr+BVmxR267Yjnlqxoqy9J44Nw1Ty7eSW1YC", - "N5Z9L+T5FocL3vpwZCXYK6UvGiykb/cVSDDCZOnoqm/pKUYC++WvfVQw1hWgxyHKsk2cnblldnLl/+9n", - "//HszUn2Xzz77WH25f84fvf+6Yf7DwY/Pv7wl7/8v+5PTz785f5//HtqpwLsqSwqD/npC6/Snr5AvaV1", - "3gxg/2iG+42QWZLI4jCMHm2xzzBj2BPQ/a5Vy67hrbRb6QjpkpeicLzlJuTQv2EGZ5FOR49qOhvRs2KF", - "tV5TG7gFl2EJJtNjjTeWooYBiel8RfQm+hREPC/LWtJWBumb0nFCYJhazpucVCpX84xhwuKah6hG/+fj", - "z7+YzdtEw+b5bD7zT98lKFkU21Q6aQHblJLnDwgejHuGVXxnwKa5B8KejIGjoIx42A1sFqDNWlQfn1MY", - "KxZpDheSHLyxaCtPJUW0u/ODvsmdd3mo5ceH22qAAiq7TpWx6Ahq+Fa7mwC9eJFKq0uQcyaO4KhvrCmc", - "vuij8UrgSyyngNqnmqINNeeACC1QRYT1eCGTLCIp+unF8/vL39y5OuQHTsHVn7NxRIa/rWL3vv36nB17", - "hmnuUWYzDR3loiZUaZ9u1YkkctyMiveQkPdWvpUvYCmkcM+fvZUFt/x4wY3IzXFtQH/FSy5zOFop9ixk", - "cL3glr+VA0lrtL5WlDvHqnpRipxdxApJS55UM2U4wtu3b3i5Um/fvhsEVQzVBz9Vkr/QBJkThFVtM1/x", - "IdNwxXXKaWWajH8cmUq67JuVhGxVk2UzVJTw46d5Hq8q08/8HS6/qkq3/IgMjc9rdVvGjFU6yCJOQCFo", - "cH9/UP5i0Pwq2FVqA4b9uuHVGyHtO5a9rR8+fAKskwr7q7/yHU3uKphsXRnNTO4bVXDhpFbC1mqeVXyV", - "8o29ffvGAq9w91Fe3qCNoywZftZJwQ0R9ThUu4CAj/ENIDiunU6Iizujr0J1r/QS8BFuIb7jxI3WY3/T", - "/YqScm+8Xb3E3sEu1XadubOdXJVxJB52pin6s3JCVgijMGKF2qqvj7QAlq8hv/CFa2BT2d2883mI1PGC", - "ZmAdwlBJI0qpw6Ia6FlYAKurgntRnMtdv7qBAWtDPPBruIDduWprclynnEE3u96MHVSk1Ei6dMQaH1s/", - "Rn/zfTgYKvZVFZLUMVsxkMWzhi7CN+MHmUTeOzjEKaLoZH+PIYLrBCKI+EdQcIOFuvFuRfqp5TktY0E3", - "X6K8UeD9zL/SKk8+citeDVrd6fkGsD6aujJswZ3crnxpL8ogj7hYbfgKRiTk2LkzMU+74xDCQQ7de8mb", - "Ti37F9rgvkmCTC9nbs1JSgH3xJEKKjO9eL0wE/kPvWcCK3Z6hC1KFJOawEZiOlx3nGxUgnAMtDQBg5at", - "wBHA6GIklmzW3ISqY1icLZzlSTLA71gRYV8dnNMo1CyqwNZUuQk8t39OB9qlr4YTSuCEujexajmhho2T", - "8DG6PbUdSqIAVEAJK1o4vRwIpa3O0G6Qg+PH5bIUEliWilqLzKDRNePnACcfP2CMLPBs8ggpMo7ARr84", - "Dsx+UPHZlKvrACl9dQkexkaPevQ3pPO+KI7biTyqcixcjHi18sABuA91bO6vXsAtDsOEnDPH5i556dic", - "1/jaQQblWFBs7RVf8ZEZ98fE2T0OELpYrrUmuopusppYZgpApwW6PRAv1DajxM+kxLvYLhy9J0PbMQ01", - "dTCp8M09wxZqi9E+eLVQKPUBWMbhCGBEGv5WGKRX/G7sNidg9k27X5pKUaFBkvHmvIZcxsSJKVOPSDBj", - "5PJZVMvmRgD0jB1tYWiv/B5UUrviyfAyb2+1eVujLWQNpY7/2BFK7tII/oZWmKb6zKu+xJK0U3SDVrqF", - "dyIRMkX0jk0MnTRDV5CBElApyDpCVHaR8pw63QbwxjkLn0XGCyzvw+XufhQJpWEljIXWiB7iJD6FeZJj", - "VUGlluOrs5VeuvW9Vqq5psiNiB92lvnRV4ChxEuhjc3QA5FcgnvpG4NK9Tfu1bSs1I21ohq8okjzBpz2", - "AnZZIco6Ta9+3u9euGl/aFiiqRfIb4WkgJUF1oxORmDumZqCdPcu+CUt+CW/s/VOOw3uVTexduTSneOf", - "5Fz0OO8+dpAgwBRxDHdtFKV7GGSUOTvkjpHcFPn4j/ZZXweHqQhjH4zaCfm7Y3cUjZRcS2Qw2LsKgW4i", - "J5YIG5VcHqa0jpwBXlWi2PZsoTTqqMbMr2XwCIXqeljA3fWDHcAAirSvYQkakiaE5hFFRzfiUlyoEDO7", - "O6VwEps+avzvmtLCRdl0jogmuoERzJeWHN/jNvayU3qxu5RE74LhrLWQ9ounQ4psbPwOlim7cZY2rZ85", - "RaOL+EjdolLmBzZBjCjuMXlG7DmeSpjQiGNItk0O5CHKPQdefge7n927uJzZh/nsdobsFOX7EQ/g+lVz", - "2JJ4xkAJMmx2/FLXRDmvKq0ueZl5c/8Yo9Dq0jMKfD14Bz7yxZOm7POvT16+8uB/mM/yErjOGsFtdFX4", - "XvVPsyoqRjlyQEKhf6eBBw2KBPto85sKerGL4GoNvmJ6pBsMSru27p/oKHqXwTIdr3WQ93lPFS1xj8cK", - "qsZh1RpTyV/V9VHxSy7KYMUM0I7EVuHiptUHTnKFeIBb+7oil2V2p+xmcLrTp6OlrgM8Cef6EUsipaUT", - "6QsmISvyvqsuC7pnPGUd46qPF2rb3p4T7+RvlO4wfx9Yn/R9hQu7zxjv5O72eBwJNQpdOPqC5xFDWmK/", - "rn51p/HBg/ioPXgwZ7+W/kEEIP6+8L+jsejBg6RZMql1OCaBSoXkG7jfBAmObsTHVVElXE27oE8uN4g6", - "jPUeJ8OGQsmJFdB95bF3pYXHZ+F/KaAE99PhBJrephO6Y2CmnKCzsUD6JkZiQ40/DFOyHxKEORyOtJDZ", - "bziWNiYr7/AIyXqDltHMlCJP+4zkwjj2KikWwL3M8OUR5dqNWIuR0BJZi2gs99qUWl09IKM5ksg0yXJh", - "Le4Wyh/vWop/1MBEAdK6Rxrvtd5VF5QDHHUgkDpdaDiXH5g8ju3wt9GZ4rLefZkRgdivMMWRBwNwXzQm", - "wLDQxsLe6kzXDWCKZxww7j3BR54+PDVTMPa6G0EwTY+Z0gAuMDpfX3xkjmRDN2GypVa/Qdpuhea+RAJm", - "KGQuMGrvN4jVs7iNUYelNNbqti9dO/uh7Z6uG49t/K114bDopnb6TS7T9Km+3kbeROk16TKBHsljSljs", - "uuhGto2wFjxeUSwHlq0Obk0u6TxR9mEnQDp9KuNUhGMavz2VHuZB+kbJrxY8VdPb6UIOpmh7Ow5Yq1j4", - "OGyAaVL0aHYWBSA17wqqYFKBbhPQh9XQbqjX0LSTNZpWgUGKilWXOQWNlEYlhqnlFZfUC819R/zKf22A", - "PCbuqyulsf6QSfuKC8jFhpdpBafIh37BQqwEtfmqDUR9pPxA1EKRqMj34moSTz1qTpfs4TxqZud3oxCX", - "wohFCfjGI3pjwQ1el433ovnELQ+kXRt8/fGE19e1LDQUdm0IsUaxRvdEIa+JeFiAvQKQ7CG+9+hL9hnG", - "ehhxCfcdFr0QNHv26Ev01NEfD1O3rG/Tto9lF8iz/+Z5dpqOMdiFxnBM0o96lCzVQn1ax2+HPaeJPp1y", - "lvBNf6EcPksbLvkK0uGFmwMw0be4m+h96eFFFtRk0FitdkzY9PxgueNPIylLjv0RGCxXm42wGx8RYNTG", - "0VPbJIomDcNRx0Jf3z/AFR5iYE0V4gp6tq6PrMbwzUjIMYY//cA30EXrnHEqOlWKNuQtdB1hp6GmHTY8", - "aPocEG7cXG7pKEtiBNySVVpIi/aP2i6zPzu1WPPcsb+jMXCzxRdPE40DurW15fUA/+h412BAX6ZRr0fI", - "Psgs/lv2mVQy2ziOUtxvUwSjUzkaAZSO9RgLONk/9FTJ142SjZJb3SE3HnHqWxGe3DPgLUmxWc+16PHa", - "K/volFnrNHnw2u3QT69feiljo3SqUG173L3EocFqAZcY8J3eJDfmLfdCl5N24TbQf1p3dRA5I7EsnOWk", - "IhCMTvsSvZwI//P3vinxQPYeCU6j6LPmm4+cwJY0WpKE1jGbPfqVaadJojT64AEC/eDB3Atzvz7uPiYm", - "9eBBunxb0nDkfm2xcBu9Dr9N7eFXKmHGCb1SGhe6T1JLmNHGWK174I7ywg81Z92+FB//Lryb8Od0iEv6", - "FLx9+wafBDzgH31EfOIjjxvYBvHRSkYIJerLkySZonkeBddx9pXaTiWcHicNxPMHQNEISiYamXAlg75D", - "SafzwaiHiEbdqAsolVOV4pLqsVX6nwfPbvHzPdiuRVn83BbY6F0kmst8nQxNWrgPf2n7AzdLJFaZrNK8", - "5lJCmRyONLRfgiaX0DX/rqbOsxFy4rv9vle03N7iWsC7YAagwoQOvcKWboIYq93aBU1uXLlSBcN52pLA", - "LXMcNpCLutr8owZjU0cDH1B8PrpsHPOlpioMZIE2nCP2LWYRO1g69R7RdhIKcnWL09RVqXgxx0Jh51+f", - "vGQ0K31DXS6pqcsKTQfdVSRtvdOL9TQNK9NZqNPH2Z8W51ZtbNb0YEnV+XBvtF1iRC8AAI0KMXaO2Iuo", - "mT+VBHFDMKwTpzdQRC1fSKNAmnD/sZbnazSUdC6ycZKf3o0oUKWJWqI3rU2bEuB47hzcviER9SOaM2XX", - "oK+EAcw7gkvolhZp6ux4Q10oNdJdnq6lJEo5uoZM0RT8vi7aA3AkkAQPZxKyHuKvqSZTM6/rNmc6w6+S", - "FUn7nZ4GvdCpUEXTsvL70M2eSyVFjvVAUwIRlkGY5jOZUDo17ewwM39CE4cr2V+qyXjwWBztOBUYoUfc", - "0P8YPXWbStRBf1rY+r4DK7DGczYo5qFNmrfOC2nAl3R3RBTzSaUTERYpkSNrvLnXJCPMcB4xt3zjnv3g", - "jXGY+nchJKrdHm1ezCb7OXawt05XF5atFBi/nm6ZF/PGfXOEFU8K2L47Ch3vcQyK6XHLpgC24VAnIZzN", - "h4+5d5+7d30dyubnTmwKTXpSVX7S8SZ66c6hWzmK4FQQRfBqR8htxo9H20Nue+NQ8T51hAaXGEIDFd7D", - "A8JoGsr1urc6FYEoCt9gFI2fLEYlZAKMl0IGf076gsiTVwJuDJ7Xke9MrrklEXASTzsHXjYxM32GZqx3", - "CN52qH4VTocSXGOYY3wb2154I4yjeaEV3LjcsXAoHHVHwsRzXjZxnInOdihVeSGqwOTQXq+7FONwjDt0", - "0+xeAAca6M7bz7Ek7XVvorF6H4u6WIHNeFGkKux/hU8ZPmVFjZIDbCGvm0rsVcVyLG/Xrfc3pDY/Ua6k", - "qTd75gov3HK6qHlkghriBpZhhzGfeLHDf6/T2riJ4Lx2RkcI1yyuV+RymKGSknodTWdGrLLpmMA75fbo", - "aKe+GaG3398ppZdq1QXkUxhJR7hcvEcp/va1uzjiIliDYFm6WpoaVRiYqkIPdFQbm+oqXa6EV9mg2D66", - "YJuWwvvNEOPNged4+Y1kUcUmb7pfyQw8lkuVj6b+ceuLEFjO9rKg0cRuClzsGdGH/oyxYEWKVbw747Nf", - "616EhjjyIUDfhSQVVnHhA1ZaZjHErA/zHaZ7TomjbTe4vwifsjdqH/3uciy9LtS8xef95qEX4CsTVRou", - "hapDKEgIyAwqIf3aacXZJDgm158Mc/7UxudRU/m5b+JEy/Q6+Xc/U/guA2n17g9gOB9s+qAt6VDaJfNU", - "+wpr+n9M6gfSuRWn1INOlR72smGnMeqBtq4DsnoxRRwYtmmdz06La12YqfLVMxoldezSTVfHq3u2FT3x", - "iFXKiLYNT6ob68TI53NsqBpVJx2OFSLiLiG32HupjfTRANepVeomi/q7/6vK54g63QSI++Ke+yp6Dhsu", - "HbjjB0n3UeEIalZzNL1+5UkTz0npKFfcYLVnarHeTeCcnEa2XEJuxeWBIgd/W4OMEujnwS6DsCyjmgei", - "SarAGnnXtzq2AO2rQbAXnqhW9a3BGUuqvYDdPcM61JDsntNkFN2kPBpiALlD5khEmVS8FBmSfQiLMA1l", - "IBZCfCJ9Dm2h2dHGm1HJjhvOFUjSXRxtGY89U6Y7/02ay316reI2mB8wVgdh2DhsXP94gX3aTNMUO5RX", - "i7V0djosQn3ly7NhSYrGdxIKtYEJv4X6MzRLKS4gbg2KnqorrovwRtL0Eqw62Z77aFC8IDS96gO9bGYW", - "bTT50FedKGuKiRl5qZwYkY1lt3QDuJvop3uGwtSoyw6Gpju4lqB9C2WUf0tlILMqRJ/vg2MfKigW70ZI", - "MKOlxAm40QJ/r9sKhthSgWNBP+5D8OIFMg0b7qDTUZ3B8Tn3Ifs5PQ8ZwaGk/kELU0Ovh3s7hTwCYQZI", - "jKl+yfxteTjT+CbGJiEl6Cx4nvpFByXorjek0qqoc7qg44PRGOQml/Tcw0qSdpp8uMqejhBl7F7A7piU", - "oNAUK+xgDDRJTgR6VKyqt8l3an4zKbhXdwLep7RczWeVUmU24uw4HVZK7FP8hcgvoGDupgjxtiONCtln", - "aGNvvNlX612oDFhVIKG4f8TYiaQMh+DY7rbq6E0u79l9829x1qKm4qXeqHb0VqZDxbGsqL4lNwvD7Odh", - "Bhyru+VUNMiBOnzbkSqNml8l2nYeTdXKh67mfivFlqgIipRMckYeq+d40FOGI8zHjgoHoCOTM+/pYqZU", - "qZDMm+SMu6HSmIonQ4AsyCmpyw0UfvAkApo2iQcChZoYobbDXBsnNBSPylJdZXiMsqbObErpcu+Z7jUR", - "Suu33zl6W0AUccSNFyF2bM0LliutIY+/SKdFEVQbpSErFQYgpXyjS+skwg3mQkhWqhVTlVP0qV5z8CIl", - "+x8O5qql5HihQxTvkUQBz3PUPhXz37Dmm6lT3lV7SSp+QovOyMs2EhIJxhc78Riil4fw7unweK1KyadL", - "tFUIjMLo5raSXBT3uYRrtrkUZRlU2bFOl+wnU2OgDCY2uCmeso1y+jDqHKHheRiqDT76LFfSalWWXfME", - "CWsrb3P9nm9P8ty+VOpiwfOL+6jhYJ/9kHw2D2l//TCxdibdq3gzsSXn+TphgcRZwqm7dt9Nzzmu3S4v", - "AnMCxzpsfT1JtRXtrqvf4Has3bRVG5GnafifK+5qNFoqxRKSpXSoYwUlP+NryKjjy6FxsyNLGqIZpCPY", - "1H55nubdjcg83H9RFuuPy5bgL4mRi2nIJ/19muWjt34PAISUMvJsranNRXwnN1xFrSiDF52lfUAncnGM", - "SbkdbG6EOwfKwq2AGsTBNQB+RmronEoeUUzdQm3D8/ttTaQbAf9hP5WnWgMnTnFDWr5zcaifMMIRkqE6", - "+yNjqF38Ymp8TNOSaOKNGgEwHjHTgWFS3Mx1wVhyUUKRcTtyuaO1Yh7pXD7Xot9oThjPyXNOF/YamBu7", - "1uDz+alPfK8xbcUdKanm9aFNURawBYPJ9tRdkxuygAdLvG9S31cLVZWVcAmdQCJfZKBG0U5cQtzgnj5m", - "BUCFfqm+tSQVIRPf5T0V2q89i2IspmA3qVMTYmmn2AGFOaneb2VGx8RMPUoOoktR1LyDP3OLVt/jXb4H", - "MnlGsjcdiCnT/EQjvA4DnITvU6JMwMS7aXzo2iwojbp9DOhgxByeqOSpl+mAubiCRmNqx9mKxiVHJN7y", - "DVPxKzlumhqSfKveTG/BHyH26y3kKNV0I8JujxOGgzHTq44zKoLrZodvbuL8JDS8l4RHx0upGgaQwbYa", - "buuACOto6MIL7PgCthaTTux1UjO28/D83/O/OXZDpoGcXk3dRWIN7gUEXxIW7G3M6F6gFc2FFiLf5r5e", - "W18pF1HM74bvmNL4j9PX/lHzUix3eEIJ/PAZM2vuSMg7r8ir6iPp3MT7BZN5ACzYBVSYitYtpo4ZDbdz", - "o0RAuyuQKe39IBt+AfE2oMOYOE9uHcsx9WIjjMHLrredQyz4xYec+w0vYh0ZK39127qFWpDu6//Z5hPF", - "U4WCPVXJ87ZNs+GbnqmW+kUF4rJr2OxPOBuqx4EEmh5ULdHqkGhaUD0Ywl9T/AElEfzPQljN9W5P+OvB", - "mIJUFDdKzofAHvTmQTH8zpZxnWaRbc7unlS9SUu5612YGrkwABrdn6Fq0gHwqdpdqLD0MfCfLMo3towp", - "4P9R8D7S0iiGl7oXfQQsd5LRE7CSXXWhtpmGpTnkpCfDqlOEdZvGHoyTQuYauKGohdMfvcrW1pwT0qmQ", - "FFfX+IWaUQpYCtkySyGr2iY0ACw9J3cRwmLzNKJ1xA0xJiU4MeySlz9egtaiGNs4dzqopUpc8zuY5P23", - "CeW/uVOHAwjTaj+Y4wZtDlX0mrvAC7FcgqaQN2O5LLgu4teFZDlod++zK74zN/d9OGh17eSLA94PHkkz", - "3czryA+CpE2AlDvvWLulZ6IBkN+hi2KCawFjKxNuBTKKWDXiSRjCkE7459usVCvMfBohQF/cD30/pKwo", - "iQZbkoeuN48Rv8H+abCusT/4VuGsU6bYf85+RNShwvOTFHbvSSNrWj8VjWIF6SAE+perNmCZNmdI/6ns", - "wXMMr+9kEPYbAIe9psAFmg9GPBldC+7ILqLr1qeexuZaM92T0fEOp3IUSYfNULc1e0KSwbThtzz3ISVD", - "o89AKSakzH2G5zVtQmRJDvfACHjUNdCfre60jZvfjTNd1oh82mmIKlVl+ZQ4NSp9XniDtoe0C+MIfUTm", - "6pF1Ny79tpF1p+RGpysASco3EXd7XQkO+WWqfJ+SPWbQGOGgXWO5WiIvwyNMZhzMPmiMF/N+XkzXYNMw", - "CcaZhrzWaNC84rvDfVtGSm6e/fXk80ePf3n8+RfMvcAKsQLTlm3t9T1pY5mE7NtZPm700mB5Nr0JIWOa", - "EBc8ZSERpNkUf9aI25LkJpNdX65jCU1cAKn+3sN+GzfaKxynDUf+Y21XapF3vmMpFPw+e+ZjLtMLOJFe", - "f1FLtp9ntI6RcNwT/MIJ/4lLKmztDRY4Zo8dz9i9CT22Btk/DBUmUpDvjPaa5f4eFJeUMm/WynASaMN0", - "1AR5IAAjeWadDKG402lbSVGTbRetwMFh1r/Evm8daQcDohGS8MEB8OLEsfa9JobXg/OJSxJ+3yAlWsq7", - "MUroLP9QLppfYOt5jLbIq7rWAvWdpsJK3X2JEg3N8yZ/b0S2HaT5YVtTp9+UZSI9kLRvPFMx4TjBUl/y", - "8uNzDex3e4L4gOL1eFJAnCMWI5lQaW5WoeolnzR3lA92d1PLV5iS+Ddwe5S85/xQ3uk4uM3QdsJLCt9c", - "+vRuNyS7wjEpqOTRF2zha15XGnJh+s5M8jhFUYGXoMXSB/DB1h7IwTq0zp+VvQUZL0PkAfshckooNP60", - "ELZH9BMzlZGTm6TyFPUNyCKBvxSPinvkHbguLjqFDlpZPLrRlIY7LngQlS66ZsGDYfe/qcujpH536dQG", - "huucfFt3cJu4qNu1Ta3WMblA9du3b+xiSpGNdDFp9zlW+biTqtLXqin9O9T3IBz5Mfy8KYr5eaziI1U1", - "HCku2tuPWpQHwww6pWI/zGcrkGCEwWKov/gS7h/3Lg0QUM7x8KgSrLcplECISay1M3k0VVQEdkL9V/9Z", - "otor5vPktRZ2h+37ghlG/JKsRPJtk9XuqyI0HhB/91l1AU0L1TYHvjbhdv1W8RLvI3LMSHcLqfKIfb3l", - "m6r0RkX2l3uLP8GTPz8tHj559KfFnx9+/jCHp59/+fAh//Ipf/Tlk0fw+M+fP30Ij5ZffLl4XDx++njx", - "9PHTLz7/Mn/y9NHi6Rdf/ume40MOZAI01CZ+Nvs/2Um5UtnJq9Ps3AHb4oRX4jtwe4O68lJheymH1BxP", - "Imy4KGfPwk//K5ywo1xt2uHDrzPfJmG2trYyz46Pr66ujuJPjleY9JpZVefr4zAPNv3pyCuvTpuYZIqe", - "wB1tbZC4qZ4UTvDZ66/PztnJq9OjlmBmz2YPjx4ePfIdJiWvxOzZ7An+hKdnjft+7Ilt9uz9h/nseA28", - "xBoR7o8NWC3y8EgDL3b+/+aKr1agjzDsnH66fHwcxIrj9z7598O+Z8exY/74fSdHujjwJTqVj9+HPnP7", - "3+70GPPxPNEHE6HY99rxAqvyT30VTPTy+FJQ2TDH71FcHv392Ns80g9RbaHzcBwKCaTf7GDpvd06WA98", - "sRVFtJKc23xdV8fv8T9IvRHQVGTu2G7lMfrfjt931uofD9ba/b39PH7jcqMKCMCp5ZL67+17fPye/o0m", - "gm0FWjixEAs7+F+pAM8xtmHZDX/eSe+9KiFVNuEnaYDU1lD0eifzNuemOdCnRXj5bCfzIL+GkDI8po8f", - "PqTpn+J/Zr7BQ6+4wLE/jxNbvXfLuiET7BnOGngpswjs0QxhePTxYDiVFEbmuCJx7w/z2ecfEwunTqOX", - "vGT4Jk3/5CNuAuhLkQM7h02lNNei3LGfZBMJFzWNS1HghVRXMkDurv56s+F6hyL1Rl2CYb4fXUScTIMT", - "Yshbjh7dlobx7uErg/6nelGKfDanIn7vUGyyKQkiWHOGMwVLVjt491R8e/BMTN+FrmC6p2rCJDgP5NPS", - "8EOperi/Ye/7HjWa6l5qg2b/YgT/YgR3yAhsreXoEY3uLyz9A5XPrct5voZ9/GB4W0YX/KxSqQzysz3M", - "whfYH+MVZ11e0UZqzZ69mdZGyLsfyLJcgBG+FTlqFU5kboV+3XCkcOYx+ina6319Pj+8+0Pc78+5DOe5", - "s+NUfYLrUoBuqIDLYc+Df3GB/zZcgJq3cNrXObNQliY++1bh2SdXjK/oJslFNpEPdArwtcJ05+fj950/", - "uwqRWde2UFfRt2hQJ2/QUHdwD2vT//v4igubLZX21dywI/HwYwu8PPatG3q/ttWSB0+wBHT0Y5ydlvz1", - "mHslIvWs6biffNhXZFNPvSI38lIIDQ2PW6NWbCRC7tmYh968c7wLW416xtraPJ4dH2OuwFoZezz7MH/f", - "s4fED9815BJ6i80qLS6xePa7D/8/AAD//+ddbZxf8AAA", + "H4sIAAAAAAAC/+x9/ZPbNrLgv4LSe1X+OHHGX8lufLX1bmIn2bnYicszyd57Hl8CkS0JOxTABUCNFJ//", + "9ys0ABIkAYmaUezsq/3JHpEEGo1Go7/7wyQXq0pw4FpNnn+YVFTSFWiQ+BfNc1FznbHC/FWAyiWrNBN8", + "8tw/I0pLxheT6YSZXyuql5PphNMVtO+Y76cTCf+omYRi8lzLGqYTlS9hRc3AeluZt5uRNtlCZG6IMzvE", + "+cvJxx0PaFFIUGoI5Y+83BLG87IugGhJuaK5eaTIDdNLopdMEfcxYZwIDkTMiV52XiZzBmWhTvwi/1GD", + "3AardJOnl/SxBTGTooQhnC/EasY4eKigAarZEKIFKWCOLy2pJmYGA6t/UQuigMp8SeZC7gHVAhHCC7xe", + "TZ6/myjgBUjcrRzYGv87lwC/QaapXICevJ/GFjfXIDPNVpGlnTvsS1B1qRXBd3GNC7YGTsxXJ+R1rTSZ", + "AaGcvP32BXn69OlXZiErqjUUjsiSq2pnD9dkP588nxRUg388pDVaLoSkvMia999++wLnv3ALHPsWVQri", + "h+XMPCHnL1ML8B9GSIhxDQvchw71my8ih6L9eQZzIWHkntiXj7op4fyfdVdyqvNlJRjXkX0h+JTYx1Ee", + "Fny+i4c1AHTerwympBn03aPsq/cfHk8fP/r4b+/Osv9yf37x9OPI5b9oxt2DgeiLeS0l8HybLSRQPC1L", + "yof4eOvoQS1FXRZkSde4+XSFrN59S8y3lnWuaVkbOmG5FGflQihCHRkVMKd1qYmfmNS8NGzKjOaonTBF", + "KinWrIBiarjvzZLlS5JTZYfA98gNK0tDg7WCIkVr8dXtOEwfQ5QYuG6FD1zQHxcZ7br2YAI2yA2yvBQK", + "Mi32XE/+xqG8IOGF0t5V6rDLilwugeDk5oG9bBF33NB0WW6Jxn0tCFWEEn81TQmbk62oyQ1uTsmu8Xu3", + "GoO1FTFIw83p3KPm8KbQN0BGBHkzIUqgHJHnz90QZXzOFrUERW6WoJfuzpOgKsEVEDH7O+TabPv/vvjx", + "ByIkeQ1K0QW8ofk1AZ6LAooTcj4nXOiANBwtIQ7Nl6l1OLhil/zflTA0sVKLiubX8Ru9ZCsWWdVrumGr", + "ekV4vZqBNFvqrxAtiARdS54CyI64hxRXdDOc9FLWPMf9b6ftyHKG2piqSrpFhK3o5i+Ppg4cRWhZkgp4", + "wfiC6A1PynFm7v3gZVLUvBgh5mizp8HFqirI2ZxBQZpRdkDiptkHD+OHwdMKXwE4fpAkOM0se8DhsInQ", + "jDnd5gmp6AICkjkhPznmhk+1uAbeEDqZbfFRJWHNRK2ajxIw4tS7JXAuNGSVhDmL0NiFQ4dhMPYdx4FX", + "TgbKBdeUcSgMc0aghQbLrJIwBRPu1neGt/iMKvjyWeqOb5+O3P256O/6zh0ftdv4UmaPZOTqNE/dgY1L", + "Vp3vR+iH4dyKLTL782Aj2eLS3DZzVuJN9Hezfx4NtUIm0EGEv5sUW3CqawnPr/hD8xfJyIWmvKCyML+s", + "7E+v61KzC7YwP5X2p1diwfILtkggs4E1qnDhZyv7jxkvzo71JqpXvBLiuq7CBeUdxXW2JecvU5tsxzyU", + "MM8abTdUPC43Xhk59Au9aTYyAWQSdxU1L17DVoKBluZz/GczR3qic/mb+aeqSvO1ruYx1Bo6dlcymg+c", + "WeGsqkqWU4PEt+6xeWqYAFhFgrZvnOKF+vxDAGIlRQVSMzsoraqsFDktM6WpxpH+XcJ88nzyb6et/eXU", + "fq5Og8lfma8u8CMjsloxKKNVdcAYb4zoo3YwC8Og8RGyCcv2UGhi3G6iISVmWHAJa8r1SauydPhBc4Df", + "uZlafFtpx+K7p4IlEU7sizNQVgK2L95TJEA9QbQSRCsKpItSzJof7p9VVYtBfH5WVRYfKD0CQ8EMNkxp", + "9QCXT9uTFM5z/vKEfBeOjaK44OXWXA5W1DB3w9zdWu4Wa2xLbg3tiPcUwe0U8sRsjUeDEfOPQXGoVixF", + "aaSevbRiXv6rezckM/P7qI//OUgsxG2auFDRcpizOg7+Eig393uUMyQcZ+45IWf9b29HNmaUOMHcilZ2", + "7qcddwceGxTeSFpZAN0Te5cyjkqafcnCekduOpLRRWEOznBAawjVrc/a3vMQhQRJoQfD16XIr/9K1fII", + "Z37mxxoeP5yGLIEWIMmSquXJJCZlhMerHW3METMvooJPZsFUJ80Sj7W8PUsrqKbB0hy8cbHEoh6/Q6YH", + "MqK7/Ij/oSUxj83ZNqzfDntCLpGBKXucnZOhMNq+VRDsTOYFtEIIsrIKPjFa90FQvmgnj+/TqD36xtoU", + "3A65RTQ7dLlhhTrWNuFgqb0KBdTzl1aj07BSEa2tWRWVkm7ja7dzjUHApahICWso+yBYloWjWYSIzdH5", + "wtdiE4Ppa7EZ8ASxgaPshBkH5WqP3T3wvXSQCbkf8zj2GKSbBRpZXiF74KEIZGZprdVnMyFvx457fJaT", + "1gZPqBk1uI2mPSThq3WVubMZsePZF3oDtW7P3Vy0P3wMYx0sXGj6O2BBmVGPgYXuQMfGglhVrIQjkP4y", + "egvOqIKnT8jFX8++ePzklydffGlIspJiIemKzLYaFLnvlFWi9LaEB8OVobpYlzo++pfPvOW2O25sHCVq", + "mcOKVsOhrEXYyoT2NWLeG2Kti2ZcdQPgKI4I5mqzaCfW2WFAe8mUETlXs6NsRgphRTtLQRwkBewlpkOX", + "106zDZcot7I+hm4PUgoZvboqKbTIRZmtQSomIu6lN+4N4t7w8n7V/91CS26oImZutIXXHCWsCGXpDR/P", + "9+3Qlxve4mYn57frjazOzTtmX7rI96ZVRSqQmd5wUsCsXnRUw7kUK0JJgR/iHf0daCu3sBVcaLqqfpzP", + "j6M7CxwoosOyFSgzE7FvGKlBQS64DQ3Zo666Ucegp48Yb7PUaQAcRi62PEfD6zGObVqTXzGOXiC15Xmg", + "1hsYSygWHbK8u/qeQoed6p6KgGPQ8Qofo+XnJZSafivkZSv2fSdFXR1dyOvPOXY51C3G2ZYK8603KjC+", + "KLvhSAsD+0lsjZ9lQS/88XVrQOiRIl+xxVIHetYbKcT8+DDGZokBig+sllqab4a66g+iMMxE1+oIIlg7", + "WMvhDN2GfI3ORK0JJVwUgJtfq7hwlghgQc85Ovx1KO/ppVU8Z2CoK6e1WW1dEXRnD+6L9sOM5vaEZoga", + "lXDmNV5Y+5adzgZHlBJosSUzAE7EzHnMnC8PF0nRF6+9eONEwwi/6MBVSZGDUlBkzlK3FzT/nr069A48", + "IeAIcDMLUYLMqbwzsNfrvXBewzbDyBFF7n//s3rwGeDVQtNyD2LxnRh6G7uHc4sOoR43/S6C608ekh2V", + "QPy9QrRAabYEDSkUHoST5P71IRrs4t3RsgaJDsrfleL9JHcjoAbU35ne7wptXSXiIZ16ayQ8s2GccuEF", + "q9hgJVU628eWzUsdHdysIOCEMU6MAycEr1dUaetUZ7xAW6C9TnAeK4SZKdIAJ9UQM/LPXgMZjp2be5Cr", + "WjXqiKqrSkgNRWwNHDY75voBNs1cYh6M3eg8WpBawb6RU1gKxnfIsiuxCKK68T25qJPh4tBDY+75bRSV", + "HSBaROwC5MK/FWA3jAlLAMJUi2hLOEz1KKcJRJtOlBZVZbiFzmrefJdC04V9+0z/1L47JC6q23u7EKAw", + "FM297yC/sZi10YBLqoiDg6zotZE90Axivf9DmM1hzBTjOWS7KB9VPPNWeAT2HtK6WkhaQFZASbfDQX+y", + "j4l9vGsA3PFW3RUaMhvWFd/0lpJ9FM2OoQWOp2LCI8EnJDdH0KgCLYG4r/eMXACOHWNOjo7uNUPhXNEt", + "8uPhsu1WR0bE23AttNlxRw8IsuPoYwBO4KEZ+vaowI+zVvfsT/GfoNwEjRxx+CRbUKkltOMftICEDdVF", + "zAfnpcfeexw4yjaTbGwPH0kd2YRB9w2VmuWsQl3ne9geXfXrTxD1u5ICNGUlFCR4YNXAKvye2ICk/pi3", + "UwVH2d6G4A+Mb5HllEyhyNMF/hq2qHO/sZGuganjGLpsZFRzP1FOEFAfP2dE8PAV2NBcl1sjqOklbMkN", + "SCCqnq2Y1jaCvavqalFl4QBRv8aOGZ1XM+pT3OlmvcChguUNt2I6sTrBbvgue4pBBx1OF6iEKEdYyAbI", + "iEIwKgCGVMLsOnPB9D6c2lNSB0jHtNGl3Vz/91QHzbgC8p+iJjnlqHLVGhqZRkgUFFCANDMYEayZ04W6", + "tBiCElZgNUl88vBhf+EPH7o9Z4rM4cZnoJgX++h4+BDtOG+E0p3DdQR7qDlu55HrAx0+5uJzWkifp+wP", + "tXAjj9nJN73BGy+ROVNKOcI1y78zA+idzM2YtYc0Mi7MBMcd5cvpuOyH68Z9v2CruqT6GF4rWNMyE2uQ", + "khWwl5O7iZng36xp+WPzGWbXQG5oNIcsx5yQkWPBpfnGppGYcRhn5gDbENKxAMG5/erCfrRHxWyj9Nhq", + "BQWjGsotqSTkYLMnjOSomqWeEBtXmS8pX6DCIEW9cIF9dhxk+LWyphlZ88EQUaFKb3iGRu7YBeCCuX0C", + "jRGngBqVrm8htwrMDW3mczlTY27mYA/6HoOok2w6SWq8BqnrVuO1yOlmAY24DDryXoCfduKRrhREnZF9", + "hvgKt8UcJrO5v4/Jvh06BuVw4iDUsH2YijY06na5PYLQYwciEioJCq+o0Eyl7FMxDzP+3B2mtkrDamjJ", + "t5/+kjh+b5P6ouAl45CtBIdtNMmdcXiND6PHCa/JxMcosKS+7esgHfh7YHXnGUONd8Uv7nb/hPY9Vupb", + "IY/lErUDjhbvR3gg97rb3ZS39ZPSsoy4Fl0+UJ8BqGlTf4BJQpUSOUOZ7bxQU3vQnDfSJQ910f+miXI+", + "wtnrj9vzoYWppmgjhrIilOQlQwuy4ErLOtdXnKKNKlhqJPjJK+Npq+UL/0rcTBqxYrqhrjjFwLfGchUN", + "2JhDxEzzLYA3Xqp6sQCle7rOHOCKu7cYJzVnGudameOS2fNSgcQIpBP75opuydzQhBbkN5CCzGrdlf4x", + "3U1pVpbOoWemIWJ+xakmJVClyWvGLzc4nHf6+yPLQd8Ied1gIX67L4CDYiqLB2l9Z59iQLFb/tIFF2N5", + "AvvYB2u2+bcTs8xOyv3/vf8fz9+dZf9Fs98eZV/9j9P3H559fPBw8OOTj3/5y//r/vT0418e/Me/x3bK", + "wx5LxnKQn790mvH5S1R/Wh/QAPZPZv9fMZ5FiSyM5ujRFrmPiceOgB50jWN6CVdcb7ghpDUtWWF4y23I", + "oX/DDM6iPR09qulsRM8Y5td6oFJxBy5DIkymxxpvLUUN4xrjaY/olHSZjHhe5jW3W+mlb5vV4+PLxHza", + "pLbaqjfPCeY9LqkPjnR/Pvniy8m0zVdsnk+mE/f0fYSSWbGJZaUWsInpiu6A4MG4p0hFtwp0nHsg7NFQ", + "OhvbEQ67gtUMpFqy6tNzCqXZLM7hfK6Eszlt+Dm3gfHm/KCLc+s8J2L+6eHWEqCASi9j1TA6ghq+1e4m", + "QC/spJJiDXxK2Amc9G0+hdEXXVBfCXSOVRlQ+xRjtKHmHFhC81QRYD1cyCjDSox+emkB7vJXR1eH3MAx", + "uPpzNv5M/7cW5N5331ySU8cw1T2bIG2HDlJaI6q0y9rqBCQZbmZrAFkh74pf8ZcwR+uD4M+veEE1PZ1R", + "xXJ1WiuQX9OS8hxOFoI894lgL6mmV3wgaSXLdAUpeKSqZyXLyXWokLTkaUuvDEe4unpHy4W4uno/iM0Y", + "qg9uqih/sRNkRhAWtc5c4YhMwg2VMd+XagoH4Mi2MsyuWa2QLWprIPWFKdz4cZ5Hq0r1E4iHy6+q0iw/", + "IEPl0mPNlhGlhfSyiBFQLDS4vz8IdzFIeuPtKrUCRX5d0eod4/o9ya7qR4+eAulk1P7qrnxDk9sKRltX", + "kgnOfaMKLtyqlbDRkmYVXcRcbFdX7zTQCncf5eUV2jjKkuBnnUxeH5iPQ7UL8PhIb4CF4+CsRFzchf3K", + "FwmLLwEf4RbiO0bcaB3/t92vILf31tvVyw8e7FKtl5k529FVKUPifmea2kELI2T5aAzFFqitujJLMyD5", + "EvJrV/8GVpXeTjuf+4AfJ2h61sGUrYxkM/OwNgc6KGZA6qqgThSnfNsvkqBAax9W/BauYXsp2tIeh1RF", + "6Cbpq9RBRUoNpEtDrOGxdWP0N99FlaFiX1U+1x2THj1ZPG/own+TPshW5D3CIY4RRSeJPIUIKiOIsMSf", + "QMEtFmrGuxPpx5ZntIyZvfkiVZI87yfulVZ5cgFg4WrQ6m6frwDLrIkbRWbUyO3CVQiziegBF6sVXUBC", + "Qg59RCPTvTt+JRxk370XvenEvH+hDe6bKMj25cysOUopYJ4YUkFlphf252eybkjnmcDCnw5hsxLFpCY+", + "0jIdKju+OlvJMAVanIBB8lbg8GB0MRJKNkuqfPEyrPHmz/IoGeB3LKywq5zOeRCxFhRya4rleJ7bP6cD", + "7dIV1fGVdHz5nFC1HFEKx0j4GCQf2w7BUQAqoISFXbh92RNKW+Sh3SADx4/zeck4kCwW/BaYQYNrxs0B", + "Rj5+SIi1wJPRI8TIOAAb3es4MPlBhGeTLw4BkrsiFdSPjY754G+Ip4/ZcHAj8ojKsHCW8GrlngNQFzHZ", + "3F+9uF0chjA+JYbNrWlp2JzT+NpBBlVdUGzt1XBxAR4PUuLsDgeIvVgOWpO9im6zmlBm8kDHBbodEM/E", + "JrP5o1GJd7aZGXqPRshjNmvsYNr6OfcUmYkNBg3h1WIjsvfAkobDgxFo+BumkF7xu9RtboHZNe1uaSpG", + "hQpJxpnzGnJJiRNjpk5IMClyuR+UxLkVAD1jR1tf2im/e5XUrngyvMzbW23alnrzyUex4586QtFdSuBv", + "aIVpiti86UssUTtFN/alW78nECFjRG/YxNBJM3QFKSgBlYKsI0Rl1zHPqdFtAG+cC/9ZYLzAKkGUbx8E", + "AVUSFkxpaI3oPk7ic5gnKRYnFGKeXp2u5Nys760QzTVl3Yj4YWeZn3wFGJE8Z1LpDD0Q0SWYl75VqFR/", + "a16Ny0rdkC1bypcVcd6A017DNitYWcfp1c37/Usz7Q8NS1T1DPkt4zZgZYalp6OBnDumtrG+Oxf8yi74", + "FT3aesedBvOqmVgacunO8U9yLnqcdxc7iBBgjDiGu5ZE6Q4GGSTgDrljIDcFPv6TXdbXwWEq/Nh7o3Z8", + "GnDqjrIjRdcSGAx2roKhm8iIJUwHlZuHmbGJM0CrihWbni3UjprUmOlBBg9f766HBdxdN9geDHTj8qJh", + "zp1agS76z9l8TlFAPjUinA0HdLFuIFHLsTmhRS3RqNYJthsWpmwEu5Fr//7nCy0kXYAzjGYWpDsNgcs5", + "BA1B2UdFNLMezoLN5xAaBNVtjFkd4Ppmn2hzhxFEFrca1ozrL5/FyGgP9bQw7kdZnGIitJByE10ODa9e", + "rAr0zqZzSbA1t7CeRjNIv4dt9rPRUEhFmVRtxJizhHb53wG7vl59D1sceW8glgFsz66gmvoWkAZjZsHm", + "kU2caFSgsIYpFn3obOEBO3UW36UjbY2rOpsm/jYsu1OVtbuUuxyM1m9nYBmzGxdxd5k5PdBFfJ+U920C", + "SxjjQnIMRK5wKqZ8j57hVdSkR++j3UugpSdeXM7k43RyN+dU7DZzI+7B9ZvmAo3iGYOfrLOi42s+EOW0", + "qqRY0zJzLrzU5S/F2l3++Lr3+H1iYTJO2ZffnL1648D/OJ3kJVCZNcpYclX4XvVPsypbp3b3VYISi7eK", + "WGU92PymuGbo9rtZgmumEOj7g6rPrUs3OIrODTiPx2Du5X3O+2yXuMMLDVXjhG4dJNYH3fU70zVlpfdM", + "eGgT8ZK4uHGlw6NcIRzgzv7rIAwhOyq7GZzu+OloqWsPT8K5fsRqaXGNg7taasiKnD+aHl16+lbIDvN3", + "yTJRf/bvJ1YZIdviMRE+6Bv09IWpE2IFr18Xv5rT+PBheNQePpySX0v3IAAQf5+531G/ePgw6mqIWhIM", + "k0BDAacreNAE/iY34tOanTjcjLugz9arRrIUaTJsKNQ6pj26bxz2biRz+CzcLwWUYH7an1vX23SL7hCY", + "MSfoIpUc08Q9rWxPIEUE74f5YV6WIS1k9iuKVc+t52Z4hHi9Qm9HpkqWx/3AfKYMe+U2vse8TPDlhMHM", + "jFizRLgYr1kwlnltTBm/HpDBHFFkqmglwRZ3M+GOd83ZP2ogrDBazZyBxHutd9V55QBHHQikRvUczuUG", + "tlEE7fB3sYOEFf/7MiMCsdsIEkYTDcB92Zj1/UIbr1mrMx0alBjOOGDcOwIKHX04arYJFstuVNA4PWZM", + "b0jP6FzrgcQc0V6PTGVzKX6DuC0aTfiR3Gzf44BhJO5vEKpnYYezDktpPFBty8p29n3bPV43Tm38nXVh", + "v+imrcJtLtP4qT5sI2+j9Kp4BVGH5JQSFroju9GqCdaCxyuIz8KK9j5UgXJ7nmxicifpIX4qw/SiUzt+", + "eyodzIOUrJLezGis3L/RhQxMwfZ2giq0IP5jvwGqSbu1s5MgqLB5l9niRhXItjbFsFDiLfUaO+1ojaZV", + "YJCiQtVlagPBSiUiw9T8hnLbJtF8Z/mV+1qB9YKar26ExNJkKh7/UUDOVlFz7NXVuyIf+voLtmC2A2Ct", + "IGgx5way3VUtFbk2fU0yuUPN+Zw8mgZ9Lt1uFGzNFJuVgG88tm/MqMLrsvFINp+Y5QHXS4WvPxnx+rLm", + "hYRCL5VFrBKk0T1RyGuimGagbwA4eYTvPf6K3Mf4LcXW8MBg0QlBk+ePv0Lvu/3jUeyWdR0cd7HsAnn2", + "3xzPjtMxBrDZMQyTdKOeRKs42RbO6dthx2myn445S/imu1D2n6UV5XQB8ZDh1R6Y7Le4m+hR7eGFW28A", + "KC3FljAdnx80NfwpkYZo2J8Fg+RitWJ65aJ8lFgZemr7x9lJ/XC2malr/eHh8g8xWK7ysUI9W9cnVmPo", + "KpFGgCGNP9AVdNE6JdTWoytZG8bqGxKRc1/uEnuhNC1QLG7MXGbpKEtiVOucVJJxjfaPWs+zPxu1WNLc", + "sL+TFLjZ7MtnkZ4i3bL7/DDAPzneJSiQ6zjqZYLsvcziviX3ueDZynCU4kGb9hucymRUXzx+KxVEtnvo", + "sZKvGSVLklvdITcacOo7ER7fMeAdSbFZz0H0ePDKPjll1jJOHrQ2O/TT21dOylgJGath3R53J3FI0JLB", + "GpM44ptkxrzjXshy1C7cBfrPG4LiRc5ALPNnOaoIBB7NXfmbRor/+XVbjBcdqzY5pmcDFDJi7XR2u08c", + "8HWY1a3vv7UxO/gsgbnRaLOd3gdYSYTq2ljc5ptPnM4bNffaPe8YHB//SqTRwVGOf/gQgX74cOrE4F+f", + "dB9b9v7wYbwmZtTkZn5tsXAXjRi/je3h1yJiAPMNqJqAIpeyGzFApi4p88AwwZkbakq6zX4+vRRxnGSQ", + "eMBf/BRcXb3DJx4P+EcfEZ+ZWeIGtiHN6cPebXYWJZmieR6EGlPytdiMJZzeHeSJ5w+AogRKRprncCWD", + "Zm5Rd/3eeJGARs2oMyiFUTLDPhWhPf+fB89m8dMd2K5ZWfzclhvqXSSS8nwZDdScmQ9/aZuuN0u0rDJa", + "+n5JOYcyOpzVbX/xOnBES/+7GDvPivGR7/abCdrl9hbXAt4F0wPlJzToZbo0E4RY7VZyaTKFy4UoCM7T", + "1llvmeOwK2fQKuwfNSgdOxr4wGYrobPLMF/bqYoAL9D6dUK+w5oKBpZOEV20OvnyhN1SXXVVClpMsWzi", + "5Tdnr4id1X5jWwfbTlkLNLp0VxG1ko8vXdZ0AY7n5I8fZ3eSsFm10lnT2CpW9ci80bbeYr3QCTTHhNg5", + "IS+tJUx5O4udhGDxTbmCIuijZXUxpAnzH61pvkQTU+ciS5P8+BZvnipbA3zQL7rpq4DnzsDturzZJm9T", + "IvQS5A1TgFmYsIZuoaWm6pgzcfrCS93lyZpzSyknB8gUTReFQ9HugbMCifcNRyHrIf5AA4PtkHhox7sL", + "/Cpa5rnfPq/nvPVle5o+wK+djTinXHCWY5HlmECERWHGeZtG1KOOu4nUxJ3QyOGKNu1r8r8cFpNt/Dwj", + "dIgbem6Dp2ZTLXXYPzVsXDOXBWjlOBsUU9970vk1GFfg+mQYIgr5pJCR2JRoPHvjBz+QjLDeQ8JQ9a15", + "9oMzY2Ii9DXjaLBwaHNitvU8lIqhg5ETpslCgHLr6Ra9Uu/MNydY/6mAzfuTV2LB8gu2wDFsNJRZtg39", + "Gw515gMBXeCdefeFeddV5W1+7kT12EnPqspNmu5MGm/HvOFJBMfCT3w8QIDcZvxwtB3ktjOCF+9TQ2iw", + "xuAjqPAeHhBG06Wz1xLbqAiWovANYnOToqX5GI+A8Ypx7wmLXxB59ErAjcHzmvhO5ZJqKwKO4mmXQMtE", + "HDvm+llX6l2H6tckNijBNfo50tvYNhhNMI7mhVZwo3xL/KEw1B0IEy9o2UTARtqFolTlhKgCc0R6DURj", + "jMMwbt+iuHsB7OlKPm0/xzrfh95EqepHs7pYgM5oUcTalnyNTwk+9bk+sIG8btpbVBXJsdhnt/rpkNrc", + "RLngql7tmMu/cMfpgo68EWoIuwL7HcbqCrMt/ntIv/gm9vXg/DYf6FocVvJ3mK8Xk3oNTWeKLbLxmMA7", + "5e7oaKe+HaG33x+V0kux6ALyOYykCS4X7lGMv31jLo6wJOAgzNheLU3FPgzpFfjcF7loak11uRJeZYMO", + "Jui8bvq07zZDpDuuT/HyS+SUhiZve79aM3AqszRPJkJT7UqyaEp2sqBkmQsb8tkzog89QakwTxvleTzj", + "s1vrToSmXTDfdxwuNtSnZRZJR8vtfCHtBh/qDPl+nUo29hXA8Xm/I/M1uDptlYQ1E7UPovGhrF4ltL92", + "+hs36d7R9UcDxD+38TlpKr90nfHsMp1O/v3P1plGgGu5/QMYzgebPuj1PJR2rXmqfYU0TZVGNVnq3Ipj", + "quPHCrE72bDTbXpPr+wBWb0cIw4Me19PJ+fFQRdmrJj/xI4SO3bxTtbpWsdtfWM8YpVQrO1tFmtxPTJm", + "/BK7VAe1modj+VjCNeQaG9q1MVIS4JDKzWYyb7v/V83jtDrdhNa7Use76hsPu9jtueMHJUiCMjq2A9jJ", + "+Gq+Z00krE3kuaEKa99LtHF3U19HJ+DN55Brtt5T8uVvS+BBOZGpt8sgLPOgAgxr0lGwYujhVscWoF0V", + "WXbCE1TuvzM4qXTka9jeU6RDDdGWZE0u1m2KRSIGkDtkhkSEikWaWUOyC/5hqqEMxIKP7LSfQ1t2O9nN", + "OChgdMu5PEmai6MtarRjyng71VFzmU8PKvWFmRWpqjDDboxp/eMlNr9ULs6JNsUmQy2dnA9L8t+4YpVY", + "oKfxnfiylaD8b74al52lZNcQ9ltGT9UNlYV/I2p68VadbMd9NCjl4jsJ9oGeNzOzNg5/6KuOFHnGlJa8", + "FEaMyFJ5Qd3Q9yZu7J6yAX5tHRaEaw7S9aVH+bcUCjItfNz+Ljh2ocJGMd4KCSrZWMEClyx3+rat54oN", + "ZiiWN6UueDFcIJGwogY6GVRdTc+5C9kv7HOfS+0bjOy1MDX0ur/Tnc/AYGqAxJDq58TdlvtztG9jbGKc", + "g8y856lfgpWD7HpDKimKOrcXdHgwGoPc6BIoO1hJ1E6TD1fZ0xGCXOdr2J5aJci3CPQ7GAJtJScLelC6", + "r7fJRzW/qRjci6OA9zktV9NJJUSZJZwd58O6sX2Kv2b5NRTE3BQ+UjnR/ZXcRxt7482+WW59ndSqAg7F", + "gxNCzrjNDfGO7W7jot7k/J7eNf8GZy1qW8rZGdVOrng8yB6LLMs7cjM/zG4epsCwujtOZQfZU5V0k6hZ", + "K+lNpBfyyVitfOhq7venbYnKQhGTSS6sx+oFHvSY4Qgz2YOSC+jIpMR5uogqRSwk8zbZ9maoOKbCyRAg", + "DXxM0ncDhRs8ioBox9XIKbQVzFztMjEnElon8m2LuA2bw8Y0+v7MzSxdfjcXEjptXs3XQhZe5GGq7cdM", + "5YxpSeX2NqXWBs1pB9aTJJb3hmM1kVjtQtporCEOy1LcZMissqa2eUy1Ne+p7mXs27m035lTPYMgrosq", + "J6htyZIWJBdSQh5+EU/bs1CthISsFBjmFfNAz7WRu1eYq8NJKRZEVLkowPYIiFNQaq6ac4piEwRRNVEU", + "WNrBpE/7TUDHI6c8VmdkW5zHLjqzvsxE4CkoV4zHYci+PIR3R1fhg6rzn8/RIsQw1qWbe22lz7C3MhzY", + "WpmVpTcYpLork59UjeFImHhjpnhGVkJpp9nZkVQzVBvidT8XXEtRll0jkBWJF86y/ZpuzvJcvxLiekbz", + "6weoR3Khm5UWU5+W2g/Ga2eSvYpMI9tAXy4jdl6cxZ+6g3s9O85xcIvWAMz3+znWfhv3WayVdXdd/d7s", + "PFE7U4sVy+M0/M8V3ZaMSYuxhGipJ9slySbn42vIqMPLoQlmQJY0RDNwQ7Cx/XI8zTl1kXmY/6LE2x+X", + "zMFdEomLacgnndSS5UnZqgcAQmozRnUtbWulUPJpuIpY2AxzdEn3AR3JxTHy526wmRGODpSGOwE1iDZs", + "ALxvlf2pLcllIxdnYuOfP2hrdt0K+I+7qTzWjj5yihvSct3yfX2PBEeIVwbeGX+EjcP9Dbo/Cqlpgzfy", + "Rg0ASMcldWAYFZ10KBhzykooMqoTlzvahKaBZusyWvrNTZlynDyn9sJeAjFj1xJcvQkrUveaoVfUkJJo", + "Xh9abnkBG1BYDMJ2dKbK+hm8vwNK21aqp3yLKithDZ1wLVcEo0bRjq3Bf6uaj0kBUKH3r2+TisUhhXd5", + "z1Dh1p4FkSxjsBu1XFjE2p0ie8wSUSPKhmf2mKixR8lAtGZFTTv4U4eKHF2zmznKEVQNZPLM621jp/nJ", + "jvDWD3Dmv4+JMh4T78fxoYNZUBx1uxjQ3rjEWqVOPY+HJYYVXhqHBs5WNI5PS+It31AVveFpA+CQ5Fv1", + "ZuQ+McEDxH6zgRylmm7c3d1xQnAwonrVm5IiuGx2+PaG5M9CwztJODleTNVQgAx2p6XG04UT2PEFbGfJ", + "jdhrpGZsIeX4v+N/U+zAbwcyerXtaBVqcC/Be+ywoHTjrHACLWsuNB9fOHX1BPtKOQsiq1d0S4TEf4y+", + "9o+almy+xRNqwfefEbWkhoSci9D6rl28opl4t2Ay9YB5u4DwU9l1s7FjBsNtzSgB0OYKdMYprAx0DeE2", + "oFvecp5cG5aj6tmKKYWXXW87h1hwi/c1IVa0CHVkrEzXbSXqa5War/9nm7UVTuULSlUlzX3/MiCKrnoG", + "cduj0BOXXsJqd1rfUD32JND0PWyJVvp03uIWxr0DIzdisfKpfg8dsAf94AatLu60jEMaFLeZ0TsSIkct", + "5di7MDY+ZAA0Opl9Va894NtqjL4C2KfAf7RoZGoZY8D/o+A90UYvhNd2zPsEWO6k/EdgtXbVmdhkEuZq", + "XyiENawaRVi2xQK8cZLxXAJVNjbk/EensrU1ERk3KqSNXmy8b80oBcwZb5kl41WtIxoAlkbk2wBhoXka", + "0Zpw9qSkBCOGrWn54xqkZEVq48zpsG28wpr03iTvvo0o/82dOhyAqVb7wUxCaDPVgtfMBW673tjAQqUp", + "L6gswtcZJzlIc++TG7pVt/d9GGhlbeSLPd4PGkgz3fz2wA+CpG0BKbfOfXlHz0QDID2ii2KEawEjWCNu", + "BWsU0SLhSRjCEC+rQDdZKRaYX5YgQFd8En0/VlkRHA22Vh46bB7FfoPd02DdbXfwtcBZx0yx+5z9iKhD", + "hecnzvTOk2ataf2EPxuRaQ+Cp3++aMPC7eYM6T+Wo3mJSQydPM1+03m/1zY8xM4HCU9G14Kb2EV0kLsE", + "39BcO76fUdcHH8sEtTpshrqt2hH4DaoNcqa5C9wZGn0GSrFFytTl0R5oE7KWZH8PJMCznWrd2epO2wRT", + "mHEOaQK1O3M2q0SV5WOiAW1p/sIZtB2kXRgT9BGYqxPrbgInVNOsolPYpNO14tA+WMmuGfv8MlW+S8lO", + "GTQSHLRrLBdz5GV4hK0ZB3M8GuPFtJ991DXYNEyCUCIhryUaNG/odn9foURJ2Iu/nn3x+MkvT774kpgX", + "SMEWoNqywr2+PG3EGON9O8unjREbLE/HN8HnpVvEeU+ZT7dpNsWdNcttVVszcNCV6BBLaOQCiBzHSD+Y", + "W+0VjtMGff+xtiu2yKPvWAwFv8+eucjW+ALOuNNfxJzs5hndnn86zi+M8B+5pPzW3mKBKXtsOi/6NvTY", + "GmT/MFQYSfQ+Gu01y/09KC4qZd6ufe4o0IZJvxHyQAAS2XydPKywu3Zbr1Ja2y5agb3DrH+JvW4daXvD", + "zhES/8Ee8ML0vPa9JlLagfOZCz++bpASLOV9ihI6y9+X8ecW2Hoegy1yqq7WoCxbEkPhIkjnVC+aLMmE", + "bDtIpsRW2ka/KctIEqbVvvFMhYRjBEu5puWn5xrYY/0M8QHF23TqRZiJFyLZolLdrg7YKzpq7iDr7nhT", + "8zeY+Pk3MHsUvefcUM7pOLjN0HaCjY0X/lawuaTkBse0QSWPvyQzV5O9kpAz1XdmWo9TEBW4BsnmLoAP", + "NnpPptu+df4s9B3IeO4jD8gPgVNCoPGnhbA9op+ZqSRObpTKY9Q3IIsI/mI8KuzhuOe6uGP97tuVlQgK", + "RB1YVmLYnXLs8mzpBHPp1AqG6xx9W3dwG7mo27WNrYkyugz41dU7PRtTyiRestt8jrVUjlK7+6DK3b9D", + "FRWLIzeGmzdGMT+n6mra2pGJEq69/ahZuTfMoFOQ9+N0sgAOiiksOfuLazHwae9SD4HN7B4eVQvrXcpR", + "WMRE1tqZPJgqKLU7osqu+yxSUxezpvJaMr3F9pLeDMN+idZ7+a6pHeBqTzQeEHf3aXENTYvfttJArfzt", + "+p2gJd5H1jHDzS0kyhPyzYauqtIZFclf7s3+BE///Kx49PTxn2Z/fvTFoxyeffHVo0f0q2f08VdPH8OT", + "P3/x7BE8nn/51exJ8eTZk9mzJ8++/OKr/Omzx7NnX371p3uGDxmQLaC+AvTzyf/JzsqFyM7enGeXBtgW", + "J7Ri34PZG9SV5wLbnxmk5ngSYUVZOXnuf/pf/oSd5GLVDu9/nbg2HpOl1pV6fnp6c3NzEn5yusDU4kyL", + "Ol+e+nmwKVVHXnlz3sQk2+gJ3NHWBomb6kjhDJ+9/ebikpy9OT9pCWbyfPLo5NHJY9cBldOKTZ5PnuJP", + "eHqWuO+njtgmzz98nE5Ol0BLrMRh/liBliz3jyTQYuv+r27oYgHyBMPO7U/rJ6derDj94FKsP+56dho6", + "5k8/dDLRiz1folP59IPvg7j77U4PPBfPE3wwEopdr53OsPfB2FdBBS+nl4LKhjr9gOJy8vdTZ/OIP0S1", + "xZ6HU1+uIf5mB0sf9MbAuueLDSuCleRU58u6Ov2A/0HqDYC2pfxO9Yafov/t9ENnre7xYK3d39vPwzfW", + "K1GAB07M57Y/5K7Hpx/sv8FEsKlAMiMWYvkM96stc3SKbYK2w5+33HmvSogVp/iJK7Bqqy8tvuV5m3PT", + "HOjzwr98seW5l199SBke0yePHtnpn+F/Jq6NRq+Ew6k7j5NxvcG7xfOQCfYMZw28NrMI9MkEYXj86WA4", + "5zaMzHBFy70/TidffEosnBuNntOS4Jt2+qefcBNArlkO5BJWlZBUsnJLfuJNJFzQ1DBGgddc3HAPubn6", + "69WKyi2K1CuxBkVcv8SAOIkEI8RYbzl6dFsaxruHLhT6n+pZyfLJ1JZKfI9ik45JEN6aM5zJW7Lawbun", + "4ru9Z2L8LnQF0x21KUbBuSdr2Q4/lKqH++v3vu9Rs1Pdi23Q5F+M4F+M4IiMQNeSJ49ocH9hgSWoXG5d", + "TvMl7OIHw9syuOAnlYhlkF/sYBaujUGKV1x0eUUbqTV5/m5csybnfrCW5QIUc63yUaswInMr9MuGI/kz", + "j9FPwV7v6kP78f0f4n5/Qbk/z50dtzU+qCwZyIYKKB92lvgXF/hvwwVsixxq93VKNJSlCs++Fnj2rSvG", + "1c3j1kU2kg90yhy2wnTn59MPnT+7CpFa1roQN8G3aFC33qCh7mAe1qr/9+kNZTqbC+lq5mHH7OHHGmh5", + "6hpk9H5ta1IPnmCh7eDHMDst+uspdUpE7Fnle8pHH/YV2dhTp8glXvKhof5xa9QKjUTIPRvz0Lv3hndh", + "K1zHWFubx/PTU8wVWAqlTycfpx969pDw4fuGXHwHt0kl2RpLlL//+P8DAAD//wKdD2Ya9wAA", } // 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 2ca24cb9a1..77f2fccbea 100644 --- a/daemon/algod/api/server/v2/generated/experimental/routes.go +++ b/daemon/algod/api/server/v2/generated/experimental/routes.go @@ -90,206 +90,210 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+y9e5PcNpIg/lUQtRuhx6/YrZe1o/7FxF5bsj19liyFuu25XbXORpFZVZgmAQ4A1sM6", - "ffcLJAASJMEqVndZ8kTcX1IX8UgkEonMRD4+TVJRlIID12py9mlSUkkL0CDxL5qmouI6YZn5KwOVSlZq", - "JvjkzH8jSkvGF5PphJlfS6qXk+mE0wKaNqb/dCLhnxWTkE3OtKxgOlHpEgpqBtbb0rSuR9okC5G4Ic7t", - "EBevJp93fKBZJkGpPpRveb4ljKd5lQHRknJFU/NJkTXTS6KXTBHXmTBOBAci5kQvW43JnEGeqRO/yH9W", - "ILfBKt3kw0v63ICYSJFDH86XopgxDh4qqIGqN4RoQTKYY6Ml1cTMYGD1DbUgCqhMl2Qu5B5QLRAhvMCr", - "YnL2YaKAZyBxt1JgK/zvXAL8DommcgF68nEaW9xcg0w0KyJLu3DYl6CqXCuCbXGNC7YCTkyvE/KmUprM", - "gFBO3n//kjx9+vSFWUhBtYbMEdngqprZwzXZ7pOzSUY1+M99WqP5QkjKs6Ru//77lzj/pVvg2FZUKYgf", - "lnPzhVy8GlqA7xghIcY1LHAfWtRvekQORfPzDOZCwsg9sY2Puinh/F91V1Kq02UpGNeRfSH4ldjPUR4W", - "dN/Fw2oAWu1LgylpBv3wKHnx8dPj6eNHn//tw3ny3+7Pb55+Hrn8l/W4ezAQbZhWUgJPt8lCAsXTsqS8", - "j4/3jh7UUlR5RpZ0hZtPC2T1ri8xfS3rXNG8MnTCUinO84VQhDoyymBOq1wTPzGpeG7YlBnNUTthipRS", - "rFgG2dRw3/WSpUuSUmWHwHZkzfLc0GClIBuitfjqdhymzyFKDFy3wgcu6M+LjGZdezABG+QGSZoLBYkW", - "e64nf+NQnpHwQmnuKnXYZUWulkBwcvPBXraIO25oOs+3ROO+ZoQqQom/mqaEzclWVGSNm5OzG+zvVmOw", - "VhCDNNyc1j1qDu8Q+nrIiCBvJkQOlCPy/Lnro4zP2aKSoMh6CXrp7jwJqhRcARGzf0Cqzbb/z8u3PxEh", - "yRtQii7gHU1vCPBUZJCdkIs54UIHpOFoCXFoeg6tw8EVu+T/oYShiUItSprexG/0nBUssqo3dMOKqiC8", - "KmYgzZb6K0QLIkFXkg8BZEfcQ4oF3fQnvZIVT3H/m2lbspyhNqbKnG4RYQXd/PXR1IGjCM1zUgLPGF8Q", - "veGDcpyZez94iRQVz0aIOdrsaXCxqhJSNmeQkXqUHZC4afbBw/hh8DTCVwCOH2QQnHqWPeBw2ERoxpxu", - "84WUdAEByZyQnx1zw69a3ACvCZ3MtviplLBiolJ1pwEYcerdEjgXGpJSwpxFaOzSocMwGNvGceDCyUCp", - "4JoyDplhzgi00GCZ1SBMwYS79Z3+LT6jCp4/G7rjm68jd38uuru+c8dH7TY2SuyRjFyd5qs7sHHJqtV/", - "hH4Yzq3YIrE/9zaSLa7MbTNnOd5E/zD759FQKWQCLUT4u0mxBae6knB2zR+av0hCLjXlGZWZ+aWwP72p", - "cs0u2cL8lNufXosFSy/ZYgCZNaxRhQu7FfYfM16cHetNVK94LcRNVYYLSluK62xLLl4NbbId81DCPK+1", - "3VDxuNp4ZeTQHnpTb+QAkIO4K6lpeANbCQZams7xn80c6YnO5e/mn7LMTW9dzmOoNXTsrmQ0HzizwnlZ", - "5iylBonv3Wfz1TABsIoEbVqc4oV69ikAsZSiBKmZHZSWZZKLlOaJ0lTjSP8uYT45m/zbaWN/ObXd1Wkw", - "+WvT6xI7GZHVikEJLcsDxnhnRB+1g1kYBo2fkE1YtodCE+N2Ew0pMcOCc1hRrk8alaXFD+oD/MHN1ODb", - "SjsW3x0VbBDhxDacgbISsG14T5EA9QTRShCtKJAucjGrf7h/XpYNBvH7eVlafKD0CAwFM9gwpdUDXD5t", - "TlI4z8WrE/JDODaK4oLnW3M5WFHD3A1zd2u5W6y2Lbk1NCPeUwS3U8gTszUeDUbMPwbFoVqxFLmRevbS", - "imn8N9c2JDPz+6jO/xokFuJ2mLhQ0XKYszoO/hIoN/c7lNMnHGfuOSHn3b63IxszSpxgbkUrO/fTjrsD", - "jzUK15KWFkD3xd6ljKOSZhtZWO/ITUcyuijMwRkOaA2huvVZ23seopAgKXRg+DYX6c3fqFoe4czP/Fj9", - "44fTkCXQDCRZUrU8mcSkjPB4NaONOWKmISr4ZBZMdVIv8VjL27O0jGoaLM3BGxdLLOqxHzI9kBHd5S3+", - "h+bEfDZn27B+O+wJuUIGpuxxdo8MmdH2rYJgZzIN0AohSGEVfGK07oOgfNlMHt+nUXv0nbUpuB1yi6h3", - "6GrDMnWsbcLBhvYqFFAvXlmNTkOhIlpbvSoqJd3G127nGoOAK1GSHFaQd0GwLAtHswgRm6PzhW/FJgbT", - "t2LT4wliA0fZCTMOytUeu3vge+UgE3I/5nHsMUg3CzSyvEL2wEMRyMzSWKvPZ0Lejh13+CwnjQ2eUDNq", - "cBtNO0jCplWZuLMZsePZBp2BmmfP3Vy0O3wMYy0sXGr6B2BBmVGPgYX2QMfGgihKlsMRSH8ZvQVnVMHT", - "J+Tyb+ffPH7y65NvnhuSLKVYSFqQ2VaDIvedskqU3ubwoL8yVBerXMdHf/7MW27b48bGUaKSKRS07A9l", - "LcJWJrTNiGnXx1obzbjqGsBRHBHM1WbRTuxjhwHtFVNG5CxmR9mMIYRlzSwZcZBksJeYDl1eM802XKLc", - "yuoYuj1IKWT06iql0CIVebICqZiIPC+9cy2Ia+Hl/bL7u4WWrKkiZm60hVccJawIZekNH8/37dBXG97g", - "Zifnt+uNrM7NO2Zf2sj3plVFSpCJ3nCSwaxatFTDuRQFoSTDjnhH/wDayi2sgEtNi/LtfH4c3VngQBEd", - "lhWgzEzEtjBSg4JUcOsaskdddaOOQU8XMd5mqYcBcBi53PIUDa/HOLbDmnzBOL4CqS1PA7XewJhDtmiR", - "5d3V9yF02KnuqQg4Bh2v8TNafl5Brun3Ql41Yt8PUlTl0YW87pxjl0PdYpxtKTN9vVGB8UXedkdaGNhP", - "Ymv8Kgt66Y+vWwNCjxT5mi2WOtCz3kkh5seHMTZLDFD8YLXU3PTp66o/icwwE12pI4hgzWANhzN0G/I1", - "OhOVJpRwkQFufqXiwtmAAwu+nOODvw7lPb20iucMDHWltDKrrUqCz9m9+6LpmNDUntAEUaMGHvPqV1jb", - "yk5nnSNyCTTbkhkAJ2LmXszcWx4ukuJbvPbijRMNI/yiBVcpRQpKQZY4S91e0Hw7e3XoHXhCwBHgehai", - "BJlTeWdgb1Z74byBbYKeI4rc//EX9eArwKuFpvkexGKbGHpru4d7Fu1DPW76XQTXnTwkOyqB+HuFaIHS", - "bA4ahlB4EE4G968LUW8X746WFUh8oPxDKd5PcjcCqkH9g+n9rtBW5YA/pFNvjYRnNoxTLrxgFRssp0on", - "+9iyadTSwc0KAk4Y48Q48IDg9ZoqbR/VGc/QFmivE5zHCmFmimGAB9UQM/IvXgPpj52ae5CrStXqiKrK", - "UkgNWWwNHDY75voJNvVcYh6MXes8WpBKwb6Rh7AUjO+QZVdiEUR1/fbkvE76i8MXGnPPb6OobAHRIGIX", - "IJe+VYDd0CdsABCmGkRbwmGqQzm1I9p0orQoS8MtdFLxut8Qmi5t63P9c9O2T1xUN/d2JkChK5pr7yBf", - "W8xab8AlVcTBQQp6Y2QPNIPY1/8+zOYwJorxFJJdlI8qnmkVHoG9h7QqF5JmkGSQ021/0J/tZ2I/7xoA", - "d7xRd4WGxLp1xTe9oWTvRbNjaIHjqZjwSPALSc0RNKpAQyCu956RM8CxY8zJ0dG9eiicK7pFfjxctt3q", - "yIh4G66ENjvu6AFBdhx9DMADeKiHvj0qsHPS6J7dKf4LlJugliMOn2QLamgJzfgHLWDAhuo85oPz0mHv", - "HQ4cZZuDbGwPHxk6sgMG3XdUapayEnWdH2F7dNWvO0H03ZVkoCnLISPBB6sGlmF/Yh2SumPeThUcZXvr", - "g98zvkWWkzOFIk8b+BvYos79znq6BqaOY+iykVHN/UQ5QUC9/5wRwcMmsKGpzrdGUNNL2JI1SCCqmhVM", - "a+vB3lZ1tSiTcIDou8aOGd2rZvRNcecz6yUOFSyvvxXTidUJdsN31VEMWuhwukApRD7CQtZDRhSCUQ4w", - "pBRm15lzpvfu1J6SWkA6po1P2vX1f0+10IwrIP8lKpJSjipXpaGWaYREQQEFSDODEcHqOZ2rS4MhyKEA", - "q0nil4cPuwt/+NDtOVNkDmsfgWIadtHx8CHacd4JpVuH6wj2UHPcLiLXBz74mIvPaSFdnrLf1cKNPGYn", - "33UGr1+JzJlSyhGuWf6dGUDnZG7GrD2kkXFuJjjuqLec1pN9f92475esqHKqj/FqBSuaJ2IFUrIM9nJy", - "NzET/LsVzd/W3TC6BlJDoykkKcaEjBwLrkwfG0ayTzds3OtYUUDGqIZ8S0oJKdiwByPyqRrGE2IdItMl", - "5QuU9KWoFs4jz46DnLpS1qYiK94bIioN6Q1P0Dod49zOC9tHvhg5CKjRxbqmbat5rGk9nwt2GnOlBsjr", - "mvqjr1vTyaCqapC6alRVi5x2+M4ILt4S1AL8NBOPfANB1BmhpY+vcFvMKTCb+8fY2puhY1D2Jw58BJuP", - "Q26CRk/Ot0eQVuxAREIpQeHdEtqXlP0q5mGonrt81FZpKPomeNv114Hj935Q0RM8ZxySQnDYRqPTGYc3", - "+DF6nPB+G+iMksZQ367y0IK/A1Z7njHUeFf84m53T2j3qUl9L+Sx3jLtgKPl8hFPh3vfyd2Ut33gpHke", - "eRN0gTxdBqCmdeIAJglVSqQMha2LTE3tQXPPiC7qp43+d7V78hHOXnfczuNXGCOKxl3IS0JJmjM0/Qqu", - "tKxSfc0pGpeCpUa8lrwWPWxufOmbxO2bEfOjG+qaU/RYq01OUU+LOUTsK98DeKujqhYLULqjpMwBrrlr", - "xTipONM4V2GOS2LPSwkSXYdObMuCbsnc0IQW5HeQgswq3RbbMU5NaZbn7iXOTEPE/JpTTXKgSpM3jF9t", - "cDj/Wu+PLAe9FvKmxkL8dl8AB8VUEveu+sF+RU9gt/yl8wrGvAL2s/eybAJnJ2aZrVj5/33/P88+nCf/", - "TZPfHyUv/r/Tj5+efX7wsPfjk89//ev/af/09PNfH/znv8d2ysMei6JykF+8cirtxSvUW5rHmx7sX8xw", - "XzCeRIksdMPo0Ba5jxHDjoAetK1aegnXXG+4IaQVzVlmeMttyKF7w/TOoj0dHappbUTHiuXXeqA2cAcu", - "QyJMpsMaby1F9R0S4/GK+JroQhDxvMwrbrfSS982HMc7hon5tI5JtelqzggGLC6p92p0fz755vlk2gQa", - "1t8n04n7+jFCySzbxMJJM9jElDx3QPBg3FOkpFsFOs49EPaoD5x1ygiHLaCYgVRLVn55TqE0m8U5nA9y", - "cMaiDb/g1qPdnB98m9y6Jw8x//JwawmQQamXsTQWLUENWzW7CdDxFymlWAGfEnYCJ11jTWb0ReeNlwOd", - "YzoF1D7FGG2oPgeW0DxVBFgPFzLKIhKjn44/v7v81dHVITdwDK7unPVDpP9bC3Lvh++uyKljmOqejWy2", - "QwexqBFV2oVbtTyJDDezyXuskHfNr/krmDPOzPeza55RTU9nVLFUnVYK5Lc0pzyFk4UgZz6C6xXV9Jr3", - "JK3B/FpB7Bwpq1nOUnITKiQNedqcKf0Rrq8/0Hwhrq8/9pwq+uqDmyrKX+wEiRGERaUTl/EhkbCmMvZo", - "peqIfxzZpnTZNasVskVlLZs+o4QbP87zaFmqbuRvf/llmZvlB2SoXFyr2TKitJBeFjECioUG9/cn4S4G", - "SdferlIpUOS3gpYfGNcfSXJdPXr0FEgrFPY3d+UbmtyWMNq6MhiZ3DWq4MKtWgkbLWlS0kXsbez6+oMG", - "WuLuo7xcoI0jzwl2a4Xgeo96HKpZgMfH8AZYOA4OJ8TFXdpePrtXfAn4CbcQ2xhxo3mxv+1+BUG5t96u", - "TmBvb5cqvUzM2Y6uShkS9ztTJ/1ZGCHLu1EotkBt1eVHmgFJl5DeuMQ1UJR6O2119546TtD0rIMpm9LI", - "htRhUg18WZgBqcqMOlGc8m03u4ECrb0/8Hu4ge2VaHJyHJLOoB1dr4YOKlJqIF0aYg2PrRuju/nOHQwV", - "+7L0QeoYrejJ4qymC99n+CBbkfcIhzhGFK3o7yFEUBlBhCX+ARTcYqFmvDuRfmx5RsuY2Zsvkt7I837i", - "mjTKk/PcCleDVnf7vQDMjybWisyokduFS+1lI8gDLlYpuoABCTl83BkZp916EMJB9t170ZtOzLsXWu++", - "iYJsGydmzVFKAfPFkAoqMx1/PT+TfT90LxOYsdMhbJajmFQ7NlqmQ2Xrkc2mIBwCLU7AIHkjcHgw2hgJ", - "JZslVT7rGCZn82d5lAzwB2ZE2JUH5yJwNQsysNVZbjzP7Z7TnnbpsuH4FDg+702oWo7IYWMkfPRuj22H", - "4CgAZZDDwi7cNvaE0mRnaDbIwPF2Ps8ZB5LEvNYCM2hwzbg5wMjHDwmxFngyeoQYGQdg47s4Dkx+EuHZ", - "5ItDgOQuuwT1Y+OLevA3xOO+rB+3EXlEaVg4G3jVSj0HoM7Vsb6/Og63OAxhfEoMm1vR3LA5p/E1g/TS", - "saDY2km+4jwzHgyJszseQOzFctCa7FV0m9WEMpMHOi7Q7YB4JjaJDfyMSryzzczQe9S1HcNQYwfTJr65", - "p8hMbNDbB68W60q9B5ZhODwYgYa/YQrpFfsN3eYWmF3T7pamYlSokGScOa8mlyFxYszUAxLMELncD3LZ", - "3AqAjrGjSQztlN+9SmpbPOlf5s2tNm1ytPmoodjxHzpC0V0awF/fClNnn3nXlViidoq200o78U4gQsaI", - "3rCJ/iNN/ylIQQ6oFCQtISq5ib2cGt0G8Ma59N0C4wWm96F8+yDwhJKwYEpDY0T3fhJfwzxJMaugEPPh", - "1elSzs363gtRX1P2GRE7tpb5xVeArsRzJpVO8AUiugTT6HuFSvX3pmlcVmr7WtkcvCyL8wac9ga2Scby", - "Kk6vbt4fX5lpf6pZoqpmyG8Ztw4rM8wZHfXA3DG1ddLdueDXdsGv6dHWO+40mKZmYmnIpT3Hv8i56HDe", - "XewgQoAx4ujv2iBKdzDIIHK2zx0DuSl44z/ZZX3tHabMj73Xa8fH7w7dUXak6FoCg8HOVTB8JjJiCdNB", - "yuV+SOvAGaBlybJNxxZqRx3UmOlBBg+fqK6DBdxdN9geDKBI+x7mICFqQqg/We/oWlwKExViZHcrFU5k", - "0weN/21Tmr8o68oRwUS3MIK51JLDe9z4XrZSL7aXEqld0J+1Ylw/f9anyNrGb2AZsxuXcdP6pVE02ogP", - "1C2bynzPJrABxT0kz4A9h1Mx5Qtx9Mm2joHcR7lXQPMfYfuLaYvLmXyeTu5myI5RvhtxD67f1Yctimd0", - "lLCGzda71IEop2UpxYrmiTP3DzEKKVaOUWBz/zrwhS+eOGVffXf++p0D//N0kuZAZVILboOrwnblv8yq", - "bDLKgQPiE/0bDdxrUFawDza/zqAXPhGsl+Aypge6QS+1a/P8ExxF92Qwj/tr7eV97qXKLnHHixWU9YNV", - "Y0y171XtNyq6oiz3VkwP7YBvFS5uXH7gKFcIB7jzW1fwZJkcld30Tnf8dDTUtYcn4VxvMSVSXDrhLmES", - "siL3dtVmQfeUo6xTXPXpTGya23Pknfy9kC3m7xzro29f/sLuMsaj3N0OjwOuRr4KR1fwPCFIS+S3xW/m", - "ND58GB61hw+n5LfcfQgAxN9n7nc0Fj18GDVLRrUOwyRQqeC0gAe1k+DgRnxZFZXDetwFfb4qEHXo6z1M", - "hjWF2kcsj+61w95aMofPzP2SQQ7mp/0BNJ1Nt+gOgRlzgi6HHOlrH4nCFv5QRPCuSxDGcBjSQmZfUExt", - "bK28/SPEqwIto4nKWRp/M+IzZdgrt74ApjHBxgPKtRmxYgOuJbxiwVim2ZhcXR0ggzmiyFTRdGEN7mbC", - "He+Ks39WQFgGXJtPEu+1zlXnlQMctSeQGl2oP5cb2L44NsPfRWcK03p3ZUYEYrfCFHoe9MB9VZsA/UJr", - "C3ujMx3qwBTO2GPcO5yPHH04arbO2Mu2B8E4PWZMATjP6Fx+8YE5ogXdmErmUvwOcbsVmvsiAZg+kTlD", - "r73fIVTPwjJGLZZSW6ubunTN7Pu2e7xuPLTxd9aF/aLr3Om3uUzjp/qwjbyN0qviaQIdkoeUsPDpou3Z", - "NsBa8HgFvhyYtto/a1Juz5ONPmw5SMdPZRiKcGrHb06lg7kXvpHT9YzGcnobXcjAFGxv6wFWC+I7+w1Q", - "dYienZ0EDkh1W2YzmJQgmwD0fja0W+o1dtrRGk2jwCBFharL1DqN5EpEhqn4mnJbC830s/zK9VZgX0xM", - "r7WQmH9Ixd+KM0hZQfO4gpOl/XfBjC2YLfNVKQjqSLmBbAlFS0WuFlcdeOpQczEnj6ZBMTu3GxlbMcVm", - "OWCLx7bFjCq8LuvXi7qLWR5wvVTY/MmI5suKZxIyvVQWsUqQWvdEIa/2eJiBXgNw8gjbPX5B7qOvh2Ir", - "eGCw6ISgydnjF/hSZ/94FLtlXZm2XSw7Q579d8ez43SMzi52DMMk3agn0VQttk7r8O2w4zTZrmPOErZ0", - "F8r+s1RQThcQdy8s9sBk++Ju4utLBy88s0UGlZZiS5iOzw+aGv40ELJk2J8Fg6SiKJgunEeAEoWhp6ZI", - "lJ3UD2crFrr8/h4u/xEda0rvV9CxdX1hNYYWAy7H6P70Ey2gjdYpoTbpVM4alzdfdYRc+Jx2WPCgrnNg", - "cWPmMktHWRI94OaklIxrtH9Uep78xajFkqaG/Z0MgZvMnj+LFA5o59bmhwH+xfEuQYFcxVEvB8jeyyyu", - "L7nPBU8Kw1GyB02IYHAqBz2A4r4eQw4nu4ceK/maUZJBcqta5EYDTn0nwuM7BrwjKdbrOYgeD17ZF6fM", - "SsbJg1Zmh35+/9pJGYWQsUS1zXF3EocELRms0OE7vklmzDvuhcxH7cJdoP+6z9Ve5AzEMn+Wo4qANzrt", - "CvQyIvwvb1xR4p7sPeCcZr3P6j5fOIAtarS0ElrLbPb4NyKNJonS6MOHCPTDh1MnzP32pP3ZMqmHD+Pp", - "26KGI/Nrg4W76HXYN7aH34qIGcfXSqmf0F2QWsSMNsRqzQdzlGduqClp16X48nfhcdyf4y4u8VNwff0B", - "v3g84B9dRHzlI48b2Djx2ZUMEEpQlydKMln9PXCuo+RbsRlLOB1O6onnT4CiAZSMNDLhSnp1h6KPznu9", - "HgIaNaPOIBdGVQpTqodW6X8dPJvFT3dgu2J59kuTYKNzkUjK02XUNWlmOv7a1Aeul2hZZTRL85JyDnl0", - "OKuh/eo1uYiu+Q8xdp6C8ZFtu3Wv7HI7i2sAb4PpgfITGvQynZsJQqy2cxfUsXH5QmQE52lSAjfMsV9A", - "Lqhq888KlI4dDfxg/fPxycYwX1tUhQDP0IZzQn7AKGIDSyvfI9pOfEKudnKaqswFzaaYKOzqu/PXxM5q", - "+9gql7aoywJNB+1VRG2945P11AUr41Go48fZHRZnVq10UtdgieX5MC2aKjGs4wCARoUQOyfkVVDM36YE", - "MUMQzBMnC8iCki9Wo0CaMP/RmqZLNJS0LrJhkh9fjchTpQpKotelTesU4HjuDNyuIJGtRzQlQi9BrpkC", - "jDuCFbRTi9R5dpyhzqcaaS9PVpxbSjk5QKaoE34finYPnBVI/AtnFLIO4g9Uk20xr0OLM11ir2hG0m6l", - "p14tdJuooi5Z+cZXs6dccJZiPtCYQIRpEMa9mYxInRp/7FATd0IjhytaX6qOeHBYHKw45RmhQ1z//TH4", - "ajbVUof9U8PG1R1YgFaOs0E29WXSnHWecQUupbshopBPChnxsIiJHEn9mnsgGWGE84C55Xvz7SdnjMPQ", - "vxvGUe12aHNitrWfYwV7bXR1pslCgHLraad5UR9MnxPMeJLB5uOJr3iPY1ifHrNs68DWH+rcu7M59zHT", - "9qVp6/JQ1j+3fFPspOdl6SYdLqIXrxy64YMIjjlR+FftALn1+OFoO8htpx8q3qeG0GCFLjRQ4j3cI4y6", - "oFyneqtRESxFYQtivfGjyagYj4DxmnH/nhO/INLolYAbg+d1oJ9KJdVWBBzF066A5rXPTJehKe0eBO86", - "VDcLp0EJrtHPMbyNTS28AcZRN2gEN8q3xB8KQ92BMPGS5rUfZ6SyHUpVTojKMDi0U+suxjgM4/bVNNsX", - "wJ4CutOmO6akPfQmGsr3MauyBeiEZlksw/63+JXgV5JVKDnABtKqzsReliTF9HbtfH99anMTpYKrqtgx", - "l29wx+mC4pERaggLWPodxnji2Rb/PaS0ce3BeXBEh3fXzA5LctmPUIlJvYamE8UWyXhM4J1yd3Q0U9+O", - "0Jv+R6X0XCzagHwNI+kAlwv3KMbfvjMXR5gEq+csa6+WOkcVOqYKXwMd1cY6u0qbK+FV1ku2j0+wdUnh", - "3WaI4eLAU7z8BqKoQpO3vV+tGXgoliodDP2j2iUh0JTsZEGDgd3WcbFjRO+/Zww5K1pfxeMZn91adyLU", - "+5H3AfrRB6mQkjLnsNIwiz5mnZtvP9xzjB9ts8HdRbiQvUH76I+rofA6n/MWv3eLh96Ay0xUSlgxUXlX", - "EO+Q6VVC+2urFGcd4Bhdf9TN+WsbnwdN5VeuiJNdptPJf/zFuu8S4Fpu/wSG896m98qS9qVda55qmpC6", - "/seoeiCtW3FMPuhY6mEnG7YKo+4p69ojq1djxIF+mdbp5CI76MKMpa+e2FFixy5edHU4u2eT0ROPWCkU", - "a8rwxKqxjvR8vsKCqkF20v5Y3iNuBanG2kuNp48EOCRXqZksqO/+/7J8DqjTtYO4S+65K6Nnv+DSnju+", - "F3QfJI6wxWpOxuevPK/9OW04ypoqzPZsS6y3AzhHh5HN55BqttqT5ODvS+BBAP3U22UQlnmQ84DVQRWY", - "I+9wq2MD0K4cBDvhCXJV3xmcoaDaG9jeU6RFDdHqOXVE0W3SoyEGkDskhkSEivlLWUOyc2FhqqYMxIL3", - "T7TdoUk0O1h4M0jZccu5PEmai6NJ47Fjynjlv1Fzma4HJbfB+IChPAj9wmHD+scrrNOm6qLYPr1aqKWT", - "i34S6rVLz4YpKeq3E5+oDZT/zeefsbPk7AbC0qD4UrWmMvMtoqYXb9VJdtxHveQFvuhVF+h5PTNrvMn7", - "b9WRtKYYmJHmwogRyVB0S9uBu/Z+uqesm5qtsoOu6QauOUhXQhnl31woSLTw3ue74NiFCuuLdyskqMFU", - "4ha4wQR/75sMhlhSgWJCP+pc8MIFEgkFNdDJIM/g8Jy7kP3SfvcRwT6l/l4LU02v+2s7+TgCpnpIDKl+", - "TtxtuT/S+DbGJsY5yMS/PHWTDnKQ7deQUoqsSu0FHR6M2iA3OqXnDlYStdOk/VV2dIQgYvcGtqdWCfJF", - "sfwOhkBbycmCHiSr6mzyUc1vKgb34ijgfU3L1XRSCpEnA48dF/1MiV2Kv2HpDWTE3BTe33agUCG5jzb2", - "+jV7vdz6zIBlCRyyByeEnHMb4eAfttulOjqT83t61/wbnDWrbPJSZ1Q7ueZxV3FMKyrvyM38MLt5mALD", - "6u44lR1kTx6+zUCWRknXkbKdJ2O18v5Tc7eUYkNUFoqYTHJpX6xe4kGPGY4wHjtIHIAPmZS4ly6ichFz", - "ybxNzLgZKo6pcDIESAMfE7pcQ+EGjyKgLpO4x1Go9hFqKsw1fkJ98SjPxTrBY5TUeWZjSpdpp9rXhE+t", - "3/Qz9DaDwOOIKidCbMmSZiQVUkIa9oiHRVmoCiEhyQU6IMXeRufaSIQFxkJwkosFEaVR9G2+Zv+KFK1/", - "2Jur4pzihQ6Bv0cUBTRNUfsUxPUhdZ+xUx6rvKRNfmIXndhXtgGXSFAu2YnDkG3ch3dHhceDMiVfzNFW", - "wdALox3bauWisM4lHFjmkuW5V2WHKl2Sn1WFjjIY2GCmeEYKYfRh1Dl8wXM/VON8dD8VXEuR523zhBXW", - "Fs7m+oZuztNUvxbiZkbTmweo4WCdfR98NvVhf103sWYm2cl4M7Ik59UyYoHEWfypO7jupuMcB5fLC8Ac", - "wbH2W1/PY2VF2+vqFrgdKjetRcHSOA3/a/ldDXpLxVhCNJWOrVhhg5+xGTLq8HKon9mRJfXRDNwQbGy/", - "HE9zz43IPMx/URbrjkvm4C6JgYupzyfdfZqkg7d+BwCE1Ebk6UraMhfhnVxzFbGwEbz4WNoFdCQXR5+U", - "u8FmRjg6UBruBFTPD64G8L5VQ6c25ZH1qZuJjf/+oMmJdCvgP++m8lhp4MgprknLVS72+RMGOELUVWe3", - "Z4wtFz8b6x9TlyQaeaMGAAx7zLRgGOU3cygYc8pyyBKqBy53tFZMA53LxVp0C80x5Th5Su2FvQRixq4k", - "uHh+Wye+U5i2pIaURN28b1PkGWxAYbC9ra5JlbWAe0u8K1LfVQtFmeSwgpYjkUsyUKFox1YQFri3nUkG", - "UOK7VNdaEvOQCe/yjgrt1p4EPhZjsBvVqS1i7U6RPQpzVL3f8MQeEzX2KBmIViyraAt/6g6lvoerfPdk", - "8sTK3vZAjJnmZzvCez/Aue8fE2U8Jj6O40MHs6A46nYxoL0ec3iioqeexx3mwgwatakdZ8vqJzlL4g3f", - "UCVd82HTVJ/kG/VmfAn+ALHfbSBFqabtEXZ3nBAcjKhOdpxBEVzWO3x7E+dXoeGdJDw4XkzVUIAMttFw", - "mwcIv46aLpzAjg2wtBg3Yq+RmrGch+P/jv9NsRqyHcjo1ba6SKjBvQL/loQJe2szuhNoWX2hec+3qcvX", - "1lXKWeDzW9AtERL/MfraPyuas/kWT6gF33cjakkNCbnHK/uq6jzpzMS7BZOpB8zbBYSfyq6bjR0zGG5r", - "RgmANlcgEdK9gxT0BsJtwAdjy3lSbViOqmYFUwovu8529rHgFu9j7guahToyZv5ql3XzuSBN7/+/iScK", - "p/IJe8qcpk2ZZkWLjqnW1ovyxKWXUOwOOOurx54E6hpUDdFKH2ia2XwwFn918geURPA/M6Ylldsd7q97", - "fQpiXtwoOe8Du1ebB8Xwoy3jkGKRTczujlC9UUs59i6M9VzoAY3Pnz5r0h7wbbY7n2HpS+A/mpRvaBlj", - "wP+z4H2gpFEIr61e9AWw3ApGj8Bq7aozsUkkzNW+R3prWDWKsGzC2L1xkvFUAlXWa+HirVPZmpxzjBsV", - "0vrV1e9C9SgZzBlvmCXjZaUjGgCmnuPbAGGheRrROvAMMSQlGDFsRfO3K5CSZUMbZ06HLakS5vz2JnnX", - "N6L813dqfwCmGu0HY9ygiaEKmpkLPGPzOUjr8qY05RmVWdiccZKCNPc+WdOtuv3bh4FWVka+2PP6QQNp", - "ph15HbyDIGlbQPKte1i748tEDSA94hPFiKcF9K2MPCtYo4gWAy8JfRjiAf90k+RigZFPAwTokvvh249V", - "VgRHg62Vhw6bR7HfYfc0mNfYHXwtcNYxU+w+Z28Rdajw/MyZ3nnSrDWtG4pmfQXtQfD0zxeNw7LdnD79", - "x6IHr9C9vhVB2C0A7PfaOi7Y+WDgJaNtwR3YRXy6daGnoblWjX/JaL0Ox2IUrQ6boG6rdrgkg2rcb2nq", - "XEr6Rp+eUmyRMnURngfahKwl2d8DA+DZqoHubLWnrZ/5zTjjZY3gTTsOUSnKJB3jp2ZTn2fOoO0gbcM4", - "QB+BuXpg3fWTflPIupVyo1UVwErKtxF3O1UJ9r3LlOkuJXvIoDHAQdvGcjFHXoZH2JpxMPqgNl5Mu3Ex", - "bYNNzSQIJRLSSqJBc023++u2DKTcvPzb+TePn/z65JvnxDQgGVuAatK2duqeNL5MjHftLF/We6m3PB3f", - "BB8xbRHnX8p8IEi9Ke6sWW5rJTcerfpyiCU0cgHE6nv3623caq9wnMYd+c+1XbFFHn3HYij4Y/bM+VzG", - "F3DOnf4i5mQ3z2geRvxxj/ALI/xHLim/tbdY4JA9djhi9zb02Bhk/zRUGAlBPhrt1cv9IyguKmXerpTh", - "KND64agR8kAABuLMWhFCYaXTJpOitLZdtAL7B7PuJfameUjb6xCNkPgOe8ALA8eadrUPrwPnK6ckfFMj", - "JVjKxyFKaC1/XyyaW2Dz8hhskVN1tQZbd9omVmrvSxBoqF7W8XsDsm0vzA/Lmhr9Js8j4YFW+8YzFRKO", - "ESzliuZfnmtgvdtzxAdk74eDAsIYsRDJFpXqdhmqXtNRcwfxYMebmr/DkMS/g9mj6D3nhnKPjr3bDG0n", - "NLfum3MX3m2GJGsc0zqVPH5OZi7ndSkhZar7mGlfnAKvwBVINncOfLDRe2Kw9q3zF6HvQMZz73lAfgoe", - "JQQafxoImyP6lZnKwMmNUnmM+npkEcFfjEeFNfL2XBc3rUQHjSwe3GhCwpETHgSpiw5MeNCv/jd2eTao", - "31w6lYL+Okff1i3cRi7qZm1js3WMTlB9ff1Bz8Yk2YgnkzbdMcvHUbJKH5RT+g/I72Fx5MZw88Yo5peh", - "jI82q+FActHOflQs3+tm0EoV+3k6WQAHxRQmQ/3VpXD/sneph8DGHPePqoX1LokSLGIia21NHkwVJIEd", - "kf/VdYtke8V4nrSSTG+xfJ83w7Bfo5lIfqij2l1WhPoFxN19WtxAXUK1iYGvlL9dfxA0x/vIPsxwcwuJ", - "/IR8t6FFmTujIvnrvdl/wNO/PMsePX38H7O/PPrmUQrPvnnx6BF98Yw+fvH0MTz5yzfPHsHj+fMXsyfZ", - "k2dPZs+ePHv+zYv06bPHs2fPX/zHPcOHDMgWUJ+b+Gzyv5LzfCGS83cXyZUBtsEJLdmPYPYGdeW5wPJS", - "BqkpnkQoKMsnZ/6n/+FP2EkqimZ4/+vElUmYLLUu1dnp6Xq9Pgm7nC4w6DXRokqXp34eLPrTklfeXdQ+", - "ydZ7Ane0sUHipjpSOMdv77+7vCLn7y5OGoKZnE0enTw6eewqTHJassnZ5Cn+hKdnift+6ohtcvbp83Ry", - "ugSaY44I80cBWrLUf5JAs637v1rTxQLkCbqd259WT069WHH6yQX/ft717TR8mD/91IqRzvb0xEfl00++", - "ztzu1q0aY86fJ+gwEopdzU5nmJV/bFNQQePhpaCyoU4/obg8+Pups3nEP6LaYs/DqU8kEG/ZwtInvTGw", - "7umxYVmwkpTqdFmVp5/wP0i9AdA2ydyp3vBTfH87/dRaq/vcW2v796Z72GJViAw8cGI+t/X3dn0+/WT/", - "DSaCTQmSGbHQJnZwb431obvIJmeT74JGL5eQ3kywZg96fuFpevLoUSQDZ9CL2MNNZzlk5mQ+e/RsRAcu", - "dNjJhfX0O/7Mb7hYc4L52iynr4qCyi1KULqSXJG3PxI2J9Cdgik/A3IXulD4wlDNcpZOppMWej5+dkiz", - "+YlOsUrNtsGl/3nL0+iP/W1u5WYZ+Pn0U+vP9llRy0pnYh30RV3LGgr685mPler+fbqmTBvpySX6wGJ1", - "/c4aaH7qsvp2fm0S6fW+YHbA4MfQcTn66yl1CJyUQkWI8T1dBwbSc2xsRQxQ+luBvHriCoF0klCcbpIZ", - "40gXnyZNne5GxLIf+zpa764yGie+SHsrVT9IF+MxpaBZanR/LXyC7EkoD2lZwefoYcJD8mjHWtwdNBlX", - "b7ydyjCyom9pRnwYa0Le0NxgBTJy7i7y1tLsEX785aC74Nap0hxZK8t8nk6++ZL4ueBG7Ka5ZzJm+qdf", - "bvpLkCuWArmCohSSSpZvyc+89gu9NXv8HolT0vQGRa6aYK0Tg6TrtqupjIcJtvO/+6hRIHpDlpRnuQus", - "EhUWQDSUhVZlEbyOmWvF1z8ohUQAbGIZyGxGAHVCLpfe1ISxpdapGcu4rCAXJZp9MF2anYRyTFCOqwnZ", - "e5urGx3SHOIF8MSxkWQmsq2vKSzpWm9sjFSPV9XFoaMfuzJX7KuTOQYaeS8m/7nRv0J9ZnL2IdBkPnz8", - "/NF8kyt0t/jwKRDPz05tsfilUPp08nn6qSO6hx8/1gjzZXAmpWQrzPP68fP/DQAA//9vaqsGCusAAA==", + "H4sIAAAAAAAC/+y9e3MbN7Yg/lVQvLfKjx9b8iu5Y/1q6q5iJxmt7dhlKZm91/ImYPchiVET6AHQFBmv", + "v/sWDoBudDdANiXFzlTtX7bYeBwcHADnfT5NcrGqBAeu1eTk06Sikq5Ag8S/aJ6LmuuMFeavAlQuWaWZ", + "4JMT/40oLRlfTKYTZn6tqF5OphNOV9C2Mf2nEwn/rJmEYnKiZQ3TicqXsKJmYL2tTOtmpE22EJkb4tQO", + "cfZy8nnHB1oUEpQaQvmWl1vCeF7WBRAtKVc0N58UuWZ6SfSSKeI6E8aJ4EDEnOhlpzGZMygLdeQX+c8a", + "5DZYpZs8vaTPLYiZFCUM4XwhVjPGwUMFDVDNhhAtSAFzbLSkmpgZDKy+oRZEAZX5ksyF3AOqBSKEF3i9", + "mpx8mCjgBUjcrRzYGv87lwC/Q6apXICefJzGFjfXIDPNVpGlnTnsS1B1qRXBtrjGBVsDJ6bXEXlTK01m", + "QCgn7394QZ4+ffrcLGRFtYbCEVlyVe3s4Zps98nJpKAa/OchrdFyISTlRda0f//DC5z/3C1wbCuqFMQP", + "y6n5Qs5ephbgO0ZIiHENC9yHDvWbHpFD0f48g7mQMHJPbOM73ZRw/q+6KznV+bISjOvIvhD8Suzn6B0W", + "dN91hzUAdNpXBlPSDPrhUfb846fH08ePPv/bh9Psv92f3zz9PHL5L5px92Ag2jCvpQSeb7OFBIqnZUn5", + "EB/vHT2opajLgizpGjefrvCqd32J6WuvzjUta0MnLJfitFwIRagjowLmtC418ROTmpfmmjKjOWonTJFK", + "ijUroJia2/d6yfIlyamyQ2A7cs3K0tBgraBI0Vp8dTsO0+cQJQauG+EDF/TnRUa7rj2YgA3eBlleCgWZ", + "FnueJ//iUF6Q8EFp3yp12GNFLpZAcHLzwT62iDtuaLost0TjvhaEKkKJf5qmhM3JVtTkGjenZFfY363G", + "YG1FDNJwczrvqDm8KfQNkBFB3kyIEihH5PlzN0QZn7NFLUGR6yXopXvzJKhKcAVEzP4BuTbb/j/P3/5E", + "hCRvQCm6gHc0vyLAc1FAcUTO5oQLHZCGoyXEoemZWoeDK/bI/0MJQxMrtahofhV/0Uu2YpFVvaEbtqpX", + "hNerGUizpf4J0YJI0LXkKYDsiHtIcUU3w0kvZM1z3P922g4vZ6iNqaqkW0TYim7++mjqwFGEliWpgBeM", + "L4je8CQfZ+beD14mRc2LEWyONnsaPKyqgpzNGRSkGWUHJG6affAwfhg8LfMVgOMHSYLTzLIHHA6bCM2Y", + "022+kIouICCZI/Kzu9zwqxZXwBtCJ7MtfqokrJmoVdMpASNOvZsD50JDVkmYswiNnTt0mAvGtnE38Mrx", + "QLngmjIOhbmcEWihwV5WSZiCCXfLO8NXfEYVfPss9ca3X0fu/lz0d33njo/abWyU2SMZeTrNV3dg45xV", + "p/8I+TCcW7FFZn8ebCRbXJjXZs5KfIn+YfbPo6FWeAl0EOHfJsUWnOpawsklf2j+Ihk515QXVBbml5X9", + "6U1danbOFuan0v70WixYfs4WCWQ2sEYFLuy2sv+Y8eLXsd5E5YrXQlzVVbigvCO4zrbk7GVqk+2YhxLm", + "aSPthoLHxcYLI4f20JtmIxNAJnFXUdPwCrYSDLQ0n+M/mznSE53L380/VVWa3rqax1Br6Ng9yag+cGqF", + "06oqWU4NEt+7z+aruQTAChK0bXGMD+rJpwDESooKpGZ2UFpVWSlyWmZKU40j/buE+eRk8m/Hrf7l2HZX", + "x8Hkr02vc+xkWFbLBmW0qg4Y451hfdSOy8Jc0PgJrwl77SHTxLjdRENKzFzBJawp10etyNK5D5oD/MHN", + "1OLbcjsW3z0RLIlwYhvOQFkO2Da8p0iAeoJoJYhWZEgXpZg1P9w/raoWg/j9tKosPpB7BIaMGWyY0uoB", + "Lp+2Jymc5+zlEfkxHBtZccHLrXkcLKth3oa5e7XcK9boltwa2hHvKYLbKeSR2RqPBsPm3wXFoVixFKXh", + "evbSimn8N9c2JDPz+6jO/xokFuI2TVwoaDnMWRkHfwmEm/s9yhkSjlP3HJHTft+bkY0ZJU4wN6KVnftp", + "x92BxwaF15JWFkD3xb6ljKOQZhtZWG95m4686KIwB2c4oDWE6sZnbe95iEKCpNCD4btS5Fd/o2p5B2d+", + "5scaHj+chiyBFiDJkqrl0STGZYTHqx1tzBEzDVHAJ7NgqqNmiXe1vD1LK6imwdIcvHG2xKIe++GlBzIi", + "u7zF/9CSmM/mbJur3w57RC7wAlP2ODsjQ2GkfSsg2JlMA9RCCLKyAj4xUvdBUL5oJ4/v06g9+t7qFNwO", + "uUU0O3SxYYW6q23CwVJ7FTKoZy+tRKdhpSJSW7MqKiXdxtdu5xqDgAtRkRLWUPZBsFcWjmYRIjZ3fi98", + "JzYxmL4Tm8GdIDZwJzthxkG+2mN3D3wvHWRC7sc8jj0G6WaBhpdXeD3wkAUys7Ta6tOZkDe7jnv3LCet", + "Dp5QM2rwGk17SMKmdZW5sxnR49kGvYFas+fuW7Q/fAxjHSyca/oHYEGZUe8CC92B7hoLYlWxEu6A9JfR", + "V3BGFTx9Qs7/dvrN4ye/PvnmW0OSlRQLSVdkttWgyH0nrBKltyU8GK4MxcW61PHRv33mNbfdcWPjKFHL", + "HFa0Gg5lNcKWJ7TNiGk3xFoXzbjqBsBRNyKYp82inVhjhwHtJVOG5VzN7mQzUggr2lkK4iApYC8xHbq8", + "dpptuES5lfVdyPYgpZDRp6uSQotclNkapGIiYl5651oQ18Lz+1X/dwstuaaKmLlRF15z5LAilKU3fPy9", + "b4e+2PAWNztvfrveyOrcvGP2pYt8r1pVpAKZ6Q0nBczqRUc0nEuxIpQU2BHf6B9BW76FreBc01X1dj6/", + "G9lZ4EARGZatQJmZiG1huAYFueDWNWSPuOpGHYOePmK8zlKnAXAYOd/yHBWvd3Fs05L8inG0AqktzwOx", + "3sBYQrHokOXtxfcUOuxU91QEHIOO1/gZNT8vodT0ByEvWrbvRynq6s6ZvP6cY5dD3WKcbqkwfb1SgfFF", + "2XVHWhjYj2Jr/CoLeuGPr1sDQo8U+ZotljqQs95JIeZ3D2Nslhig+MFKqaXpM5RVfxKFuUx0re6ABWsH", + "a284Q7fhvUZnotaEEi4KwM2vVZw5SziwoOUcDf465Pf00gqeMzDUldParLauCJqzB+9F2zGjuT2hGaJG", + "JYx5jRXWtrLTWeeIUgIttmQGwImYOYuZs+XhIina4rVnbxxrGLkvOnBVUuSgFBSZ09TtBc23s0+H3oEn", + "BBwBbmYhSpA5lbcG9mq9F84r2GboOaLI/Ve/qAdfAV4tNC33IBbbxNDb6D2cWXQI9bjpdxFcf/KQ7KgE", + "4t8VogVysyVoSKHwIJwk968P0WAXb4+WNUg0UP6hFO8nuR0BNaD+wfR+W2jrKuEP6cRbw+GZDeOUC89Y", + "xQYrqdLZvmvZNOrI4GYFwU0Yu4lx4ATj9ZoqbY3qjBeoC7TPCc5jmTAzRRrgpBhiRv7FSyDDsXPzDnJV", + "q0YcUXVVCamhiK2Bw2bHXD/BpplLzIOxG5lHC1Ir2DdyCkvB+A5ZdiUWQVQ3tifndTJcHFpozDu/jaKy", + "A0SLiF2AnPtWAXZDn7AEIEy1iLaEw1SPchpHtOlEaVFV5rbQWc2bfik0ndvWp/rntu2QuKhu3+1CgEJX", + "NNfeQX5tMWu9AZdUEQcHWdErw3ugGsRa/4cwm8OYKcZzyHZRPop4plV4BPYe0rpaSFpAVkBJt8NBf7af", + "if28awDc8VbcFRoy69YV3/SWkr0XzY6hBY6nYswjwS8kN0fQiAItgbjee0YuAMeOXU6Oju41Q+Fc0S3y", + "4+Gy7VZHRsTXcC202XFHDwiyu9HHAJzAQzP0zVGBnbNW9uxP8V+g3AQNH3H4JFtQqSW04x+0gIQO1XnM", + "B+eld733buDotZm8xvbcI6kjm1DovqNSs5xVKOu8gu2di379CaJ2V1KApqyEggQfrBhYhf2JdUjqj3kz", + "UXCU7m0I/kD5FllOyRSyPF3gr2CLMvc76+kaqDruQpaNjGreJ8oJAur95wwLHjaBDc11uTWMml7CllyD", + "BKLq2YppbT3Yu6KuFlUWDhC1a+yY0Vk1ozbFnWbWcxwqWN5wK6YTKxPshu+iJxh00OFkgUqIcoSGbICM", + "KASjHGBIJcyuM+dM792pPSV1gHSXNpq0m+f/nuqgGVdA/kvUJKccRa5aQ8PTCImMAjKQZgbDgjVzOleX", + "FkNQwgqsJIlfHj7sL/zhQ7fnTJE5XPsIFNOwj46HD1GP804o3Tlcd6APNcftLPJ8oMHHPHxOCunfKftd", + "LdzIY3byXW/wxkpkzpRSjnDN8m99AfRO5mbM2kMaGedmguOOsuV0TPbDdeO+n7NVXVJ9F1YrWNMyE2uQ", + "khWw9yZ3EzPBv1/T8m3TDaNrIDc0mkOWY0zIyLHgwvSxYSRmHMaZOcDWhXQsQHBme53bTntEzNZLj61W", + "UDCqodySSkIONnrCcI6qWeoRsX6V+ZLyBQoMUtQL59hnx8ELv1ZWNSNrPhgiylTpDc9QyR17AJwztw+g", + "MewUUCPS9TXkVoC5ps18LmZqzMsc7EHfYhA1kk0nSYnXIHXdSrwWOd0ooBGPQYffC/DTTjzSlIKoM7zP", + "EF/htpjDZDb3j1HZt0PHoBxOHLgath9T3oZG3C63d8D02IGIhEqCwicqVFMp+1XMw4g/94aprdKwGmry", + "bddfE8fvfVJeFLxkHLKV4LCNBrkzDm/wY/Q44TOZ6IwMS6pvXwbpwN8DqzvPGGq8LX5xt/sntG+xUj8I", + "eVcmUTvgaPZ+hAVyr7ndTXlTOykty4hp0cUD9S8ANW3yDzBJqFIiZ8iznRVqag+as0a64KEu+t81Xs53", + "cPb64/ZsaGGoKeqIoawIJXnJUIMsuNKyzvUlp6ijCpYacX7ywnhaa/nCN4mrSSNaTDfUJafo+NZorqIO", + "G3OIqGl+APDKS1UvFqB0T9aZA1xy14pxUnOmca6VOS6ZPS8VSPRAOrItV3RL5oYmtCC/gxRkVusu94/h", + "bkqzsnQGPTMNEfNLTjUpgSpN3jB+scHhvNHfH1kO+lrIqwYL8dd9ARwUU1ncSetH+xUdit3yl865GNMT", + "2M/eWbONv52YZXZC7v/3/f88+XCa/TfNfn+UPf//jj9+evb5wcPBj08+//Wv/6f709PPf33wn/8e2ykP", + "eywYy0F+9tJJxmcvUfxpbUAD2L+Y/n/FeBYlstCbo0db5D4GHjsCetBVjuklXHK94YaQ1rRkhblbbkIO", + "/RdmcBbt6ehRTWcjesowv9YDhYpb3DIkcsn0rsYbc1FDv8Z42CMaJV0kI56Xec3tVnru20b1eP8yMZ82", + "oa02680JwbjHJfXOke7PJ998O5m28YrN98l04r5+jFAyKzaxqNQCNjFZ0R0QPBj3FKnoVoGO3x4Ie9SV", + "zvp2hMOuYDUDqZas+vI3hdJsFr/hfKyE0zlt+Bm3jvHm/KCJc+ssJ2L+5eHWEqCASi9j2TA6jBq2ancT", + "oOd2UkmxBj4l7AiO+jqfwsiLzqmvBDrHrAwofYox0lBzDiyheaoIsB4uZJRiJUY/vbAA9/irOxeH3MAx", + "uPpzNvZM/7cW5N6P31+QY3dhqns2QNoOHYS0RkRpF7XVcUgyt5nNAWSZvEt+yV/CHLUPgp9c8oJqejyj", + "iuXquFYgv6Ml5TkcLQQ58YFgL6mml3zAaSXTdAUheKSqZyXLyVUokLTkaVOvDEe4vPxAy4W4vPw48M0Y", + "ig9uquj9YifIDCMsap25xBGZhGsqY7Yv1SQOwJFtZphds1omW9RWQeoTU7jx43cerSrVDyAeLr+qSrP8", + "gAyVC481W0aUFtLzIoZBsdDg/v4k3MMg6bXXq9QKFPltRasPjOuPJLusHz16CqQTUfube/INTW4rGK1d", + "SQY495UquHArVsJGS5pVdBEzsV1eftBAK9x95JdXqOMoS4LdOpG83jEfh2oX4PGR3gALx8FRibi4c9vL", + "JwmLLwE/4RZiG8NutIb/m+5XENt74+3qxQcPdqnWy8yc7eiqlCFxvzNN7qCFYbK8N4ZiC5RWXZqlGZB8", + "CfmVy38Dq0pvp53u3uHHMZr+6mDKZkaykXmYmwMNFDMgdVVQx4pTvu0nSVCgtXcrfg9XsL0QbWqPQ7Ii", + "dIP0VeqgIqUG3KUh1vDYujH6m++8ylCwryof645Bj54sThq68H3SB9myvHdwiGNE0QkiTyGCyggiLPEn", + "UHCDhZrxbkX6seUZKWNmX75IliR/9xPXpBWenANYuBrUutvvK8A0a+JakRk1fLtwGcJsIHpwi9WKLiDB", + "IYc2opHh3h27Eg6y792LvnRi3n/QBu9NFGTbODNrjlIKmC+GVFCY6bn9+ZmsGdJZJjDxp0PYrEQ2qfGP", + "tJcOlR1bnc1kmAItTsAgectweDC6GAk5myVVPnkZ5njzZ3kUD/AHJlbYlU7nLPBYCxK5Ncly/J3bP6cD", + "6dIl1fGZdHz6nFC0HJEKx3D46CQf2w7BkQEqoISFXbht7AmlTfLQbpCB4+18XjIOJIs5vwVq0OCZcXOA", + "4Y8fEmI18GT0CDEyDsBG8zoOTH4S4dnki0OA5C5JBfVjo2E++Bvi4WPWHdywPKIyVzhLWLVyfwNQ5zHZ", + "vF89v10chjA+JeaaW9PSXHNO4msHGWR1Qba1l8PFOXg8SLGzOwwg9mE5aE32KbrJakKeyQMdZ+h2QDwT", + "m8zGj0Y53tlmZug96iGP0ayxg2nz59xTZCY26DSET4v1yN4DSxoOD0Yg4W+YQnrFfqnX3AKza9rd3FSM", + "ChWSjFPnNeSSYifGTJ3gYFLkcj9IiXMjAHrKjja/tBN+9wqpXfZk+Ji3r9q0TfXmg49ixz91hKK7lMDf", + "UAvTJLF51+dYonqKru9LN39PwELGiN5cE0MjzdAUpKAEFAqyDhOVXcUsp0a2AXxxzn23QHmBWYIo3z4I", + "HKokLJjS0CrRvZ/E11BPUkxOKMQ8vTpdyblZ33shmmfKmhGxY2eZX3wF6JE8Z1LpDC0Q0SWYRj8oFKp/", + "ME3jvFLXZcum8mVF/G7Aaa9gmxWsrOP06uZ99dJM+1NzJap6hvct49ZhZYapp6OOnDumtr6+Oxf82i74", + "Nb2z9Y47DaapmVgacunO8S9yLno3767rIEKAMeIY7loSpTsuyCAAd3g7BnxTYOM/2qV9HRymwo+912vH", + "hwGn3ig7UnQtgcJg5yoYmokMW8J0kLl5GBmbOAO0qlix6elC7ahJiZkepPDw+e56WMDddYPtwUDXLy/q", + "5tzJFei8/5zO5xgZ5GPDwll3QOfrBhKlHBsTWtQSlWodZ7thYsqGsRu59le/nGsh6QKcYjSzIN1qCFzO", + "IWgI0j4qopm1cBZsPodQIahuoszqANdX+0SLO4wgsrjWsGZcf/ssRkZ7qKeFcT/K4hQToYWUmehiqHj1", + "bFUgdzaVS4KtuYH2NBpB+gq22S9GQiEVZVK1HmNOE9q9/w7Y9fXqFWxx5L2OWAawPbuCYup7QBqMqQWb", + "TzZwohGBwhymmPShs4UH7NRpfJfuaGtc1tk08bdu2Z2srN2l3OZgtHY7A8uY3TiPm8vM6YEu4vukvG8T", + "WEIZF5JjwHKFUzHla/QMn6ImPHof7V4ALT3x4nImn6eT2xmnYq+ZG3EPrt81D2gUz+j8ZI0VHVvzgSin", + "VSXFmpaZM+GlHn8p1u7xx+be4veFmck4ZV98f/r6nQP/83SSl0Bl1ghjyVVhu+pfZlU2T+3upwQ5Fq8V", + "scJ6sPlNcs3Q7He9BFdMIZD3B1mfW5NucBSdGXAe98Hce/c567Nd4g4rNFSNEbo1kFgbdNfuTNeUld4y", + "4aFN+Evi4salDo/eCuEAt7ZfB24I2Z1eN4PTHT8dLXXtuZNwrreYLS0ucXCXSw2vImePpnfOPf0gZOfy", + "d8EyUXv2H8dWGSbb4jHhPugL9PSZqSNiGa/fFr+Z0/jwYXjUHj6ckt9K9yEAEH+fud9Rvnj4MGpqiGoS", + "zCWBigJOV/CgcfxNbsSXVTtxuB73QJ+uVw1nKdJk2FCoNUx7dF877F1L5vBZuF8KKMH8tD+2rrfpFt0h", + "MGNO0HkqOKbxe1rZmkCKCN5388O4LENaeNmvKGY9t5ab4RHi9QqtHZkqWR63A/OZMtcrt/49pjHBxgmF", + "mRmxZgl3MV6zYCzTbEwavx6QwRxRZKpoJsEWdzPhjnfN2T9rIKwwUs2cgcR3rffUeeEARx0wpEb0HM7l", + "BrZeBO3wt9GDhBn/+zwjArFbCRJ6Ew3Afdmo9f1CG6tZKzMd6pQYzji4uHc4FDr6cNRsAyyWXa+gcXLM", + "mNqQ/qJzpQcSc0RrPTKVzaX4HeK6aFThR2KzfY0Dhp64v0MonoUVzjpXSmOBaktWtrPv2+7xsnFq428t", + "C/tFN2UVbvKYxk/1YRt5E6FXxTOIOiSnhLDQHNn1Vk1cLXi8Av8szGjvXRUot+fJBiZ3gh7ipzIMLzq2", + "47en0sE8CMkq6fWMxtL9G1nIwBRsb8epQgviO/sNUE3YrZ2dBE6FTVtmkxtVINvcFMNEiTeUa+y0oyWa", + "VoBBigpFl6l1BCuViAxT82vKbZlE08/eV663AmsFNb2uhcTUZCru/1FAzlZRdezl5YciH9r6C7ZgtgJg", + "rSAoMecGstVVLRW5Mn1NMLlDzdmcPJoGdS7dbhRszRSblYAtHtsWM6rwuWwskk0Xszzgeqmw+ZMRzZc1", + "LyQUeqksYpUgjeyJTF7jxTQDfQ3AySNs9/g5uY/+W4qt4YHBomOCJiePn6P13f7xKPbKugqOu67sAu/s", + "v7s7O07H6MBmxzCXpBv1KJrFyZZwTr8OO06T7TrmLGFL96DsP0sryukC4i7Dqz0w2b64m2hR7eGFW2sA", + "KC3FljAdnx80NfdTIgzRXH8WDJKL1YrplfPyUWJl6KmtH2cn9cPZYqau9IeHy39EZ7nK+wr1dF1fWIyh", + "q0QYAbo0/kRX0EXrlFCbj65krRurL0hEzny6S6yF0pRAsbgxc5mlIy+JXq1zUknGNeo/aj3P/mLEYklz", + "c/0dpcDNZt8+i9QU6abd54cB/sXxLkGBXMdRLxNk73kW15fc54JnK3OjFA/asN/gVCa9+uL+Wyknst1D", + "j+V8zShZktzqDrnR4Ka+FeHxHQPekhSb9RxEjwev7ItTZi3j5EFrs0M/v3/tuIyVkLEc1u1xdxyHBC0Z", + "rDGII75JZsxb7oUsR+3CbaD/ui4onuUM2DJ/lqOCQGDR3BW/abj4X960yXjRsGqDY3o6QCEj2k6nt/vC", + "Dl+Had369lvrs4PfEpgbjTZb6X2AlYSrrvXFbfp84XDeqLrX7nlH4fj4NyKNDI58/MOHCPTDh1PHBv/2", + "pPvZXu8PH8ZzYkZVbubXFgu3kYixb2wPvxMRBZgvQNU4FLmQ3YgCMvVImQ/mEpy5oaakW+zny3MRdxMM", + "Enf4i5+Cy8sP+MXjAf/oI+IrX5a4ga1Lc/qwd4udRUmmaL4HrsaUfCc2Ywmn9wZ54vkToCiBkpHqOVzJ", + "oJhb1Fy/118koFEz6gxKYYTMsE5FqM//18GzWfx0B7ZrVha/tOmGeg+JpDxfRh01Z6bjr23R9WaJ9qqM", + "pr5fUs6hjA5nZdtfvQwckdL/IcbOs2J8ZNt+MUG73N7iWsC7YHqg/IQGvUyXZoIQq91MLk2kcLkQBcF5", + "2jzr7eU4rMoZlAr7Zw1Kx44GfrDRSmjsMpevrVRFgBeo/ToiP2JOBQNLJ4kuap18esJuqq66KgUtppg2", + "8eL709fEzmr72NLBtlLWApUu3VVEteTjU5c1VYDjMfnjx9kdJGxWrXTWFLaKZT0yLdrSW6znOoHqmBA7", + "R+Sl1YQpr2exkxBMvilXUAR1tKwshjRh/qM1zZeoYuo8ZGmSH1/izVNlq4AP6kU3dRXw3Bm4XZU3W+Rt", + "SoRegrxmCjAKE9bQTbTUZB1zKk6feKm7PFlzbinl6ACeoqmicCjaPXCWIfG24ShkPcQfqGCwFRIPrXh3", + "jr2iaZ775fN6xluftqepA/zG6YhzygVnOSZZjjFEmBRmnLVpRD7quJlITdwJjRyuaNG+Jv7LYTFZxs9f", + "hA5xQ8tt8NVsqqUO+6eGjSvmsgCt3M0GxdTXnnR2DcYVuDoZhojCe1LIiG9K1J+9sYMfSEaY7yGhqPrB", + "fPvJqTExEPqKcVRYOLQ5NttaHkrF0MDICdNkIUC59XSTXqkPps8R5n8qYPPx6LVYsPycLXAM6w1llm1d", + "/4ZDnXpHQOd4Z9q+MG1dVt7m545Xj530tKrcpOnKpPFyzBueRHDM/cT7AwTIbcYPR9tBbjs9ePE9NYQG", + "a3Q+ggrf4QFhNFU6eyWxjYhgKQpbEBubFE3Nx3gEjNeMe0tY/IHIo08Cbgye10Q/lUuqLQs46k67AFom", + "/Ngx1s+aUm87VD8nsUEJrtHPkd7GtsBo4uJoGrSMG+Vb4g+Foe6AmXhBy8YDNlIuFLkqx0QVGCPSKyAa", + "uzjMxe1LFHcfgD1Vyadtd8zzfehLlMp+NKuLBeiMFkWsbMl3+JXgVx/rAxvI66a8RVWRHJN9drOfDqnN", + "TZQLrurVjrl8g1tOF1TkjVBDWBXY7zBmV5ht8d9D6sU3vq8Hx7d5R9fisJS/w3i9GNdraDpTbJGNxwS+", + "KbdHRzv1zQi97X+nlF6KRReQr6EkTdxy4R7F7rfvzcMRpgQcuBnbp6XJ2IcuvQK/+yQXTa6p7q2ET9mg", + "ggkar5s67bvVEOmK61N8/BIxpaHK276vVg2ciizNk4HQVLuULJqSnVdQMs2FdfnsKdGHlqCUm6f18rw7", + "5bNb606Epk0wrzoGF+vq014WSUPLzWwh7QYfagx5tU4FG/sM4Pi9X5H5ClyetkrCmonaO9F4V1YvEtpf", + "O/WNm3Dv6PqjDuJfW/mcVJVfuMp4dplOJn/1izWmEeBabv8EivPBpg9qPQ+5XaueapuQpqjSqCJLnVdx", + "THb8WCJ2xxt2qk3vqZU9IKuXY9iBYe3r6eSsOOjBjCXzn9hRYscuXsk6neu4zW+MR6wSirW1zWIlrkf6", + "jF9gleogV/NwLO9LuIZcY0G71kdKAhySudlM5nX3/y/ncVqcblzrXarjXfmNh1Xs9rzxgxQkQRodWwHs", + "aHw239PGE9YG8lxThbnvJeq4u6GvowPw5nPINVvvSfny9yXwIJ3I1OtlEJZ5kAGGNeEomDH0cK1jC9Cu", + "jCw74Qky998anFQ48hVs7ynSoYZoSbImFusmySIRA3g7ZIZEhIp5mllFsnP+YaqhDMSC9+y03aFNu52s", + "ZhwkMLrhXJ4kzcPRJjXaMWW8nOqouUzXg1J9YWRFKivMsBpjWv54icUvlfNzok2yyVBKJ2fDlPzXLlkl", + "JuhpbCc+bSUo/5vPxmVnKdkVhPWW0VJ1TWXhW0RVL16rk+14jwapXHwlwT7Q82Zm1vrhD23VkSTPGNKS", + "l8KwEVkqLqjr+t74jd1T1sGvzcOCcM1Burr0yP+WQkGmhffb3wXHLlRYL8YbIUElCytY4JLpTt+3+Vyx", + "wAzF9KbUOS+GCyQSVtRAJ4Osq+k5dyH7hf3uY6l9gZG9GqaGXvdXuvMRGEwNkBhS/Zy413J/jPZNlE2M", + "c5CZtzz1U7BykF1rSCVFUef2gQ4PRqOQG50CZcdVEtXT5MNV9mSEINb5CrbHVgjyJQL9DoZAW87Jgh6k", + "7utt8p2q31QM7sWdgPc1NVfTSSVEmSWMHWfDvLF9ir9i+RUUxLwU3lM5Uf2V3Ecde2PNvl5ufZ7UqgIO", + "xYMjQk65jQ3xhu1u4aLe5Pye3jX/BmctapvK2SnVji553MkekyzLW95mfpjdd5gCc9Xdcio7yJ6spJtE", + "zlpJryO1kI/GSuVDU3O/Pm1LVBaKGE9ybi1WL/CgxxRHGMkepFxAQyYlztJFVCliLpk3ibY3Q8UxFU6G", + "AGngY4K+Gyjc4FEERCuuRk6hzWDmcpeJOZHQGpFvmsRtWBw2JtH3Z25m6d53cyGhU+bV9Bay8CwPU209", + "ZipnTEsqtzdJtTYoTjvQniSxvNcdq/HEahfSemMNcViW4jrDyyprcpvHRFvTTnUfY1/Ope1nTvUMAr8u", + "qhyjtiVLWpBcSAl52CMetmehWgkJWSnQzStmgZ5rw3evMFaHk1IsiKhyUYCtERCnoNRcNecU2SYIvGqi", + "KLC0g0Gftk9AxyOnvKvKyDY5j110Zm2ZCcdTUC4Zj8OQbTyEd0dV4YOy85/NUSPE0NelG3ttuc+wtjIc", + "WFqZlaVXGKSqK5OfVY3uSBh4Y6Z4RlZCaSfZ2ZFUM1Tr4nU/F1xLUZZdJZBliRdOs/2Gbk7zXL8W4mpG", + "86sHKEdyoZuVFlMfltp3xmtnkr2MTCPLQF8sI3penMWfuoNrPbub4+ASrQGYH/ffWPt13KexUtbddfVr", + "s/NE7kwtViyP0/C/lndb0ictdiVEUz3ZKkk2OB+b4UUdPg6NMwNeSUM0AzcEG9svd6c5oy5eHua/yPH2", + "xyVzcI9E4mEa3pOOa8nyJG/VAwAhtRGjupa2tFLI+TS3iljYCHM0SfcBHXmLo+fP7WAzI9w5UBpuBdTA", + "27AB8L4V9qc2JZf1XJyJjf/+oM3ZdSPgP++m8lg5+sgpbkjLVcv3+T0SN0I8M/BO/yMsHO5f0P1eSE0Z", + "vJEvagBA2i+pA8Mo76RDwZhTVkKRUZ143FEnNA0kWxfR0i9uypS7yXNqH+wlEDN2LcHlm7Asda8YekUN", + "KYmm+VBzywvYgMJkELaiM1XWzuDtHVDaslI94VtUWQlr6LhruSQYNbJ2bA2+r2o6kwKgQutfXycV80MK", + "3/KeosKtPQs8WcZgN6q5sIi1O0X2qCWiSpQNz+wxUWOPkoFozYqadvCnDmU5umo3c5QjqBrw5JmX28ZO", + "87Md4b0f4NT3j7EyHhMfx91DB19BcdTtuoD2+iXWKnXqedwtMczw0hg0cLaiMXxaEm/vDVXRa55WAA5J", + "vhVvRu4TEzxA7PcbyJGr6frd3R4nBAcjqpe9KcmCy2aHb65I/io0vJOEk+PFRA0FeMHu1NR4unAMOzbA", + "cpbcsL2Ga8YSUu7+d/ffFCvw24GMXG0rWoUS3EvwFjtMKN0YKxxDy5oHzfsXTl0+wb5QzgLP6hXdEiHx", + "HyOv/bOmJZtv8YRa8H03opbUkJAzEVrbtfNXNBPvZkymHjCvFxB+KrtuNnbMYLitGSUA2jyBTjmFmYGu", + "INwGNMvbmyfX5spR9WzFlMLHrredQyy4xfucECtahDIyZqbrlhL1uUpN7/+/jdoKp/IJpaqS5r5+GRBF", + "Vz2FuK1R6IlLL2G1O6xvKB57EmjqHrZEK304b3ED5d6BnhsxX/lUvYcO2IN6cINSF7daxiEFitvI6B0B", + "kaOWcte7MNY/ZAA0Gpl9Vq894NtsjD4D2JfAfzRpZGoZY8D/s+A9UUYvhNdWzPsCWO6E/EdgtXrVmdhk", + "EuZqnyuEVawaQVi2yQK8cpLxXAJV1jfk7K0T2dqciIwbEdJ6LzbWt2aUAuaMt5cl41WtIxIApkbk2wBh", + "oXoa0Zow9qS4BMOGrWn5dg1SsiK1ceZ02DJeYU56r5J3fSPCf/OmDgdgqpV+MJIQ2ki1oJl5wG3VG+tY", + "qDTlBZVF2JxxkoM07z65plt1c9uHgVbWhr/YY/2gATfTjW8P7CBI2haQcuvMl7e0TDQA0js0UYwwLaAH", + "a8SsYJUiWiQsCUMY4mkV6CYrxQLjyxIE6JJPou3HCiuCo8LW8kOHzaPY77B7Gsy77Q6+FjjrmCl2n7O3", + "iDoUeH7mTO88aVab1g/4sx6Z9iB4+ueL1i3cbs6Q/mMxmhcYxNCJ0+wXnfd7bd1D7HyQsGR0NbiJXUQD", + "uQvwDdW14+sZdW3wsUhQK8NmKNuqHY7foFonZ5o7x52h0mcgFFukTF0c7YE6IatJ9u9AAjxbqdadre60", + "jTOFGeeQIlC7I2ezSlRZPsYb0KbmL5xC20HahTFBH4G6OrHuxnFCNcUqOolNOlUrDq2Dlayasc8uU+W7", + "hOyUQiNxg3aV5WKOdxkeYavGwRiPRnkx7UcfdRU2zSVBKJGQ1xIVmtd0u7+uUCIl7PnfTr95/OTXJ998", + "S0wDUrAFqDatcK8uT+sxxnhfz/JlfcQGy9PxTfBx6RZx3lLmw22aTXFnzd62qs0ZOKhKdIgmNPIARI5j", + "pB7MjfYKx2mdvv9c2xVb5J3vWAwFf8yeOc/W+AJOuZNfxJzsvjO6Nf90/L4wzH/kkfJbe4MFpvSx6bjo", + "m9Bjq5D901BhJND7zmivWe4fQXFRLvNm5XNHgTYM+o2QBwKQiObrxGGF1bXbfJXS6nZRC+wNZv1H7E1r", + "SNvrdo6Q+A57wAvD89p2jae0A+crJ3580yAlWMrHFCV0lr8v4s8tsLU8BlvkRF2tQdlrSQyZiyCcU71o", + "oiQTvO0gmBJLaRv5piwjQZhW+sYzFRKOYSzlmpZf/tbAGuuniA8o3qdDL8JIvBDJFpXqZnnAXtNRcwdR", + "d3c3NX+HgZ9/B7NH0XfODeWMjoPXDHUnWNh44V8FG0tKrnFM61Ty+FsycznZKwk5U31jprU4BV6Ba5Bs", + "7hz4YKP3RLrtW+cvQt+CjOfe84D8FBglBCp/WgjbI/qVL5XEyY1SeYz6BmQRwV/sjgprOO55Lm6Zv/tm", + "aSWCBFEHppUYVqccuzybOsE8OrWC4TpHv9Yd3EYe6nZtY3OijE4Dfnn5Qc/GpDKJp+w23TGXyp3k7j4o", + "c/cfkEXF4siN4eaNUcwvqbyaNndkIoVrbz9qVu51M+gk5P08nSyAg2IKU87+6koMfNm31ENgI7uHR9XC", + "ept0FBYxkbV2Jg+mClLtjsiy67pFcupi1FReS6a3WF7Sq2HYr9F8Lz82uQNc7onGAuLePi2uoCnx22Ya", + "qJV/XX8UtMT3yBpmuHmFRHlEvt/QVVU6pSL5673Zf8DTvzwrHj19/B+zvzz65lEOz755/ugRff6MPn7+", + "9DE8+cs3zx7B4/m3z2dPiifPnsyePXn27TfP86fPHs+effv8P+6Ze8iAbAH1GaBPJv8rOy0XIjt9d5Zd", + "GGBbnNCKvQKzNygrzwWWPzNIzfEkwoqycnLif/of/oQd5WLVDu9/nbgyHpOl1pU6OT6+vr4+CrscLzC0", + "ONOizpfHfh4sStXhV96dNT7J1nsCd7TVQeKmOlI4xW/vvz+/IKfvzo5agpmcTB4dPTp67Cqgclqxycnk", + "Kf6Ep2eJ+37siG1y8unzdHK8BFpiJg7zxwq0ZLn/JIEWW/d/dU0XC5BH6HZuf1o/OfZsxfEnF2L9ede3", + "49Awf/ypE4le7OmJRuXjT74O4u7WnRp4zp8n6DASil3NjmdY+2BsU1BB4/RSUNhQx5+QXU7+fux0HvGP", + "KLbY83Ds0zXEW3aw9ElvDKx7emxYEawkpzpf1tXxJ/wPUm8AtE3ld6w3/Bjtb8efOmt1nwdr7f7edg9b", + "rFeiAA+cmM9tfchdn48/2X+DiWBTgWSGLbTpM5ytsTl0Z8XkZPJ90OjFEvKrCdaUQs8vPE1PHj2K5DkN", + "ehF7uOmshMKczGePno3owIUOO7mwnmHHn/kVF9ecYFY8e9PXqxWVW+SgdC25Im9fETYn0J+CKT8D3i50", + "odDCUM9Klk+mkw56Pn52SLNZoI6xitK2xaX/ecvz6I/Dbe5kwEn8fPyp82f3rKhlrQtxHfRFWcsqCobz", + "mY+16v99fE2ZNtyTS6eCxRSHnTXQ8tjlTu792qYrHHzBHIzBj6HjcvTXY+oQOKmEihDje3odKEhPsbFl", + "MUDp7wTe1RNXbqWX6uN4k80YR7r4NGnryLcslv04lNEGb5WRONEi7bVUw1BojMeUgha5kf218GnIJyE/", + "pGUNn6OHCQ/Jox1rcW/QZFw9/G7CyMiKvqMF8WGsGXlDS4MVKMipe8g7S7NH+PGXg+6MW6dKc2QtL/N5", + "OvnmS+LnjBu2m5b+kjHTP/1y05+DXLMcyAWsKiGpZOWW/Mwbv9AbX48/IHFKml8hy9UQrHVikPS662oq", + "42GC3Sz7PmoUiN6QJeVF6QKrRI0FOg1loVZZBNYx86z4KhOVkAiATd8Dhc27oI7I+dKrmjC21Do1Y7Gc", + "NZSiQrUPJqWzk1COaeBxNeH13r3VjQxpDvECeOaukWwmiq2veS3ptd7YGKnBXdUUL49+7PNcsa+O50g0", + "8l5M/nMrf4XyzOTkQyDJfPj4+aP5JtfobvHhU8Cenxwfo1vrUih9PPk8/dRj3cOPHxuE+WJDk0qyNWbT", + "/fj5/wYAAP//423gG8XxAAA=", } // 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 dc48fed4e9..ea4246238c 100644 --- a/daemon/algod/api/server/v2/generated/model/types.go +++ b/daemon/algod/api/server/v2/generated/model/types.go @@ -280,6 +280,30 @@ type Application struct { Params ApplicationParams `json:"params"` } +// ApplicationInitialStates An application's initial global/local/box states that were accessed during simulation. +type ApplicationInitialStates struct { + // AppBoxes An application's global/local/box state. + AppBoxes *ApplicationKVStorage `json:"app-boxes,omitempty"` + + // AppGlobals An application's global/local/box state. + AppGlobals *ApplicationKVStorage `json:"app-globals,omitempty"` + + // AppLocals An application's initial local states tied to different accounts. + AppLocals *[]ApplicationKVStorage `json:"app-locals,omitempty"` + + // Id Application index. + Id uint64 `json:"id"` +} + +// ApplicationKVStorage An application's global/local/box state. +type ApplicationKVStorage struct { + // Account The address of the account associated with the local state. + Account *string `json:"account,omitempty"` + + // Kvs Key-Value pairs representing application states. + Kvs []AvmKeyValue `json:"kvs"` +} + // ApplicationLocalReference References an account's local state for an application. type ApplicationLocalReference struct { // Account Address of the account with the local state. @@ -443,6 +467,14 @@ type AssetParams struct { UrlB64 *[]byte `json:"url-b64,omitempty"` } +// AvmKeyValue Represents an AVM key-value pair in an application store. +type AvmKeyValue struct { + Key []byte `json:"key"` + + // Value Represents an AVM value. + Value AvmValue `json:"value"` +} + // AvmValue Represents an AVM value. type AvmValue struct { // Bytes bytes value. @@ -696,6 +728,12 @@ type ScratchChange struct { Slot uint64 `json:"slot"` } +// SimulateInitialStates Initial states of resources that were accessed during simulation. +type SimulateInitialStates struct { + // AppInitialStates The initial states of accessed application before simulation. The order of this array is arbitrary. + AppInitialStates *[]ApplicationInitialStates `json:"app-initial-states,omitempty"` +} + // SimulateRequest Request type for simulation endpoint. type SimulateRequest struct { // AllowEmptySignatures Allows transactions without signatures to be simulated as if they had correct signatures. @@ -1243,6 +1281,9 @@ type SimulateResponse struct { // ExecTraceConfig An object that configures simulation execution trace. ExecTraceConfig *SimulateTraceConfig `json:"exec-trace-config,omitempty"` + // InitialStates Initial states of resources that were accessed during simulation. + InitialStates *SimulateInitialStates `json:"initial-states,omitempty"` + // LastRound The round immediately preceding this simulation. State changes through this round were used to run this simulation. LastRound uint64 `json:"last-round"` 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 4c1e4862f8..34234f0aea 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go +++ b/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go @@ -131,206 +131,209 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go b/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go index 80f5b62451..223956bf33 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go +++ b/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go @@ -724,283 +724,286 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "//+Ly7CKlp8BAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/algod/api/server/v2/generated/participating/private/routes.go b/daemon/algod/api/server/v2/generated/participating/private/routes.go index 97d1f43bd6..49c6517c19 100644 --- a/daemon/algod/api/server/v2/generated/participating/private/routes.go +++ b/daemon/algod/api/server/v2/generated/participating/private/routes.go @@ -158,209 +158,213 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "16auAQEA", } // 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 eb278fd54e..3003f8b27d 100644 --- a/daemon/algod/api/server/v2/generated/participating/public/routes.go +++ b/daemon/algod/api/server/v2/generated/participating/public/routes.go @@ -232,163 +232,166 @@ var swaggerSpec = []string{ "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", - "ou8//v8AAAD//1ZZbzCCCQEA", + "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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 7b190acd52..1a013b8477 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -1003,6 +1003,7 @@ type PreEncodedSimulateResponse struct { TxnGroups []PreEncodedSimulateTxnGroupResult `codec:"txn-groups"` EvalOverrides *model.SimulationEvalOverrides `codec:"eval-overrides,omitempty"` ExecTraceConfig simulation.ExecTraceConfig `codec:"exec-trace-config,omitempty"` + InitialStates *model.SimulateInitialStates `codec:"initial-states,omitempty"` } // PreEncodedSimulateRequestTransactionGroup mirrors model.SimulateRequestTransactionGroup diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index d59d9c971c..48f89538bb 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -1810,9 +1810,11 @@ func TestGetProofDefault(t *testing.T) { blkHdr, err := l.BlockHdr(1) a.NoError(err) - singleLeafProof, err := merklearray.ProofDataToSingleLeafProof(string(resp.Hashtype), resp.Treedepth, resp.Proof) + singleLeafProof, err := merklearray.ProofDataToSingleLeafProof(string(resp.Hashtype), resp.Proof) a.NoError(err) + a.Equal(uint64(singleLeafProof.TreeDepth), resp.Treedepth) + element := TxnMerkleElemRaw{Txn: crypto.Digest(txid)} copy(element.Stib[:], resp.Stibhash[:]) elems := make(map[uint64]crypto.Hashable) diff --git a/daemon/algod/api/server/v2/utils.go b/daemon/algod/api/server/v2/utils.go index 8cdf37a1d9..bc7e71527c 100644 --- a/daemon/algod/api/server/v2/utils.go +++ b/daemon/algod/api/server/v2/utils.go @@ -79,6 +79,16 @@ func convertSlice[X any, Y any](input []X, fn func(X) Y) []Y { return output } +func convertMap[X comparable, Y, Z any](input map[X]Y, fn func(X, Y) Z) []Z { + output := make([]Z, len(input)) + counter := 0 + for x, y := range input { + output[counter] = fn(x, y) + counter++ + } + return output +} + func uint64Slice[T ~uint64](s []T) []uint64 { return convertSlice(s, func(t T) uint64 { return uint64(t) }) } @@ -494,6 +504,51 @@ func convertUnnamedResourcesAccessed(resources *simulation.ResourceTracker) *mod } } +func convertAppKVStorePtr(address basics.Address, appKVPairs simulation.AppKVPairs) *model.ApplicationKVStorage { + if len(appKVPairs) == 0 && address.IsZero() { + return nil + } + return &model.ApplicationKVStorage{ + Account: addrOrNil(address), + Kvs: convertMap(appKVPairs, func(key string, value basics.TealValue) model.AvmKeyValue { + return model.AvmKeyValue{ + Key: []byte(key), + Value: convertToAVMValue(value), + } + }), + } +} + +func convertAppKVStoreInstance(address basics.Address, appKVPairs simulation.AppKVPairs) model.ApplicationKVStorage { + return model.ApplicationKVStorage{ + Account: addrOrNil(address), + Kvs: convertMap(appKVPairs, func(key string, value basics.TealValue) model.AvmKeyValue { + return model.AvmKeyValue{ + Key: []byte(key), + Value: convertToAVMValue(value), + } + }), + } +} + +func convertApplicationInitialStates(appID basics.AppIndex, states simulation.SingleAppInitialStates) model.ApplicationInitialStates { + return model.ApplicationInitialStates{ + Id: uint64(appID), + AppBoxes: convertAppKVStorePtr(basics.Address{}, states.AppBoxes), + AppGlobals: convertAppKVStorePtr(basics.Address{}, states.AppGlobals), + AppLocals: sliceOrNil(convertMap(states.AppLocals, convertAppKVStoreInstance)), + } +} + +func convertSimulateInitialStates(initialStates *simulation.ResourcesInitialStates) *model.SimulateInitialStates { + if initialStates == nil { + return nil + } + return &model.SimulateInitialStates{ + AppInitialStates: sliceOrNil(convertMap(initialStates.AllAppsInitialStates, convertApplicationInitialStates)), + } +} + func convertTxnGroupResult(txnGroupResult simulation.TxnGroupResult) PreEncodedSimulateTxnGroupResult { txnResults := make([]PreEncodedSimulateTxnResult, len(txnGroupResult.Txns)) for i, txnResult := range txnGroupResult.Txns { @@ -528,19 +583,14 @@ func convertSimulationResult(result simulation.Result) PreEncodedSimulateRespons } } - encodedSimulationResult := PreEncodedSimulateResponse{ + return PreEncodedSimulateResponse{ Version: result.Version, LastRound: uint64(result.LastRound), - TxnGroups: make([]PreEncodedSimulateTxnGroupResult, len(result.TxnGroups)), + TxnGroups: convertSlice(result.TxnGroups, convertTxnGroupResult), EvalOverrides: evalOverrides, ExecTraceConfig: result.TraceConfig, + InitialStates: convertSimulateInitialStates(result.InitialStates), } - - for i, txnGroup := range result.TxnGroups { - encodedSimulationResult.TxnGroups[i] = convertTxnGroupResult(txnGroup) - } - - return encodedSimulationResult } func convertSimulationRequest(request PreEncodedSimulateRequest) simulation.Request { diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 77b8209d96..a5a4715579 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -1778,14 +1778,14 @@ func mergeProtos(specs map[int]OpSpec) (Proto, uint64, bool) { } } if debugExplainFuncPtr == nil { - debugExplainFuncPtr = spec.Explain + debugExplainFuncPtr = spec.StackExplain } i++ } return Proto{ - Arg: typedList{args, ""}, - Return: typedList{returns, ""}, - Explain: debugExplainFuncPtr, + Arg: typedList{args, ""}, + Return: typedList{returns, ""}, + StackExplain: debugExplainFuncPtr, }, minVersion, true } diff --git a/data/transactions/logic/opcodeExplain.go b/data/transactions/logic/opcodeExplain.go index ec95d97d1b..4b4f965a65 100644 --- a/data/transactions/logic/opcodeExplain.go +++ b/data/transactions/logic/opcodeExplain.go @@ -32,6 +32,9 @@ const ( // AppStateDelete stands for deleting an app state. AppStateDelete + + // AppStateRead stands for reading from an app state. + AppStateRead ) // AppStateEnum stands for the enum of app state type, should be one of global/local/box. @@ -165,6 +168,20 @@ func opMatchStackChange(cx *EvalContext) (deletions, additions int) { return } +func opBoxExtractStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { + last := len(cx.Stack) - 1 // length + prev := last - 1 // start + pprev := prev - 1 // name + + return BoxState, AppStateRead, cx.appID, basics.Address{}, string(cx.Stack[pprev].Bytes) +} + +func opBoxGetStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { + last := len(cx.Stack) - 1 // name + + return BoxState, AppStateRead, cx.appID, basics.Address{}, string(cx.Stack[last].Bytes) +} + func opBoxCreateStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { last := len(cx.Stack) - 1 // size prev := last - 1 // name @@ -187,9 +204,51 @@ func opBoxDelStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics. } func opBoxPutStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { - last := len(cx.Stack) - 1 // name + last := len(cx.Stack) - 1 // value + prev := last - 1 // name + + return BoxState, AppStateWrite, cx.appID, basics.Address{}, string(cx.Stack[prev].Bytes) +} + +func opAppLocalGetStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { + last := len(cx.Stack) - 1 // state key + prev := last - 1 // account + + // NOTE: we swallow the error of finding account ref, for eventually it would error in execution time, + // and we don't have to complain here. + var addr basics.Address + addr, _, _, _ = cx.localsReference(cx.Stack[prev], 0) + + return LocalState, AppStateRead, cx.appID, addr, string(cx.Stack[last].Bytes) +} + +func opAppLocalGetExStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { + last := len(cx.Stack) - 1 // state key + prev := last - 1 // app id + pprev := prev - 1 // account + + // NOTE: we swallow the error of finding account ref, for eventually it would error in execution time, + // and we don't have to complain here. + addr, appID, _, _ := cx.localsReference(cx.Stack[pprev], cx.Stack[prev].Uint) + + return LocalState, AppStateRead, appID, addr, string(cx.Stack[last].Bytes) +} + +func opAppGlobalGetStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { + last := len(cx.Stack) - 1 // state key + + return GlobalState, AppStateRead, cx.appID, basics.Address{}, string(cx.Stack[last].Bytes) +} + +func opAppGlobalGetExStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { + last := len(cx.Stack) - 1 // state key + prev := last - 1 // app id - return BoxState, AppStateWrite, cx.appID, basics.Address{}, string(cx.Stack[last].Bytes) + // NOTE: we swallow the error of finding application ID, for eventually it would error in execution time, + // and we don't have to complain here. + appID, _ := cx.appReference(cx.Stack[prev].Uint, true) + + return GlobalState, AppStateRead, appID, basics.Address{}, string(cx.Stack[last].Bytes) } func opAppLocalPutStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, basics.AppIndex, basics.Address, string) { @@ -230,18 +289,15 @@ func opAppGlobalDelStateChange(cx *EvalContext) (AppStateEnum, AppStateOpEnum, b return GlobalState, AppStateDelete, cx.appID, basics.Address{}, string(cx.Stack[last].Bytes) } -// AppNewStateQuerying is used only for simulation endpoint exec trace export: +// AppStateQuerying is used for simulation endpoint exec trace export: // it reads *new* app state after opcode that writes to app-state. // Since it is collecting new/updated app state, we don't have to error again here, // and thus we omit the error or non-existence case, just returning empty TealValue. // Otherwise, we find the updated new state value, and wrap up with new TealValue. -func AppNewStateQuerying( +func AppStateQuerying( cx *EvalContext, appState AppStateEnum, stateOp AppStateOpEnum, appID basics.AppIndex, account basics.Address, key string) basics.TealValue { - if stateOp != AppStateWrite { - return basics.TealValue{} - } switch appState { case BoxState: boxBytes, exists, err := cx.Ledger.GetBox(appID, key) @@ -259,7 +315,17 @@ func AppNewStateQuerying( } return globalValue case LocalState: - addr, acctID, err := cx.mutableAccountReference(stackValue{Bytes: account[:]}) + var ( + addr basics.Address + acctID uint64 + err error + ) + switch stateOp { + case AppStateWrite, AppStateDelete: + addr, acctID, err = cx.mutableAccountReference(stackValue{Bytes: account[:]}) + default: + addr, _, acctID, err = cx.localsReference(stackValue{Bytes: account[:]}, uint64(appID)) + } if err != nil { return basics.TealValue{} } diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index 4b6a9000eb..e005d57cf3 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -392,24 +392,24 @@ type Proto struct { Arg typedList // what gets popped from the stack Return typedList // what gets pushed to the stack - // Explain is the pointer to the function used in debugging process during simulation: - // - on default construction, Explain relies on Arg and Return count. + // StackExplain is the pointer to the function used in debugging process during simulation: + // - on default construction, StackExplain relies on Arg and Return count. // - otherwise, we need to explicitly infer from EvalContext, by registering through explain function - Explain debugStackExplain + StackExplain debugStackExplain - // StateExplain is the pointer to the function used for debugging in simulation: + // AppStateExplain is the pointer to the function used for debugging in simulation: // - for an opcode not touching app's local/global/box state, this pointer is nil. // - otherwise, we call this method and check the operation of an opcode on app's state. - StateExplain stateChangeExplain + AppStateExplain stateChangeExplain } func (p Proto) stackExplain(e debugStackExplain) Proto { - p.Explain = e + p.StackExplain = e return p } -func (p Proto) stateExplain(s stateChangeExplain) Proto { - p.StateExplain = s +func (p Proto) appStateExplain(s stateChangeExplain) Proto { + p.AppStateExplain = s return p } @@ -442,9 +442,9 @@ func proto(signature string, effects ...string) Proto { retTypes := parseStackTypes(parts[1]) debugExplainFunc := defaultDebugExplain(len(filterNoneTypes(argTypes)), len(filterNoneTypes(retTypes))) return Proto{ - Arg: typedList{argTypes, argEffect}, - Return: typedList{retTypes, retEffect}, - Explain: debugExplainFunc, + Arg: typedList{argTypes, argEffect}, + Return: typedList{retTypes, retEffect}, + StackExplain: debugExplainFunc, } } @@ -618,18 +618,18 @@ var OpSpecs = []OpSpec{ {0x60, "balance", opBalance, proto("a:i"), directRefEnabledVersion, only(ModeApp)}, {0x61, "app_opted_in", opAppOptedIn, proto("ii:T"), 2, only(ModeApp)}, {0x61, "app_opted_in", opAppOptedIn, proto("ai:T"), directRefEnabledVersion, only(ModeApp)}, - {0x62, "app_local_get", opAppLocalGet, proto("ib:a"), 2, only(ModeApp)}, - {0x62, "app_local_get", opAppLocalGet, proto("ab:a"), directRefEnabledVersion, only(ModeApp)}, - {0x63, "app_local_get_ex", opAppLocalGetEx, proto("iib:aT"), 2, only(ModeApp)}, - {0x63, "app_local_get_ex", opAppLocalGetEx, proto("aib:aT"), directRefEnabledVersion, only(ModeApp)}, - {0x64, "app_global_get", opAppGlobalGet, proto("b:a"), 2, only(ModeApp)}, - {0x65, "app_global_get_ex", opAppGlobalGetEx, proto("ib:aT"), 2, only(ModeApp)}, - {0x66, "app_local_put", opAppLocalPut, proto("iba:").stateExplain(opAppLocalPutStateChange), 2, only(ModeApp)}, - {0x66, "app_local_put", opAppLocalPut, proto("aba:").stateExplain(opAppLocalPutStateChange), directRefEnabledVersion, only(ModeApp)}, - {0x67, "app_global_put", opAppGlobalPut, proto("ba:").stateExplain(opAppGlobalPutStateChange), 2, only(ModeApp)}, - {0x68, "app_local_del", opAppLocalDel, proto("ib:").stateExplain(opAppLocalDelStateChange), 2, only(ModeApp)}, - {0x68, "app_local_del", opAppLocalDel, proto("ab:").stateExplain(opAppLocalDelStateChange), directRefEnabledVersion, only(ModeApp)}, - {0x69, "app_global_del", opAppGlobalDel, proto("b:").stateExplain(opAppGlobalDelStateChange), 2, only(ModeApp)}, + {0x62, "app_local_get", opAppLocalGet, proto("ib:a").appStateExplain(opAppLocalGetStateChange), 2, only(ModeApp)}, + {0x62, "app_local_get", opAppLocalGet, proto("ab:a").appStateExplain(opAppLocalGetStateChange), directRefEnabledVersion, only(ModeApp)}, + {0x63, "app_local_get_ex", opAppLocalGetEx, proto("iib:aT").appStateExplain(opAppLocalGetExStateChange), 2, only(ModeApp)}, + {0x63, "app_local_get_ex", opAppLocalGetEx, proto("aib:aT").appStateExplain(opAppLocalGetExStateChange), directRefEnabledVersion, only(ModeApp)}, + {0x64, "app_global_get", opAppGlobalGet, proto("b:a").appStateExplain(opAppGlobalGetStateChange), 2, only(ModeApp)}, + {0x65, "app_global_get_ex", opAppGlobalGetEx, proto("ib:aT").appStateExplain(opAppGlobalGetExStateChange), 2, only(ModeApp)}, + {0x66, "app_local_put", opAppLocalPut, proto("iba:").appStateExplain(opAppLocalPutStateChange), 2, only(ModeApp)}, + {0x66, "app_local_put", opAppLocalPut, proto("aba:").appStateExplain(opAppLocalPutStateChange), directRefEnabledVersion, only(ModeApp)}, + {0x67, "app_global_put", opAppGlobalPut, proto("ba:").appStateExplain(opAppGlobalPutStateChange), 2, only(ModeApp)}, + {0x68, "app_local_del", opAppLocalDel, proto("ib:").appStateExplain(opAppLocalDelStateChange), 2, only(ModeApp)}, + {0x68, "app_local_del", opAppLocalDel, proto("ab:").appStateExplain(opAppLocalDelStateChange), directRefEnabledVersion, only(ModeApp)}, + {0x69, "app_global_del", opAppGlobalDel, proto("b:").appStateExplain(opAppGlobalDelStateChange), 2, only(ModeApp)}, {0x70, "asset_holding_get", opAssetHoldingGet, proto("ii:aT"), 2, field("f", &AssetHoldingFields).only(ModeApp)}, {0x70, "asset_holding_get", opAssetHoldingGet, proto("ai:aT"), directRefEnabledVersion, field("f", &AssetHoldingFields).only(ModeApp)}, {0x71, "asset_params_get", opAssetParamsGet, proto("i:aT"), 2, field("f", &AssetParamsFields).only(ModeApp)}, @@ -701,13 +701,13 @@ var OpSpecs = []OpSpec{ {0xb8, "gitxna", opGitxna, proto(":a"), 6, immediates("t", "f", "i").field("f", &TxnArrayFields).only(ModeApp)}, // Unlimited Global Storage - Boxes - {0xb9, "box_create", opBoxCreate, proto("Ni:T").stateExplain(opBoxCreateStateChange), boxVersion, only(ModeApp)}, - {0xba, "box_extract", opBoxExtract, proto("Nii:b"), boxVersion, only(ModeApp)}, - {0xbb, "box_replace", opBoxReplace, proto("Nib:").stateExplain(opBoxReplaceStateChange), boxVersion, only(ModeApp)}, - {0xbc, "box_del", opBoxDel, proto("N:T").stateExplain(opBoxDelStateChange), boxVersion, only(ModeApp)}, - {0xbd, "box_len", opBoxLen, proto("N:iT"), boxVersion, only(ModeApp)}, - {0xbe, "box_get", opBoxGet, proto("N:bT"), boxVersion, only(ModeApp)}, - {0xbf, "box_put", opBoxPut, proto("Nb:").stateExplain(opBoxPutStateChange), boxVersion, only(ModeApp)}, + {0xb9, "box_create", opBoxCreate, proto("Ni:T").appStateExplain(opBoxCreateStateChange), boxVersion, only(ModeApp)}, + {0xba, "box_extract", opBoxExtract, proto("Nii:b").appStateExplain(opBoxExtractStateChange), boxVersion, only(ModeApp)}, + {0xbb, "box_replace", opBoxReplace, proto("Nib:").appStateExplain(opBoxReplaceStateChange), boxVersion, only(ModeApp)}, + {0xbc, "box_del", opBoxDel, proto("N:T").appStateExplain(opBoxDelStateChange), boxVersion, only(ModeApp)}, + {0xbd, "box_len", opBoxLen, proto("N:iT").appStateExplain(opBoxGetStateChange), boxVersion, only(ModeApp)}, + {0xbe, "box_get", opBoxGet, proto("N:bT").appStateExplain(opBoxGetStateChange), boxVersion, only(ModeApp)}, + {0xbf, "box_put", opBoxPut, proto("Nb:").appStateExplain(opBoxPutStateChange), boxVersion, only(ModeApp)}, // Dynamic indexing {0xc0, "txnas", opTxnas, proto("i:a"), 5, field("f", &TxnArrayFields)}, diff --git a/docker/README.md b/docker/README.md index d218b10139..68b43feee7 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,7 @@ General purpose algod container image. ## Image Configuration -Algorand maintains a Docker image with recent snapshot builds from our `master` branch on DockerHub to support users who prefer to run containerized processes. There are a couple of different images avaliable for running the latest stable or development versions of Algod. +Algorand maintains a Docker image with recent snapshot builds from our `master` branch on DockerHub to support users who prefer to run containerized processes. There are a couple of different images available for running the latest stable or development versions of Algod. - `algorand/algod:latest` is the latest stable release version of Algod (default) - `algorand/algod:stable` is the latest stable version of Algod diff --git a/go.mod b/go.mod index d45f221f5a..3abece643d 100644 --- a/go.mod +++ b/go.mod @@ -26,10 +26,12 @@ require ( github.com/google/go-cmp v0.5.9 github.com/google/go-querystring v1.0.0 github.com/gorilla/mux v1.8.0 + github.com/ipfs/go-log v1.0.5 github.com/jmoiron/sqlx v1.2.0 github.com/karalabe/usb v0.0.2 github.com/labstack/echo/v4 v4.9.1 github.com/libp2p/go-libp2p v0.29.1 + github.com/libp2p/go-libp2p-kad-dht v0.24.3 github.com/libp2p/go-libp2p-pubsub v0.9.3 github.com/libp2p/go-yamux/v4 v4.0.1 github.com/mattn/go-sqlite3 v1.14.16 @@ -70,6 +72,8 @@ require ( github.com/flynn/noise v1.0.0 // indirect github.com/fortytw2/leaktest v1.3.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.19.5 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect @@ -82,14 +86,21 @@ require ( github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect github.com/huin/goupnp v1.2.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/invopop/yaml v0.1.0 // indirect + github.com/ipfs/boxo v0.10.0 // indirect github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-datastore v0.6.0 // indirect github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/ipld/go-ipld-prime v0.20.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/jbenet/goprocess v0.1.4 // indirect github.com/jmespath/go-jmespath v0.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.16.7 // indirect @@ -102,6 +113,8 @@ require ( github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect + github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect + github.com/libp2p/go-libp2p-record v0.2.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-nat v0.2.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect @@ -127,14 +140,16 @@ require ( github.com/multiformats/go-varint v0.0.7 // indirect github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/polydawn/refmt v0.89.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-19 v0.3.3 // indirect github.com/quic-go/qtls-go1-20 v0.2.3 // indirect @@ -148,6 +163,11 @@ require ( github.com/stretchr/objx v0.5.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.16.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/fx v1.20.0 // indirect @@ -159,6 +179,7 @@ require ( golang.org/x/term v0.10.0 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect golang.org/x/tools v0.11.0 // indirect + gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index cf7902eaa7..475b233299 100644 --- a/go.sum +++ b/go.sum @@ -2,45 +2,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= @@ -50,11 +18,6 @@ github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4K github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/algorand/avm-abi v0.2.0 h1:bkjsG+BOEcxUcnGSALLosmltE0JZdg+ZisXKx0UDX2k= github.com/algorand/avm-abi v0.2.0/go.mod h1:+CgwM46dithy850bpTeHh9MC99zpn2Snirb3QTl2O/g= github.com/algorand/falcon v0.1.0 h1:xl832kfZ7hHG6B4p90DQynjfKFGbIUgUOnsRiMZXfAo= @@ -87,7 +50,6 @@ github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo= @@ -96,14 +58,9 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chrismcguire/gobberish v0.0.0-20150821175641-1d8adb509a0e h1:CHPYEbz71w8DqJ7DRIq+MXyCQsdibK08vdcQTY4ufas= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -181,6 +138,7 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= @@ -193,18 +151,11 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -213,9 +164,9 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= @@ -235,25 +186,17 @@ github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -263,52 +206,40 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -316,9 +247,14 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -326,26 +262,38 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/ipfs/boxo v0.10.0 h1:tdDAxq8jrsbRkYoF+5Rcqyeb91hgWe2hp7iLu7ORZLY= +github.com/ipfs/boxo v0.10.0/go.mod h1:Fg+BnfxZ0RPzR0nOodzdIq3A7KgoWAOWsEIImrIQdBM= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= +github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g= +github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= +github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= @@ -353,20 +301,14 @@ github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/karalabe/usb v0.0.2 h1:M6QQBNxF+CQ8OFvxrT90BA0qBOXymndZnk5q235mFc4= github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= @@ -384,11 +326,8 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -417,8 +356,14 @@ github.com/libp2p/go-libp2p v0.29.1 h1:yNeg6XgP8gbdc4YSrwiIt5T1TGOrVjH8dzl8h0GIO github.com/libp2p/go-libp2p v0.29.1/go.mod h1:20El+LLy3/YhdUYIvGbLnvVJN32nMdqY6KXBENRAfLY= github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= +github.com/libp2p/go-libp2p-kad-dht v0.24.3 h1:VjxtDVWaaf4UFjGBf+yl2JCiGaHx7+ctAUa9oJCR3QE= +github.com/libp2p/go-libp2p-kad-dht v0.24.3/go.mod h1:BShPzRbK6+fN3hk8a0WGAYKpb8m4k+DtchkqouGTrSg= +github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0= +github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0= github.com/libp2p/go-libp2p-pubsub v0.9.3 h1:ihcz9oIBMaCK9kcx+yHWm3mLAFBMAUsM4ux42aikDxo= github.com/libp2p/go-libp2p-pubsub v0.9.3/go.mod h1:RYA7aM9jIic5VV47WXu4GkcRxRhrdElWf8xtyli+Dzc= +github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= +github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= @@ -480,11 +425,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= @@ -515,8 +457,6 @@ github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqd github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= @@ -535,6 +475,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= @@ -544,41 +486,26 @@ github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCr github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= +github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-19 v0.3.3 h1:wznEHvJwd+2X3PqftRha0SUKmGsnb6dfArMhy9PeJVE= @@ -625,14 +552,15 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go. github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= @@ -649,7 +577,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -661,12 +588,14 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -677,6 +606,10 @@ github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+ github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -685,18 +618,20 @@ github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmv github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -706,21 +641,22 @@ go.uber.org/fx v1.20.0 h1:ZMC/pnRvhsthOZh9MZjMq5U8Or3mA9zBSPaLnzs3ihQ= go.uber.org/fx v1.20.0/go.mod h1:qCUj0btiR3/JnanEr1TYEePfSw6o/4qYJscgvzQ5Ub0= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -731,36 +667,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -772,7 +688,6 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -780,48 +695,24 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -829,84 +720,49 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -917,8 +773,6 @@ golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -929,45 +783,15 @@ golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -978,83 +802,33 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM= +gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1063,13 +837,11 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1087,7 +859,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -1101,18 +872,12 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= pgregory.net/rapid v0.6.2 h1:ErW5sL+UKtfBfUTsWHDCoeB+eZKLKMxrSd1VJY6W4bw= pgregory.net/rapid v0.6.2/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= diff --git a/installer/config.json.example b/installer/config.json.example index 62cbe6427b..01192f4ecc 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -43,8 +43,9 @@ "EnableAgreementTimeMetrics": false, "EnableAssembleStats": false, "EnableBlockService": false, - "EnableBlockServiceFallbackToArchiver": true, + "EnableBlockServiceFallbackToArchiver": false, "EnableCatchupFromArchiveServers": false, + "EnableDHTProviders": false, "EnableDeveloperAPI": false, "EnableExperimentalAPI": false, "EnableFollowMode": false, diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 7e71a50f74..ca9cd55f72 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -93,7 +93,7 @@ const initializingAccountCachesMessageTimeout = 3 * time.Second // where we end up batching up to 1000 rounds in a single update. const accountsUpdatePerRoundHighWatermark = 1 * time.Second -// forceCatchpointFileGeneration defines the CatchpointTracking mode that would be used to +// forceCatchpointFileGenerationTrackingMode defines the CatchpointTracking mode that would be used to // force a node to generate catchpoint files. const forceCatchpointFileGenerationTrackingMode = 99 diff --git a/ledger/catchpointtracker.go b/ledger/catchpointtracker.go index 5f8e48b005..d3bf1f87ea 100644 --- a/ledger/catchpointtracker.go +++ b/ledger/catchpointtracker.go @@ -163,34 +163,16 @@ func (ct *catchpointTracker) initialize(cfg config.Local, paths DirsAndPrefix) { // the temp file uses the hot data directories ct.tmpDir = paths.HotGenesisDir - switch cfg.CatchpointTracking { - case -1: - // No catchpoints. - default: - // Give a warning, then fall through to case 0. - logging.Base().Warnf("catchpointTracker: the CatchpointTracking field in the config.json file contains an invalid value (%d). The default value of 0 would be used instead.", cfg.CatchpointTracking) - fallthrough - case 0: - if cfg.Archival && (cfg.CatchpointInterval > 0) { - ct.catchpointInterval = cfg.CatchpointInterval - ct.enableGeneratingCatchpointFiles = true - } - case 1: - if cfg.CatchpointInterval > 0 { - ct.catchpointInterval = cfg.CatchpointInterval - ct.enableGeneratingCatchpointFiles = cfg.Archival - } - case 2: - if cfg.CatchpointInterval > 0 { - ct.catchpointInterval = cfg.CatchpointInterval - ct.enableGeneratingCatchpointFiles = true - } - case forceCatchpointFileGenerationTrackingMode: - if cfg.CatchpointInterval > 0 { - ct.catchpointInterval = cfg.CatchpointInterval - ct.enableGeneratingCatchpointFiles = true - ct.forceCatchpointFileWriting = true - } + if cfg.TracksCatchpoints() { + ct.catchpointInterval = cfg.CatchpointInterval + } + ct.enableGeneratingCatchpointFiles = cfg.StoresCatchpoints() + + // Overwrite previous options if forceCatchpointFileGenerationTrackingMode + if cfg.CatchpointTracking == forceCatchpointFileGenerationTrackingMode && cfg.CatchpointInterval > 0 { + ct.catchpointInterval = cfg.CatchpointInterval + ct.forceCatchpointFileWriting = true + ct.enableGeneratingCatchpointFiles = true } ct.catchpointFileHistoryLength = cfg.CatchpointFileHistoryLength diff --git a/ledger/simulation/initialStates.go b/ledger/simulation/initialStates.go new file mode 100644 index 0000000000..e374719ccc --- /dev/null +++ b/ledger/simulation/initialStates.go @@ -0,0 +1,179 @@ +// Copyright (C) 2019-2023 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 simulation + +import ( + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/util" +) + +// AppKVPairs constructs a KV pair between state key and state value +type AppKVPairs map[string]basics.TealValue + +// SingleAppInitialStates gathers all relevant application on-chain states, including +// - Application Box states +// - Application Global states +// - Application Local states (which is tied to basics.Address) +type SingleAppInitialStates struct { + AppBoxes AppKVPairs + CreatedBoxes util.Set[string] + + AppGlobals AppKVPairs + CreatedGlobals util.Set[string] + + AppLocals map[basics.Address]AppKVPairs + CreatedLocals map[basics.Address]util.Set[string] +} + +// AppsInitialStates maintains a map from basics.AppIndex to SingleAppInitialStates +type AppsInitialStates map[basics.AppIndex]SingleAppInitialStates + +// ResourcesInitialStates gathers all initial states of resources that were accessed during simulation +type ResourcesInitialStates struct { + // AllAppsInitialStates gathers all initial states of apps that were touched (but not created) during simulation + AllAppsInitialStates AppsInitialStates + // CreatedApp gathers all created applications by appID, blocking initial app states in these apps being recorded + CreatedApp util.Set[basics.AppIndex] +} + +func newResourcesInitialStates(request Request) *ResourcesInitialStates { + if !request.TraceConfig.State { + return nil + } + return &ResourcesInitialStates{ + AllAppsInitialStates: make(AppsInitialStates), + CreatedApp: make(util.Set[basics.AppIndex]), + } +} + +// hasBeenRecorded checks if an application state kv-pair has been recorded in SingleAppInitialStates. +func (appIS SingleAppInitialStates) hasBeenRecorded(state logic.AppStateEnum, key string, addr basics.Address) (recorded bool) { + switch state { + case logic.BoxState: + _, recorded = appIS.AppBoxes[key] + case logic.GlobalState: + _, recorded = appIS.AppGlobals[key] + case logic.LocalState: + if kvs, addrLocalExists := appIS.AppLocals[addr]; addrLocalExists { + _, recorded = kvs[key] + } + } + return +} + +// hasBeenCreated checks if an application state kv-pair has been created during simulation. +func (appIS SingleAppInitialStates) hasBeenCreated(state logic.AppStateEnum, key string, addr basics.Address) (created bool) { + switch state { + case logic.BoxState: + created = appIS.CreatedBoxes.Contains(key) + case logic.GlobalState: + created = appIS.CreatedGlobals.Contains(key) + case logic.LocalState: + if kvs, addrLocalExists := appIS.CreatedLocals[addr]; addrLocalExists { + created = kvs.Contains(key) + } + } + return +} + +// recordCreation records a newly created application state kv-pair in SingleAppInitialStates during simulation. +func (appIS SingleAppInitialStates) recordCreation(state logic.AppStateEnum, key string, addr basics.Address) { + switch state { + case logic.BoxState: + appIS.CreatedBoxes.Add(key) + case logic.GlobalState: + appIS.CreatedGlobals.Add(key) + case logic.LocalState: + if _, addrLocalExists := appIS.CreatedLocals[addr]; !addrLocalExists { + appIS.CreatedLocals[addr] = make(util.Set[string]) + } + appIS.CreatedLocals[addr].Add(key) + } +} + +func (appsIS AppsInitialStates) increment(cx *logic.EvalContext) { + appState, stateOp, appID, acctAddr, stateKey := cx.GetOpSpec().AppStateExplain(cx) + // No matter read or write, once this code-path is triggered, something must be recorded into initial state + if _, ok := appsIS[appID]; !ok { + appsIS[appID] = SingleAppInitialStates{ + AppGlobals: make(AppKVPairs), + CreatedGlobals: make(util.Set[string]), + + AppBoxes: make(AppKVPairs), + CreatedBoxes: make(util.Set[string]), + + AppLocals: make(map[basics.Address]AppKVPairs), + CreatedLocals: make(map[basics.Address]util.Set[string]), + } + } + + // if the state has been recorded, pass + if appsIS[appID].hasBeenRecorded(appState, stateKey, acctAddr) { + return + } + + // if this state is created during simulation, pass + if appsIS[appID].hasBeenCreated(appState, stateKey, acctAddr) { + return + } + + tv := logic.AppStateQuerying(cx, appState, stateOp, appID, acctAddr, stateKey) + switch stateOp { + case logic.AppStateWrite: + // if the unrecorded value to write to is nil, pass + // this case means it is creating a state + if tv == (basics.TealValue{}) { + appsIS[appID].recordCreation(appState, stateKey, acctAddr) + return + } + fallthrough + case logic.AppStateDelete: + fallthrough + case logic.AppStateRead: + switch appState { + case logic.BoxState: + appsIS[appID].AppBoxes[stateKey] = tv + case logic.GlobalState: + appsIS[appID].AppGlobals[stateKey] = tv + case logic.LocalState: + if appsIS[appID].AppLocals[acctAddr] == nil { + appsIS[appID].AppLocals[acctAddr] = make(AppKVPairs) + } + appsIS[appID].AppLocals[acctAddr][stateKey] = tv + } + } +} + +// increment is the entry point of (potentially) adding new initial states to ResourcesInitialStates during simulation. +// This method is the top entry point of simulate-initial-state, for ResourcesInitialStates captures all initial states. +// By checking if current opcode has related `Explain` function, this method dispatch incrementing initial states by: +// +- AppStateExplain exists, then dispatch to AppsInitialStates.increment. +func (is *ResourcesInitialStates) increment(cx *logic.EvalContext) { + // This method only applies on logic.ModeApp + if cx.RunMode() == logic.ModeSig { + return + } + // If this method triggers application state changes + if cx.GetOpSpec().AppStateExplain != nil { + if is.CreatedApp.Contains(cx.AppID()) { + return + } + is.AllAppsInitialStates.increment(cx) + } + // TODO asset? +} diff --git a/ledger/simulation/simulation_eval_test.go b/ledger/simulation/simulation_eval_test.go index ab668295dc..3fbe61512c 100644 --- a/ledger/simulation/simulation_eval_test.go +++ b/ledger/simulation/simulation_eval_test.go @@ -38,6 +38,7 @@ import ( ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" + "github.com/algorand/go-algorand/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -2306,6 +2307,7 @@ byte "hello"; log; int 1`, TraceConfig: simulation.ExecTraceConfig{ Enable: true, Stack: true, + State: true, }, }, developerAPI: true, @@ -2315,6 +2317,7 @@ byte "hello"; log; int 1`, TraceConfig: simulation.ExecTraceConfig{ Enable: true, Stack: true, + State: true, }, TxnGroups: []simulation.TxnGroupResult{ { @@ -2390,6 +2393,10 @@ byte "hello"; log; int 1`, AppBudgetConsumed: 3, }, }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: simulation.AppsInitialStates{}, + CreatedApp: util.MakeSet(basics.AppIndex(1002)), + }, }, } }) @@ -3394,6 +3401,10 @@ int 1`, AppBudgetConsumed: 44, }, }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: make(simulation.AppsInitialStates), + CreatedApp: util.MakeSet(futureAppID), + }, }, } }) @@ -3404,17 +3415,7 @@ func TestAppLocalGlobalStateChange(t *testing.T) { t.Parallel() simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { - sender := env.Accounts[0] - - futureAppID := basics.AppIndex(1001) - - createTxn := env.TxnInfo.NewTxn(txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: sender.Addr, - ApplicationID: 0, - GlobalStateSchema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, - LocalStateSchema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, - ApprovalProgram: `#pragma version 8 + approvalProgramSrc := `#pragma version 8 txn ApplicationID bz end // Do nothing during create @@ -3451,12 +3452,19 @@ global: end: int 1 -`, +` + + sender := env.Accounts[0] + + createdAppID := env.CreateApp(sender.Addr, simulationtesting.AppParams{ + GlobalStateSchema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, + LocalStateSchema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, + ApprovalProgram: approvalProgramSrc, ClearStateProgram: `#pragma version 8 int 1`, }) - op, err := logic.AssembleString(createTxn.ApprovalProgram.(string)) + op, err := logic.AssembleString(approvalProgramSrc) require.NoError(t, err) progHash := crypto.Hash(op.Program) @@ -3464,26 +3472,25 @@ int 1`, Type: protocol.ApplicationCallTx, OnCompletion: transactions.OptInOC, Sender: sender.Addr, - ApplicationID: futureAppID, + ApplicationID: createdAppID, }) globalStateCall := env.TxnInfo.NewTxn(txntest.Txn{ Type: protocol.ApplicationCallTx, Sender: sender.Addr, - ApplicationID: futureAppID, + ApplicationID: createdAppID, ApplicationArgs: [][]byte{[]byte("global")}, }) localStateCall := env.TxnInfo.NewTxn(txntest.Txn{ Type: protocol.ApplicationCallTx, Sender: sender.Addr, - ApplicationID: futureAppID, + ApplicationID: createdAppID, ApplicationArgs: [][]byte{[]byte("local")}, }) - txntest.Group(&createTxn, &optIn, &globalStateCall, &localStateCall) + txntest.Group(&optIn, &globalStateCall, &localStateCall) - signedCreate := createTxn.Txn().Sign(sender.Sk) signedOptin := optIn.Txn().Sign(sender.Sk) signedGlobalStateCall := globalStateCall.Txn().Sign(sender.Sk) signedLocalStateCall := localStateCall.Txn().Sign(sender.Sk) @@ -3491,7 +3498,7 @@ int 1`, return simulationTestCase{ input: simulation.Request{ TxnGroups: [][]transactions.SignedTxn{ - {signedCreate, signedOptin, signedGlobalStateCall, signedLocalStateCall}, + {signedOptin, signedGlobalStateCall, signedLocalStateCall}, }, TraceConfig: simulation.ExecTraceConfig{ Enable: true, @@ -3513,44 +3520,6 @@ int 1`, TxnGroups: []simulation.TxnGroupResult{ { Txns: []simulation.TxnResult{ - // App creation - { - Txn: transactions.SignedTxnWithAD{ - ApplyData: transactions.ApplyData{ - ApplicationID: futureAppID, - }, - }, - AppBudgetConsumed: 4, - Trace: &simulation.TransactionTrace{ - ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ - { - PC: 1, - }, - { - PC: 4, - StackAdded: []basics.TealValue{ - { - Type: basics.TealUintType, - }, - }, - }, - { - PC: 6, - StackPopCount: 1, - }, - { - PC: 154, - StackAdded: []basics.TealValue{ - { - Type: basics.TealUintType, - Uint: 1, - }, - }, - }, - }, - ApprovalProgramHash: progHash, - }, - }, // Optin { AppBudgetConsumed: 8, @@ -3564,7 +3533,7 @@ int 1`, StackAdded: []basics.TealValue{ { Type: basics.TealUintType, - Uint: uint64(futureAppID), + Uint: uint64(createdAppID), }, }, }, @@ -3646,7 +3615,7 @@ int 1`, StackAdded: []basics.TealValue{ { Type: basics.TealUintType, - Uint: uint64(futureAppID), + Uint: uint64(createdAppID), }, }, }, @@ -3739,7 +3708,7 @@ int 1`, { AppStateOp: logic.AppStateWrite, AppState: logic.GlobalState, - AppID: futureAppID, + AppID: createdAppID, Key: "global-int-key", NewValue: basics.TealValue{ Type: basics.TealUintType, @@ -3774,7 +3743,7 @@ int 1`, { AppStateOp: logic.AppStateWrite, AppState: logic.GlobalState, - AppID: futureAppID, + AppID: createdAppID, Key: "global-bytes-key", NewValue: basics.TealValue{ Type: basics.TealBytesType, @@ -3828,7 +3797,7 @@ int 1`, StackAdded: []basics.TealValue{ { Type: basics.TealUintType, - Uint: uint64(futureAppID), + Uint: uint64(createdAppID), }, }, }, @@ -3930,7 +3899,7 @@ int 1`, { AppStateOp: logic.AppStateWrite, AppState: logic.LocalState, - AppID: futureAppID, + AppID: createdAppID, Key: "local-int-key", NewValue: basics.TealValue{ Type: basics.TealUintType, @@ -3974,7 +3943,7 @@ int 1`, { AppStateOp: logic.AppStateWrite, AppState: logic.LocalState, - AppID: futureAppID, + AppID: createdAppID, Key: "local-bytes-key", NewValue: basics.TealValue{ Type: basics.TealBytesType, @@ -3999,10 +3968,25 @@ int 1`, }, }, }, - AppBudgetAdded: 2800, - AppBudgetConsumed: 52, + AppBudgetAdded: 2100, + AppBudgetConsumed: 48, }, }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: simulation.AppsInitialStates{ + createdAppID: simulation.SingleAppInitialStates{ + AppLocals: map[basics.Address]simulation.AppKVPairs{}, + AppGlobals: simulation.AppKVPairs{}, + AppBoxes: simulation.AppKVPairs{}, + CreatedGlobals: util.MakeSet("global-bytes-key", "global-int-key"), + CreatedBoxes: make(util.Set[string]), + CreatedLocals: map[basics.Address]util.Set[string]{ + sender.Addr: util.MakeSet("local-bytes-key", "local-int-key"), + }, + }, + }, + CreatedApp: util.Set[basics.AppIndex]{}, + }, }, } }) @@ -4237,6 +4221,10 @@ int 1`, AppBudgetConsumed: 14, }, }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: make(simulation.AppsInitialStates), + CreatedApp: util.MakeSet(futureAppID), + }, }, } }) @@ -4417,130 +4405,1713 @@ int 1`, AppBudgetConsumed: 8, }, }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: make(simulation.AppsInitialStates), + CreatedApp: util.MakeSet(futureAppID), + }, }, } }) } -// TestBalanceChangesWithApp sends a payment transaction to a new account and confirms its balance -// within a subsequent app call -func TestBalanceChangesWithApp(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() +type BoxInitialStatesTestCase struct { + boxOpsForPrepare []boxOperation + boxOpsForSimulate []boxOperation + initialBoxStates simulation.AppKVPairs +} + +func testBoxInitialStatesHelper(t *testing.T, testcase BoxInitialStatesTestCase) { + t.Helper() + simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { - sender := env.Accounts[0] - senderBalance := sender.AcctData.MicroAlgos.Raw - sendAmount := senderBalance - 500_000 // Leave 0.5 Algos in the sender account - receiver := env.Accounts[1] - receiverBalance := receiver.AcctData.MicroAlgos.Raw + proto := env.TxnInfo.CurrentProtocolParams() + appCreator := env.Accounts[0] - futureAppID := basics.AppIndex(1001) - createTxn := env.TxnInfo.NewTxn(txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: sender.Addr, - ApprovalProgram: `#pragma version 6 -txn ApplicationID // [appId] -bz end // [] -int 1 // [1] -balance // [bal[1]] -itob // [itob(bal[1])] -txn ApplicationArgs 0 // [itob(bal[1]), args[0]] -== // [itob(bal[1])=?=args[0]] -assert -end: -int 1 // [1] -`, - ClearStateProgram: `#pragma version 6 + boxApprovalProgram := fmt.Sprintf(boxTestProgram, 8) + boxAppID := env.CreateApp(appCreator.Addr, simulationtesting.AppParams{ + ApprovalProgram: boxApprovalProgram, + ClearStateProgram: `#pragma version 8 int 1`, }) - checkStartingBalanceTxn := env.TxnInfo.NewTxn(txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: sender.Addr, - ApplicationID: futureAppID, - Accounts: []basics.Address{receiver.Addr}, - ApplicationArgs: [][]byte{uint64ToBytes(receiverBalance)}, - }) - paymentTxn := env.TxnInfo.NewTxn(txntest.Txn{ - Type: protocol.PaymentTx, - Sender: sender.Addr, - Receiver: receiver.Addr, - Amount: sendAmount, - }) - checkEndingBalanceTxn := env.TxnInfo.NewTxn(txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: sender.Addr, - ApplicationID: futureAppID, - Accounts: []basics.Address{receiver.Addr}, - // Receiver's balance should have increased by sendAmount - ApplicationArgs: [][]byte{uint64ToBytes(receiverBalance + sendAmount)}, - }) - txntest.Group(&createTxn, &checkStartingBalanceTxn, &paymentTxn, &checkEndingBalanceTxn) + op, err := logic.AssembleString(boxApprovalProgram) + require.NoError(t, err) + progHash := crypto.Hash(op.Program) - signedCreateTxn := createTxn.Txn().Sign(sender.Sk) - signedCheckStartingBalanceTxn := checkStartingBalanceTxn.Txn().Sign(sender.Sk) - signedPaymentTxn := paymentTxn.Txn().Sign(sender.Sk) - signedCheckEndingBalanceTxn := checkEndingBalanceTxn.Txn().Sign(sender.Sk) + transferable := env.Accounts[1].AcctData.MicroAlgos.Raw - proto.MinBalance - proto.MinTxnFee + env.TransferAlgos(env.Accounts[1].Addr, boxAppID.Address(), transferable) + + for _, boxOp := range testcase.boxOpsForPrepare { + env.Txn(env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: appCreator.Addr, + ApplicationID: boxAppID, + ApplicationArgs: boxOp.appArgs(), + Boxes: boxOp.boxRefs(), + }).SignedTxn()) + } + + boxOpToSimResult := func(boxOp boxOperation) simulation.TxnResult { + var res simulation.TxnResult + switch boxOp.op { + case logic.BoxReadOperation: + res = simulation.TxnResult{ + AppBudgetConsumed: 14, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 14}, + {PC: 22}, + {PC: 28}, + {PC: 35}, + {PC: 38}, + {PC: 69}, + {PC: 72}, + {PC: 73}, + {PC: 74}, + {PC: 75}, + {PC: 87}, + }, + ApprovalProgramHash: progHash, + }, + } + case logic.BoxWriteOperation: + res = simulation.TxnResult{ + AppBudgetConsumed: 13, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 14}, + {PC: 22}, + {PC: 28}, + {PC: 35}, + {PC: 38}, + {PC: 78}, + {PC: 81}, + {PC: 83}, + { + PC: 86, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateWrite, + AppState: logic.BoxState, + AppID: boxAppID, + Key: boxOp.name, + NewValue: basics.TealValue{ + Type: basics.TealBytesType, + Bytes: string(boxOp.contents), + }, + }, + }, + }, + {PC: 87}, + }, + ApprovalProgramHash: progHash, + }, + } + case logic.BoxCreateOperation: + res = simulation.TxnResult{ + AppBudgetConsumed: 15, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 14}, + {PC: 22}, + {PC: 28}, + {PC: 35}, + {PC: 38}, + {PC: 49}, + {PC: 52}, + {PC: 55}, + { + PC: 56, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateWrite, + AppState: logic.BoxState, + AppID: boxAppID, + Key: boxOp.name, + NewValue: basics.TealValue{ + Type: basics.TealBytesType, + Bytes: string(make([]byte, boxOp.createSize)), + }, + }, + }, + }, + {PC: 57}, + {PC: 58}, + {PC: 87}, + }, + ApprovalProgramHash: progHash, + }, + } + case logic.BoxDeleteOperation: + res = simulation.TxnResult{ + AppBudgetConsumed: 13, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 14}, + {PC: 22}, + {PC: 28}, + {PC: 35}, + {PC: 38}, + {PC: 61}, + { + PC: 64, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateDelete, + AppState: logic.BoxState, + AppID: boxAppID, + Key: boxOp.name, + }, + }, + }, + {PC: 65}, + {PC: 66}, + {PC: 87}, + }, + ApprovalProgramHash: progHash, + }, + } + } + return res + } + + txnPtrs := make([]*txntest.Txn, len(testcase.boxOpsForSimulate)) + for i, boxOp := range testcase.boxOpsForSimulate { + tempTxn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: appCreator.Addr, + ApplicationID: boxAppID, + ApplicationArgs: boxOp.appArgs(), + Boxes: boxOp.boxRefs(), + }) + txnPtrs[i] = &tempTxn + } + + txntest.Group(txnPtrs...) + signedTxns := make([]transactions.SignedTxn, len(testcase.boxOpsForSimulate)) + for i, txn := range txnPtrs { + signedTxns[i] = txn.Txn().Sign(appCreator.Sk) + } + + txnResults := make([]simulation.TxnResult, len(testcase.boxOpsForSimulate)) + for i, boxOp := range testcase.boxOpsForSimulate { + txnResults[i] = boxOpToSimResult(boxOp) + } + totalConsumed := uint64(0) + for _, txnResult := range txnResults { + totalConsumed += txnResult.AppBudgetConsumed + } + + prepareKeys := make(util.Set[string]) + for _, instruction := range testcase.boxOpsForPrepare { + if instruction.op != logic.BoxWriteOperation { + continue + } + prepareKeys.Add(instruction.name) + } + newlyCreatedGlobalKeySet := make(util.Set[string]) + for _, instruction := range testcase.boxOpsForSimulate { + if instruction.op != logic.BoxWriteOperation { + continue + } + if prepareKeys.Contains(instruction.name) { + continue + } + newlyCreatedGlobalKeySet.Add(instruction.name) + } return simulationTestCase{ input: simulation.Request{ TxnGroups: [][]transactions.SignedTxn{ - { - signedCreateTxn, - signedCheckStartingBalanceTxn, - signedPaymentTxn, - signedCheckEndingBalanceTxn, - }, + signedTxns, + }, + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, }, }, + developerAPI: true, expected: simulation.Result{ Version: simulation.ResultLatestVersion, LastRound: env.TxnInfo.LatestRound(), + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, TxnGroups: []simulation.TxnGroupResult{ { - Txns: []simulation.TxnResult{ - { - Txn: transactions.SignedTxnWithAD{ - ApplyData: transactions.ApplyData{ - ApplicationID: futureAppID, - }, - }, - AppBudgetConsumed: 4, - }, - { - AppBudgetConsumed: 10, - }, - {}, - { - AppBudgetConsumed: 10, - }, + Txns: txnResults, + AppBudgetAdded: 700 * uint64(len(txnResults)), + AppBudgetConsumed: totalConsumed, + }, + }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: simulation.AppsInitialStates{ + boxAppID: simulation.SingleAppInitialStates{ + AppGlobals: make(simulation.AppKVPairs), + AppLocals: map[basics.Address]simulation.AppKVPairs{}, + AppBoxes: testcase.initialBoxStates, + CreatedGlobals: make(util.Set[string]), + CreatedBoxes: newlyCreatedGlobalKeySet, + CreatedLocals: map[basics.Address]util.Set[string]{}, }, - AppBudgetAdded: 2100, - AppBudgetConsumed: 24, }, + CreatedApp: util.Set[basics.AppIndex]{}, }, }, } }) } -// TestOptionalSignatures tests that transactions with signatures and without signatures are both -// properly handled when AllowEmptySignatures is enabled. -func TestOptionalSignatures(t *testing.T) { +func TestAppInitialBoxStates(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - for _, signed := range []bool{true, false} { - signed := signed - t.Run(fmt.Sprintf("signed=%t", signed), func(t *testing.T) { - simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { - sender := env.Accounts[0] - txn := env.TxnInfo.NewTxn(txntest.Txn{ - Type: protocol.PaymentTx, - Sender: sender.Addr, - Receiver: sender.Addr, - Amount: 1, - }) + testBoxInitialStatesHelper(t, BoxInitialStatesTestCase{ + boxOpsForPrepare: []boxOperation{ + { + op: logic.BoxCreateOperation, + name: "A", + createSize: 21, + }, + { + op: logic.BoxWriteOperation, + name: "A", + contents: []byte("initial box A content"), + }, + }, + boxOpsForSimulate: []boxOperation{ + { + op: logic.BoxReadOperation, + name: "A", + }, + { + op: logic.BoxWriteOperation, + name: "A", + contents: []byte("box A get overwritten"), + }, + }, + initialBoxStates: simulation.AppKVPairs{ + "A": { + Type: basics.TealBytesType, + Bytes: "initial box A content", + }, + }, + }) + + testBoxInitialStatesHelper(t, BoxInitialStatesTestCase{ + boxOpsForPrepare: []boxOperation{ + { + op: logic.BoxCreateOperation, + name: "A", + createSize: 21, + }, + { + op: logic.BoxWriteOperation, + name: "A", + contents: []byte("initial box A content"), + }, + { + op: logic.BoxCreateOperation, + name: "B", + createSize: 21, + }, + { + op: logic.BoxWriteOperation, + name: "B", + contents: []byte("initial box B content"), + }, + { + op: logic.BoxCreateOperation, + name: "C", + createSize: 21, + }, + { + op: logic.BoxWriteOperation, + name: "C", + contents: []byte("initial box C content"), + }, + }, + boxOpsForSimulate: []boxOperation{ + { + op: logic.BoxDeleteOperation, + name: "C", + }, + { + op: logic.BoxReadOperation, + name: "A", + }, + }, + initialBoxStates: simulation.AppKVPairs{ + "A": { + Type: basics.TealBytesType, + Bytes: "initial box A content", + }, + "C": { + Type: basics.TealBytesType, + Bytes: "initial box C content", + }, + }, + }) + + testBoxInitialStatesHelper(t, BoxInitialStatesTestCase{ + boxOpsForPrepare: []boxOperation{ + { + op: logic.BoxCreateOperation, + name: "A", + createSize: 21, + }, + { + op: logic.BoxWriteOperation, + name: "A", + contents: []byte("initial box A content"), + }, + { + op: logic.BoxCreateOperation, + name: "C", + createSize: 21, + }, + { + op: logic.BoxWriteOperation, + name: "C", + contents: []byte("initial box C content"), + }, + }, + boxOpsForSimulate: []boxOperation{ + { + op: logic.BoxCreateOperation, + name: "B", + createSize: 21, + }, + { + op: logic.BoxWriteOperation, + name: "B", + contents: []byte("initial box B content"), + }, + }, + initialBoxStates: simulation.AppKVPairs{}, + }) +} + +func testBoxPutInitialStatesHelper(t *testing.T, testcase BoxInitialStatesTestCase) { + t.Helper() + + simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { + proto := env.TxnInfo.CurrentProtocolParams() + appCreator := env.Accounts[0] + + boxApprovalProgram := `#pragma version 8 +txn ApplicationID +bz end // Do nothing during create + +byte "write" +byte "delete" +txn ApplicationArgs 0 +match put del +err // Unknown command + +put: +txn ApplicationArgs 1 +txn ApplicationArgs 2 +box_put +b end + +del: +txn ApplicationArgs 1 +box_del +assert +b end + +end: +int 1 +` + boxAppID := env.CreateApp(appCreator.Addr, simulationtesting.AppParams{ + ApprovalProgram: boxApprovalProgram, + ClearStateProgram: `#pragma version 8 + int 1`, + }) + + op, err := logic.AssembleString(boxApprovalProgram) + require.NoError(t, err) + progHash := crypto.Hash(op.Program) + + transferable := env.Accounts[1].AcctData.MicroAlgos.Raw - proto.MinBalance - proto.MinTxnFee + env.TransferAlgos(env.Accounts[1].Addr, boxAppID.Address(), transferable) + + for _, boxOp := range testcase.boxOpsForPrepare { + env.Txn(env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: appCreator.Addr, + ApplicationID: boxAppID, + ApplicationArgs: boxOp.appArgs(), + Boxes: boxOp.boxRefs(), + }).SignedTxn()) + } + + boxOpToSimResult := func(boxOp boxOperation) simulation.TxnResult { + var res simulation.TxnResult + switch boxOp.op { + case logic.BoxWriteOperation: + res = simulation.TxnResult{ + AppBudgetConsumed: 11, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 13}, + {PC: 21}, + {PC: 24}, + {PC: 31}, + {PC: 34}, + { + PC: 37, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateWrite, + AppState: logic.BoxState, + AppID: boxAppID, + Key: boxOp.name, + NewValue: basics.TealValue{ + Type: basics.TealBytesType, + Bytes: string(boxOp.contents), + }, + }, + }, + }, + {PC: 38}, + {PC: 49}, + }, + ApprovalProgramHash: progHash, + }, + } + case logic.BoxDeleteOperation: + res = simulation.TxnResult{ + AppBudgetConsumed: 11, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 13}, + {PC: 21}, + {PC: 24}, + {PC: 31}, + {PC: 34}, + {PC: 37}, + {PC: 61}, + { + PC: 64, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateDelete, + AppState: logic.BoxState, + AppID: boxAppID, + Key: boxOp.name, + }, + }, + }, + {PC: 65}, + {PC: 66}, + {PC: 87}, + }, + ApprovalProgramHash: progHash, + }, + } + } + return res + } + + txnPtrs := make([]*txntest.Txn, len(testcase.boxOpsForSimulate)) + for i, boxOp := range testcase.boxOpsForSimulate { + tempTxn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: appCreator.Addr, + ApplicationID: boxAppID, + ApplicationArgs: boxOp.appArgs(), + Boxes: boxOp.boxRefs(), + }) + txnPtrs[i] = &tempTxn + } + + txntest.Group(txnPtrs...) + signedTxns := make([]transactions.SignedTxn, len(testcase.boxOpsForSimulate)) + for i, txn := range txnPtrs { + signedTxns[i] = txn.Txn().Sign(appCreator.Sk) + } + + txnResults := make([]simulation.TxnResult, len(testcase.boxOpsForSimulate)) + for i, boxOp := range testcase.boxOpsForSimulate { + txnResults[i] = boxOpToSimResult(boxOp) + } + totalConsumed := uint64(0) + for _, txnResult := range txnResults { + totalConsumed += txnResult.AppBudgetConsumed + } + + prepareKeys := make(util.Set[string]) + for _, instruction := range testcase.boxOpsForPrepare { + if instruction.op != logic.BoxWriteOperation { + continue + } + prepareKeys.Add(instruction.name) + } + newlyCreatedGlobalKeySet := make(util.Set[string]) + for _, instruction := range testcase.boxOpsForSimulate { + if instruction.op != logic.BoxWriteOperation { + continue + } + if prepareKeys.Contains(instruction.name) { + continue + } + newlyCreatedGlobalKeySet.Add(instruction.name) + } + + return simulationTestCase{ + input: simulation.Request{ + TxnGroups: [][]transactions.SignedTxn{ + signedTxns, + }, + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, + }, + developerAPI: true, + expected: simulation.Result{ + Version: simulation.ResultLatestVersion, + LastRound: env.TxnInfo.LatestRound(), + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, + TxnGroups: []simulation.TxnGroupResult{ + { + Txns: txnResults, + AppBudgetAdded: 700 * uint64(len(txnResults)), + AppBudgetConsumed: totalConsumed, + }, + }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: simulation.AppsInitialStates{ + boxAppID: simulation.SingleAppInitialStates{ + AppGlobals: make(simulation.AppKVPairs), + AppLocals: map[basics.Address]simulation.AppKVPairs{}, + AppBoxes: testcase.initialBoxStates, + CreatedGlobals: make(util.Set[string]), + CreatedBoxes: newlyCreatedGlobalKeySet, + CreatedLocals: map[basics.Address]util.Set[string]{}, + }, + }, + CreatedApp: util.Set[basics.AppIndex]{}, + }, + }, + } + }) +} + +func TestAppInitialBoxStatesAboutBoxPut(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testBoxPutInitialStatesHelper(t, BoxInitialStatesTestCase{ + boxOpsForPrepare: []boxOperation{ + { + op: logic.BoxWriteOperation, + name: "A", + contents: []byte("initial box A content"), + }, + }, + boxOpsForSimulate: []boxOperation{ + { + op: logic.BoxWriteOperation, + name: "A", + contents: []byte("box A get overwritten"), + }, + }, + initialBoxStates: simulation.AppKVPairs{ + "A": { + Type: basics.TealBytesType, + Bytes: "initial box A content", + }, + }, + }) + + testBoxPutInitialStatesHelper(t, BoxInitialStatesTestCase{ + boxOpsForSimulate: []boxOperation{ + { + op: logic.BoxWriteOperation, + name: "A", + contents: []byte("box A get overwritten"), + }, + }, + initialBoxStates: simulation.AppKVPairs{}, + }) +} + +type GlobalInitialStatesTestCase struct { + prepareInstruction [][][]byte + txnsArgs [][][]byte + initialGlobalStates simulation.AppKVPairs +} + +func (l GlobalInitialStatesTestCase) toSignedTxns(env simulationtesting.Environment, addr simulationtesting.Account, appID basics.AppIndex) []transactions.SignedTxn { + txns := make([]*txntest.Txn, len(l.txnsArgs)) + for i, txnArgs := range l.txnsArgs { + tempTxn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: addr.Addr, + ApplicationID: appID, + ApplicationArgs: txnArgs, + }) + txns[i] = &tempTxn + } + txntest.Group(txns...) + signedTxns := make([]transactions.SignedTxn, len(l.txnsArgs)) + for i, txn := range txns { + signedTxns[i] = txn.Txn().Sign(addr.Sk) + } + return signedTxns +} + +func testGlobalInitialStatesHelper(t *testing.T, testcase GlobalInitialStatesTestCase) { + t.Helper() + + simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { + appCreator := env.Accounts[0] + + approvalProgramSrc := `#pragma version 8 +txn ApplicationID +bz end // Do nothing during create + +byte "put" +byte "del" +txn ApplicationArgs 0 +match put del +err // Unknown command + +put: + txn ApplicationArgs 1 + txn ApplicationArgs 2 + app_global_put + b end + +del: + txn ApplicationArgs 1 + app_global_del + b end + +end: + int 1 +` + + appID := env.CreateApp(appCreator.Addr, simulationtesting.AppParams{ + GlobalStateSchema: basics.StateSchema{NumByteSlice: 8}, + ApprovalProgram: approvalProgramSrc, + ClearStateProgram: `#pragma version 8 +int 1`, + }) + + op, err := logic.AssembleString(approvalProgramSrc) + require.NoError(t, err) + progHash := crypto.Hash(op.Program) + + for _, instruction := range testcase.prepareInstruction { + txnArgs := [][]byte{[]byte("put")} + txnArgs = append(txnArgs, instruction...) + env.Txn(env.TxnInfo.NewTxn(txntest.Txn{ + Sender: appCreator.Addr, + Type: protocol.ApplicationCallTx, + ApplicationID: appID, + ApplicationArgs: txnArgs, + }).SignedTxn()) + } + + signedTxns := testcase.toSignedTxns(env, appCreator, appID) + + txnArgsToResult := func(txnAppArgs [][]byte) simulation.TxnResult { + var res simulation.TxnResult + switch string(txnAppArgs[0]) { + case "put": + res = simulation.TxnResult{ + Txn: transactions.SignedTxnWithAD{ + ApplyData: transactions.ApplyData{ + EvalDelta: transactions.EvalDelta{ + GlobalDelta: basics.StateDelta{ + string(txnAppArgs[1]): basics.ValueDelta{ + Bytes: string(txnAppArgs[2]), + Action: basics.SetBytesAction, + }, + }, + }, + }, + }, + AppBudgetConsumed: 11, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 11}, + {PC: 16}, + {PC: 19}, + {PC: 26}, + {PC: 29}, + { + PC: 32, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateWrite, + AppState: logic.GlobalState, + AppID: appID, + Key: string(txnAppArgs[1]), + NewValue: basics.TealValue{ + Type: basics.TealBytesType, + Bytes: string(txnAppArgs[2]), + }, + }, + }, + }, + {PC: 33}, + {PC: 43}, + }, + ApprovalProgramHash: progHash, + }, + } + case "del": + res = simulation.TxnResult{ + Txn: transactions.SignedTxnWithAD{ + ApplyData: transactions.ApplyData{ + EvalDelta: transactions.EvalDelta{ + GlobalDelta: basics.StateDelta{ + string(txnAppArgs[1]): basics.ValueDelta{ + Action: basics.DeleteAction, + }, + }, + }, + }, + }, + AppBudgetConsumed: 10, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 11}, + {PC: 16}, + {PC: 19}, + {PC: 36}, + { + PC: 39, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateDelete, + AppState: logic.GlobalState, + AppID: appID, + Key: string(txnAppArgs[1]), + }, + }, + }, + {PC: 40}, + {PC: 43}, + }, + ApprovalProgramHash: progHash, + }, + } + default: + } + return res + } + txnResults := make([]simulation.TxnResult, len(testcase.txnsArgs)) + for i, txnArgs := range testcase.txnsArgs { + txnResults[i] = txnArgsToResult(txnArgs) + } + + prepareKeys := make(util.Set[string]) + for _, instruction := range testcase.prepareInstruction { + prepareKeys.Add(string(instruction[0])) + } + newlyCreatedGlobalKeySet := make(util.Set[string]) + for _, txnArgs := range testcase.txnsArgs { + if string(txnArgs[0]) != "put" { + continue + } + if prepareKeys.Contains(string(txnArgs[1])) { + continue + } + newlyCreatedGlobalKeySet.Add(string(txnArgs[1])) + } + + totalConsumed := uint64(0) + for _, txnResult := range txnResults { + totalConsumed += txnResult.AppBudgetConsumed + } + + return simulationTestCase{ + input: simulation.Request{ + TxnGroups: [][]transactions.SignedTxn{ + signedTxns, + }, + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, + }, + developerAPI: true, + expected: simulation.Result{ + Version: simulation.ResultLatestVersion, + LastRound: env.TxnInfo.LatestRound(), + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, + TxnGroups: []simulation.TxnGroupResult{ + { + Txns: txnResults, + AppBudgetAdded: 700 * uint64(len(txnResults)), + AppBudgetConsumed: totalConsumed, + }, + }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: simulation.AppsInitialStates{ + appID: simulation.SingleAppInitialStates{ + AppGlobals: testcase.initialGlobalStates, + AppLocals: map[basics.Address]simulation.AppKVPairs{}, + AppBoxes: make(simulation.AppKVPairs), + CreatedGlobals: newlyCreatedGlobalKeySet, + CreatedBoxes: make(util.Set[string]), + CreatedLocals: map[basics.Address]util.Set[string]{}, + }, + }, + CreatedApp: make(util.Set[basics.AppIndex]), + }, + }, + } + }) +} + +func TestAppInitialGlobalStates(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testGlobalInitialStatesHelper(t, + GlobalInitialStatesTestCase{ + txnsArgs: [][][]byte{ + { + []byte("put"), []byte("A"), []byte("content A"), + }, + { + []byte("del"), []byte("A"), + }, + }, + initialGlobalStates: simulation.AppKVPairs{}, + }, + ) + + testGlobalInitialStatesHelper(t, + GlobalInitialStatesTestCase{ + prepareInstruction: [][][]byte{ + { + []byte("A"), []byte("initial content A"), + }, + }, + txnsArgs: [][][]byte{ + { + []byte("put"), []byte("A"), []byte("content A"), + }, + { + []byte("del"), []byte("A"), + }, + }, + initialGlobalStates: simulation.AppKVPairs{ + "A": basics.TealValue{ + Type: basics.TealBytesType, + Bytes: "initial content A", + }, + }, + }, + ) +} + +type LocalStateOperation struct { + addressIndex uint64 + appArgs [][]byte +} + +type LocalInitialStatesTestCase struct { + prepareInstructions []LocalStateOperation + simulateInstructions []LocalStateOperation + initialLocalStates map[uint64]simulation.AppKVPairs +} + +func (testcase LocalInitialStatesTestCase) toSignedTxns(env simulationtesting.Environment, appID basics.AppIndex) []transactions.SignedTxn { + txns := make([]*txntest.Txn, len(testcase.simulateInstructions)) + for i, instruction := range testcase.simulateInstructions { + tempTxn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: env.Accounts[instruction.addressIndex].Addr, + ApplicationID: appID, + ApplicationArgs: instruction.appArgs, + }) + txns[i] = &tempTxn + } + txntest.Group(txns...) + signedTxns := make([]transactions.SignedTxn, len(testcase.simulateInstructions)) + for i, txn := range txns { + signedTxns[i] = txn.Txn().Sign(env.Accounts[testcase.simulateInstructions[i].addressIndex].Sk) + } + return signedTxns +} + +func testLocalInitialStatesHelper(t *testing.T, testcase LocalInitialStatesTestCase) { + t.Helper() + + simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { + appCreator := env.Accounts[0] + + approvalProgramSrc := `#pragma version 8 +txn ApplicationID +bz end // Do nothing during create + +txn OnCompletion +int OptIn +== +bnz end // Always allow optin + +byte "put" +byte "get" +byte "del" + +txn ApplicationArgs 0 +match put get del +err // Unknown command + +put: + txn Sender // account + txn ApplicationArgs 1 // key + txn ApplicationArgs 2 // local state content + app_local_put + b end + +get: + txn Sender // account + txn ApplicationArgs 1 // key + app_local_get + pop + b end + +del: + txn Sender // account + txn ApplicationArgs 1 // key + app_local_del + b end + +end: + int 1 +` + + appID := env.CreateApp(appCreator.Addr, simulationtesting.AppParams{ + LocalStateSchema: basics.StateSchema{NumByteSlice: 8}, + ApprovalProgram: approvalProgramSrc, + ClearStateProgram: `#pragma version 8 +int 1`, + }) + + op, err := logic.AssembleString(approvalProgramSrc) + require.NoError(t, err) + progHash := crypto.Hash(op.Program) + + for _, acct := range env.Accounts[2:] { + env.Txn(env.TxnInfo.NewTxn(txntest.Txn{ + Sender: acct.Addr, + Type: protocol.ApplicationCallTx, + ApplicationID: appID, + OnCompletion: transactions.OptInOC, + }).SignedTxn()) + } + + for _, instruction := range testcase.prepareInstructions { + env.Txn(env.TxnInfo.NewTxn(txntest.Txn{ + Sender: env.Accounts[instruction.addressIndex].Addr, + Type: protocol.ApplicationCallTx, + ApplicationID: appID, + ApplicationArgs: instruction.appArgs, + }).SignedTxn()) + } + + signedTxns := testcase.toSignedTxns(env, appID) + + txnArgsToResult := func(instruction LocalStateOperation) simulation.TxnResult { + var res simulation.TxnResult + switch string(instruction.appArgs[0]) { + case "put": + res = simulation.TxnResult{ + Txn: transactions.SignedTxnWithAD{ + ApplyData: transactions.ApplyData{ + EvalDelta: transactions.EvalDelta{ + LocalDeltas: map[uint64]basics.StateDelta{ + 0: { + string(instruction.appArgs[1]): basics.ValueDelta{ + Bytes: string(instruction.appArgs[2]), + Action: basics.SetBytesAction, + }, + }, + }, + }, + }, + }, + AppBudgetConsumed: 18, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 4}, + {PC: 6}, + {PC: 9}, + {PC: 11}, + {PC: 12}, + {PC: 13}, + {PC: 16}, + {PC: 21}, + {PC: 26}, + {PC: 31}, + {PC: 34}, + {PC: 43}, + {PC: 45}, + {PC: 48}, + { + PC: 51, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateWrite, + AppState: logic.LocalState, + AppID: appID, + Key: string(instruction.appArgs[1]), + NewValue: basics.TealValue{ + Type: basics.TealBytesType, + Bytes: string(instruction.appArgs[2]), + }, + Account: env.Accounts[instruction.addressIndex].Addr, + }, + }, + }, + {PC: 52}, + {PC: 74}, + }, + ApprovalProgramHash: progHash, + }, + } + case "del": + res = simulation.TxnResult{ + Txn: transactions.SignedTxnWithAD{ + ApplyData: transactions.ApplyData{ + EvalDelta: transactions.EvalDelta{ + LocalDeltas: map[uint64]basics.StateDelta{ + 0: { + string(instruction.appArgs[1]): basics.ValueDelta{ + Action: basics.DeleteAction, + }, + }, + }, + }, + }, + }, + AppBudgetConsumed: 17, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 4}, + {PC: 6}, + {PC: 9}, + {PC: 11}, + {PC: 12}, + {PC: 13}, + {PC: 16}, + {PC: 21}, + {PC: 26}, + {PC: 31}, + {PC: 34}, + {PC: 65}, + {PC: 67}, + { + PC: 70, + StateChanges: []simulation.StateOperation{ + { + AppStateOp: logic.AppStateDelete, + AppState: logic.LocalState, + AppID: appID, + Key: string(instruction.appArgs[1]), + Account: env.Accounts[instruction.addressIndex].Addr, + }, + }, + }, + {PC: 71}, + {PC: 74}, + }, + ApprovalProgramHash: progHash, + }, + } + case "get": + res = simulation.TxnResult{ + AppBudgetConsumed: 18, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 4}, + {PC: 6}, + {PC: 9}, + {PC: 11}, + {PC: 12}, + {PC: 13}, + {PC: 16}, + {PC: 21}, + {PC: 26}, + {PC: 31}, + {PC: 34}, + {PC: 55}, + {PC: 57}, + {PC: 60}, + {PC: 61}, + {PC: 62}, + {PC: 74}, + }, + ApprovalProgramHash: progHash, + }, + } + default: + } + return res + } + txnResults := make([]simulation.TxnResult, len(testcase.simulateInstructions)) + for i, txnArgs := range testcase.simulateInstructions { + txnResults[i] = txnArgsToResult(txnArgs) + } + + prepareInitialStates := make(map[basics.Address]util.Set[string]) + for _, instruction := range testcase.prepareInstructions { + if prepareInitialStates[env.Accounts[instruction.addressIndex].Addr] == nil { + prepareInitialStates[env.Accounts[instruction.addressIndex].Addr] = make(util.Set[string]) + } + prepareInitialStates[env.Accounts[instruction.addressIndex].Addr].Add(string(instruction.appArgs[1])) + } + + newlyCreatedLocalStates := make(map[basics.Address]util.Set[string]) + for _, instruction := range testcase.simulateInstructions { + if string(instruction.appArgs[0]) != "put" { + continue + } + acctAddress := env.Accounts[instruction.addressIndex].Addr + if prepareInitialStates[acctAddress] != nil && prepareInitialStates[acctAddress].Contains(string(instruction.appArgs[1])) { + continue + } + if newlyCreatedLocalStates[acctAddress] == nil { + newlyCreatedLocalStates[acctAddress] = make(util.Set[string]) + } + newlyCreatedLocalStates[acctAddress].Add(string(instruction.appArgs[1])) + } + + totalConsumed := uint64(0) + for _, txnResult := range txnResults { + totalConsumed += txnResult.AppBudgetConsumed + } + + expectedInitialLocalStates := make(map[basics.Address]simulation.AppKVPairs) + for addrID, kvPair := range testcase.initialLocalStates { + expectedInitialLocalStates[env.Accounts[addrID].Addr] = kvPair + } + + return simulationTestCase{ + input: simulation.Request{ + TxnGroups: [][]transactions.SignedTxn{ + signedTxns, + }, + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, + }, + developerAPI: true, + expected: simulation.Result{ + Version: simulation.ResultLatestVersion, + LastRound: env.TxnInfo.LatestRound(), + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, + TxnGroups: []simulation.TxnGroupResult{ + { + Txns: txnResults, + AppBudgetAdded: 700 * uint64(len(txnResults)), + AppBudgetConsumed: totalConsumed, + }, + }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: simulation.AppsInitialStates{ + appID: simulation.SingleAppInitialStates{ + AppGlobals: make(simulation.AppKVPairs), + AppLocals: expectedInitialLocalStates, + AppBoxes: make(simulation.AppKVPairs), + CreatedGlobals: make(util.Set[string]), + CreatedLocals: newlyCreatedLocalStates, + CreatedBoxes: make(util.Set[string]), + }, + }, + CreatedApp: make(util.Set[basics.AppIndex]), + }, + }, + } + }) +} + +func TestLocalInitialStates(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + testLocalInitialStatesHelper(t, LocalInitialStatesTestCase{ + prepareInstructions: []LocalStateOperation{}, + simulateInstructions: []LocalStateOperation{ + { + addressIndex: 2, + appArgs: [][]byte{ + []byte("put"), []byte("key"), []byte("value"), + }, + }, + }, + initialLocalStates: map[uint64]simulation.AppKVPairs{}, + }) + + testLocalInitialStatesHelper(t, LocalInitialStatesTestCase{ + prepareInstructions: []LocalStateOperation{ + { + addressIndex: 2, + appArgs: [][]byte{ + []byte("put"), []byte("key"), []byte("value"), + }, + }, + }, + simulateInstructions: []LocalStateOperation{ + { + addressIndex: 2, + appArgs: [][]byte{ + []byte("put"), []byte("key"), []byte("new-value"), + }, + }, + { + addressIndex: 2, + appArgs: [][]byte{ + []byte("get"), []byte("key"), + }, + }, + { + addressIndex: 2, + appArgs: [][]byte{ + []byte("del"), []byte("key"), + }, + }, + }, + initialLocalStates: map[uint64]simulation.AppKVPairs{ + 2: { + "key": basics.TealValue{ + Type: basics.TealBytesType, + Bytes: "value", + }, + }, + }, + }) +} + +func TestInitialStatesGetEx(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { + appCreator := env.Accounts[0] + + approvalProgramSrc := `#pragma version 8 +txn ApplicationID +bz end // Do nothing during create + +txn OnCompletion +int OptIn +== +bnz end // Always allow optin + +byte "put" +byte "local_put" +byte "del" +txn ApplicationArgs 0 +match put local_put del +err // Unknown command + +put: + txn ApplicationArgs 1 + txn ApplicationArgs 2 + app_global_put + b end + +local_put: + txn Sender + txn ApplicationArgs 1 + txn ApplicationArgs 2 + app_local_put + b end + +del: + txn ApplicationArgs 1 + app_global_del + b end + +end: + int 1 +` + + appIDWithStates := env.CreateApp(appCreator.Addr, simulationtesting.AppParams{ + GlobalStateSchema: basics.StateSchema{NumByteSlice: 8}, + LocalStateSchema: basics.StateSchema{NumByteSlice: 8}, + ApprovalProgram: approvalProgramSrc, + ClearStateProgram: `#pragma version 8 +int 1`, + }) + + env.Txn(env.TxnInfo.NewTxn(txntest.Txn{ + Sender: appCreator.Addr, + Type: protocol.ApplicationCallTx, + ApplicationID: appIDWithStates, + OnCompletion: transactions.OptInOC, + }).SignedTxn()) + + prepareSteps := [][][]byte{ + { + []byte("put"), []byte("A"), []byte("initial content A"), + }, + { + []byte("local_put"), []byte("B"), []byte("initial content B"), + }, + } + + for _, txnArgs := range prepareSteps { + env.Txn(env.TxnInfo.NewTxn(txntest.Txn{ + Sender: appCreator.Addr, + Type: protocol.ApplicationCallTx, + ApplicationID: appIDWithStates, + ApplicationArgs: txnArgs, + }).SignedTxn()) + } + + // The application to read another app + approvalProgramSrc = `#pragma version 8 +txn ApplicationID +bz end // Do nothing during create + +byte "read_global" +byte "read_local" +txn ApplicationArgs 0 +match read_global read_local +err // Unknown command + +read_global: + txn ApplicationArgs 1 // AppID + btoi + txn ApplicationArgs 2 // GlobalKey + app_global_get_ex + assert + pop + b end + +read_local: + txn Sender + txn ApplicationArgs 1 // AppID + btoi + txn ApplicationArgs 2 // LocalKey + app_local_get_ex + assert + pop + b end + +end: +int 1 +` + appIDReadingStates := env.CreateApp(appCreator.Addr, simulationtesting.AppParams{ + ApprovalProgram: approvalProgramSrc, + ClearStateProgram: `#pragma version 8 +int 1`, + }) + + op, err := logic.AssembleString(approvalProgramSrc) + require.NoError(t, err) + progHash := crypto.Hash(op.Program) + + txns := make([]*txntest.Txn, 2) + tmpTxn0 := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: appCreator.Addr, + ApplicationID: appIDReadingStates, + ApplicationArgs: [][]byte{ + []byte("read_global"), + uint64ToBytes(uint64(appIDWithStates)), + []byte("A"), + }, + ForeignApps: []basics.AppIndex{appIDWithStates}, + }) + txns[0] = &tmpTxn0 + tmpTxn1 := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: appCreator.Addr, + ApplicationID: appIDReadingStates, + ApplicationArgs: [][]byte{ + []byte("read_local"), + uint64ToBytes(uint64(appIDWithStates)), + []byte("B"), + }, + ForeignApps: []basics.AppIndex{appIDWithStates}, + Note: []byte("bla"), + }) + txns[1] = &tmpTxn1 + txntest.Group(txns...) + signedTxns := make([]transactions.SignedTxn, len(txns)) + for i, txn := range txns { + signedTxns[i] = txn.Txn().Sign(appCreator.Sk) + } + + // now construct app calls for global local get ex + txnResults := []simulation.TxnResult{ + { + AppBudgetConsumed: 14, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 19}, + {PC: 31}, + {PC: 34}, + {PC: 41}, + {PC: 44}, + {PC: 45}, + {PC: 48}, + {PC: 49}, + {PC: 50}, + {PC: 51}, + {PC: 69}, + }, + ApprovalProgramHash: progHash, + }, + }, + { + AppBudgetConsumed: 15, + Trace: &simulation.TransactionTrace{ + ApprovalProgramTrace: []simulation.OpcodeTraceUnit{ + {PC: 1}, + {PC: 3}, + {PC: 6}, + {PC: 19}, + {PC: 31}, + {PC: 34}, + {PC: 54}, + {PC: 56}, + {PC: 59}, + {PC: 60}, + {PC: 63}, + {PC: 64}, + {PC: 65}, + {PC: 66}, + {PC: 69}, + }, + ApprovalProgramHash: progHash, + }, + }, + } + + totalConsumed := uint64(0) + for _, txnResult := range txnResults { + totalConsumed += txnResult.AppBudgetConsumed + } + + return simulationTestCase{ + input: simulation.Request{ + TxnGroups: [][]transactions.SignedTxn{ + signedTxns, + }, + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, + }, + developerAPI: true, + expected: simulation.Result{ + Version: simulation.ResultLatestVersion, + LastRound: env.TxnInfo.LatestRound(), + TraceConfig: simulation.ExecTraceConfig{ + Enable: true, + State: true, + }, + TxnGroups: []simulation.TxnGroupResult{ + { + Txns: txnResults, + AppBudgetAdded: 700 * uint64(len(txnResults)), + AppBudgetConsumed: totalConsumed, + }, + }, + InitialStates: &simulation.ResourcesInitialStates{ + AllAppsInitialStates: simulation.AppsInitialStates{ + appIDWithStates: simulation.SingleAppInitialStates{ + AppGlobals: simulation.AppKVPairs{ + "A": basics.TealValue{ + Type: basics.TealBytesType, + Bytes: "initial content A", + }, + }, + AppLocals: map[basics.Address]simulation.AppKVPairs{ + appCreator.Addr: { + "B": basics.TealValue{ + Type: basics.TealBytesType, + Bytes: "initial content B", + }, + }, + }, + AppBoxes: make(simulation.AppKVPairs), + CreatedGlobals: make(util.Set[string]), + CreatedBoxes: make(util.Set[string]), + CreatedLocals: map[basics.Address]util.Set[string]{}, + }, + }, + CreatedApp: make(util.Set[basics.AppIndex]), + }, + }, + } + }) +} + +// TestBalanceChangesWithApp sends a payment transaction to a new account and confirms its balance +// within a subsequent app call +func TestBalanceChangesWithApp(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { + sender := env.Accounts[0] + senderBalance := sender.AcctData.MicroAlgos.Raw + sendAmount := senderBalance - 500_000 // Leave 0.5 Algos in the sender account + receiver := env.Accounts[1] + receiverBalance := receiver.AcctData.MicroAlgos.Raw + + futureAppID := basics.AppIndex(1001) + createTxn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: sender.Addr, + ApprovalProgram: `#pragma version 6 +txn ApplicationID // [appId] +bz end // [] +int 1 // [1] +balance // [bal[1]] +itob // [itob(bal[1])] +txn ApplicationArgs 0 // [itob(bal[1]), args[0]] +== // [itob(bal[1])=?=args[0]] +assert +end: +int 1 // [1] +`, + ClearStateProgram: `#pragma version 6 +int 1`, + }) + checkStartingBalanceTxn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: sender.Addr, + ApplicationID: futureAppID, + Accounts: []basics.Address{receiver.Addr}, + ApplicationArgs: [][]byte{uint64ToBytes(receiverBalance)}, + }) + paymentTxn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.PaymentTx, + Sender: sender.Addr, + Receiver: receiver.Addr, + Amount: sendAmount, + }) + checkEndingBalanceTxn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: sender.Addr, + ApplicationID: futureAppID, + Accounts: []basics.Address{receiver.Addr}, + // Receiver's balance should have increased by sendAmount + ApplicationArgs: [][]byte{uint64ToBytes(receiverBalance + sendAmount)}, + }) + + txntest.Group(&createTxn, &checkStartingBalanceTxn, &paymentTxn, &checkEndingBalanceTxn) + + signedCreateTxn := createTxn.Txn().Sign(sender.Sk) + signedCheckStartingBalanceTxn := checkStartingBalanceTxn.Txn().Sign(sender.Sk) + signedPaymentTxn := paymentTxn.Txn().Sign(sender.Sk) + signedCheckEndingBalanceTxn := checkEndingBalanceTxn.Txn().Sign(sender.Sk) + + return simulationTestCase{ + input: simulation.Request{ + TxnGroups: [][]transactions.SignedTxn{ + { + signedCreateTxn, + signedCheckStartingBalanceTxn, + signedPaymentTxn, + signedCheckEndingBalanceTxn, + }, + }, + }, + expected: simulation.Result{ + Version: simulation.ResultLatestVersion, + LastRound: env.TxnInfo.LatestRound(), + TxnGroups: []simulation.TxnGroupResult{ + { + Txns: []simulation.TxnResult{ + { + Txn: transactions.SignedTxnWithAD{ + ApplyData: transactions.ApplyData{ + ApplicationID: futureAppID, + }, + }, + AppBudgetConsumed: 4, + }, + { + AppBudgetConsumed: 10, + }, + {}, + { + AppBudgetConsumed: 10, + }, + }, + AppBudgetAdded: 2100, + AppBudgetConsumed: 24, + }, + }, + }, + } + }) +} + +// TestOptionalSignatures tests that transactions with signatures and without signatures are both +// properly handled when AllowEmptySignatures is enabled. +func TestOptionalSignatures(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + for _, signed := range []bool{true, false} { + signed := signed + t.Run(fmt.Sprintf("signed=%t", signed), func(t *testing.T) { + simulationTest(t, func(env simulationtesting.Environment) simulationTestCase { + sender := env.Accounts[0] + + txn := env.TxnInfo.NewTxn(txntest.Txn{ + Type: protocol.PaymentTx, + Sender: sender.Addr, + Receiver: sender.Addr, + Amount: 1, + }) var stxn transactions.SignedTxn if signed { @@ -5802,6 +7373,10 @@ func (o boxOperation) appArgs() [][]byte { } } +func (o boxOperation) boxRefs() []transactions.BoxRef { + return []transactions.BoxRef{{Name: []byte(o.name)}} +} + type boxTestResult struct { Boxes map[logic.BoxRef]uint64 NumEmptyBoxRefs int diff --git a/ledger/simulation/trace.go b/ledger/simulation/trace.go index 7861292830..afc6a2124e 100644 --- a/ledger/simulation/trace.go +++ b/ledger/simulation/trace.go @@ -141,6 +141,7 @@ type Result struct { EvalOverrides ResultEvalOverrides Block *ledgercore.ValidatedBlock TraceConfig ExecTraceConfig + InitialStates *ResourcesInitialStates } // ReturnTrace reads from Result object and decides if simulation returns PC. @@ -217,6 +218,7 @@ func makeSimulationResult(lastRound basics.Round, request Request, developerAPI TxnGroups: groups, EvalOverrides: resultEvalConstants, TraceConfig: request.TraceConfig, + InitialStates: newResourcesInitialStates(request), }, nil } diff --git a/ledger/simulation/tracer.go b/ledger/simulation/tracer.go index 0d5f2eb5c1..2bc600ee38 100644 --- a/ledger/simulation/tracer.go +++ b/ledger/simulation/tracer.go @@ -270,7 +270,7 @@ func (tracer *evalTracer) makeOpcodeTraceUnit(cx *logic.EvalContext) OpcodeTrace } func (o *OpcodeTraceUnit) computeStackValueDeletions(cx *logic.EvalContext, tracer *evalTracer) { - tracer.popCount, tracer.addCount = cx.GetOpSpec().Explain(cx) + tracer.popCount, tracer.addCount = cx.GetOpSpec().StackExplain(cx) o.StackPopCount = uint64(tracer.popCount) stackHeight := len(cx.Stack) @@ -309,6 +309,7 @@ func (tracer *evalTracer) BeforeOpcode(cx *logic.EvalContext) { } if tracer.result.ReturnStateChange() { latestOpcodeTraceUnit.appendStateOperations(cx) + tracer.result.InitialStates.increment(cx) } } } @@ -325,10 +326,14 @@ func (o *OpcodeTraceUnit) appendAddedStackValue(cx *logic.EvalContext, tracer *e } func (o *OpcodeTraceUnit) appendStateOperations(cx *logic.EvalContext) { - if cx.GetOpSpec().StateExplain == nil { + if cx.GetOpSpec().AppStateExplain == nil { + return + } + appState, stateOp, appID, acctAddr, stateKey := cx.GetOpSpec().AppStateExplain(cx) + // If the operation is not write or delete, return without + if stateOp == logic.AppStateRead { return } - appState, stateOp, appID, acctAddr, stateKey := cx.GetOpSpec().StateExplain(cx) o.StateChanges = append(o.StateChanges, StateOperation{ AppStateOp: stateOp, AppState: appState, @@ -376,7 +381,7 @@ func (tracer *evalTracer) recordUpdatedScratchVars(cx *logic.EvalContext) []Scra func (o *OpcodeTraceUnit) updateNewStateValues(cx *logic.EvalContext) { for i, sc := range o.StateChanges { - o.StateChanges[i].NewValue = logic.AppNewStateQuerying( + o.StateChanges[i].NewValue = logic.AppStateQuerying( cx, sc.AppState, sc.AppStateOp, sc.AppID, sc.Account, sc.Key) } } @@ -447,6 +452,13 @@ func (tracer *evalTracer) BeforeProgram(cx *logic.EvalContext) { txnTraceStackElem.ApprovalProgramHash = programHash } } + if tracer.result.ReturnStateChange() { + // If we are recording state changes, including initial states, + // then we should exclude initial states of created app during simulation. + if cx.TxnGroup[groupIndex].SignedTxn.Txn.ApplicationID == 0 { + tracer.result.InitialStates.CreatedApp.Add(cx.AppID()) + } + } if tracer.unnamedResourcePolicy != nil { globalSharing := false diff --git a/ledger/store/trackerdb/sqlitedriver/merkle_commiter.go b/ledger/store/trackerdb/sqlitedriver/merkle_committer.go similarity index 100% rename from ledger/store/trackerdb/sqlitedriver/merkle_commiter.go rename to ledger/store/trackerdb/sqlitedriver/merkle_committer.go diff --git a/netdeploy/network.go b/netdeploy/network.go index 02202d5559..4e86bd831f 100644 --- a/netdeploy/network.go +++ b/netdeploy/network.go @@ -70,7 +70,7 @@ func CreateNetworkFromTemplate(name, rootDir string, templateReader io.Reader, b var err error template := defaultNetworkTemplate - err = loadTemplateFromReader(templateReader, &template) + err = LoadTemplateFromReader(templateReader, &template) if err == nil { if overrideDevMode { @@ -100,7 +100,7 @@ func CreateNetworkFromTemplate(name, rootDir string, templateReader io.Reader, b return n, err } template.Consensus = consensus - err = template.generateGenesisAndWallets(rootDir, n.cfg.Name, binDir) + err = template.generateGenesisAndWallets(rootDir, n.cfg.Name) if err != nil { return n, err } diff --git a/netdeploy/networkTemplate.go b/netdeploy/networkTemplate.go index 40ea5d4985..43bb9d816f 100644 --- a/netdeploy/networkTemplate.go +++ b/netdeploy/networkTemplate.go @@ -48,7 +48,7 @@ var defaultNetworkTemplate = NetworkTemplate{ Genesis: gen.DefaultGenesis, } -func (t NetworkTemplate) generateGenesisAndWallets(targetFolder, networkName, binDir string) error { +func (t NetworkTemplate) generateGenesisAndWallets(targetFolder, networkName string) error { genesisData := t.Genesis genesisData.NetworkName = networkName mergedConsensus := config.Consensus.Merge(t.Consensus) @@ -180,11 +180,12 @@ func loadTemplate(templateFile string) (NetworkTemplate, error) { } defer f.Close() - err = loadTemplateFromReader(f, &template) + err = LoadTemplateFromReader(f, &template) return template, err } -func loadTemplateFromReader(reader io.Reader, template *NetworkTemplate) error { +// LoadTemplateFromReader loads and decodes a network template +func LoadTemplateFromReader(reader io.Reader, template *NetworkTemplate) error { if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { // for arm machines, use smaller key dilution diff --git a/netdeploy/networkTemplates_test.go b/netdeploy/networkTemplates_test.go index f21c37ace9..80f2c2a43c 100644 --- a/netdeploy/networkTemplates_test.go +++ b/netdeploy/networkTemplates_test.go @@ -69,9 +69,8 @@ func TestGenerateGenesis(t *testing.T) { targetFolder := t.TempDir() networkName := "testGenGen" - binDir := os.ExpandEnv("${GOPATH}/bin") - err := template.generateGenesisAndWallets(targetFolder, networkName, binDir) + err := template.generateGenesisAndWallets(targetFolder, networkName) a.NoError(err) _, err = os.Stat(filepath.Join(targetFolder, config.GenesisJSONFile)) fileExists := err == nil diff --git a/network/p2p/capabilities.go b/network/p2p/capabilities.go new file mode 100644 index 0000000000..149d643da1 --- /dev/null +++ b/network/p2p/capabilities.go @@ -0,0 +1,172 @@ +// Copyright (C) 2019-2023 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" + "sync" + "time" + + dht "github.com/libp2p/go-libp2p-kad-dht" + "github.com/libp2p/go-libp2p/core/discovery" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + libpeerstore "github.com/libp2p/go-libp2p/core/peerstore" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/logging" + algoDht "github.com/algorand/go-algorand/network/p2p/dht" + "github.com/algorand/go-algorand/network/p2p/peerstore" +) + +// Capability represents functions that some nodes may provide and other nodes would want to know about +type Capability string + +const ( + // Archival nodes + Archival Capability = "archival" + // Catchpoints storing nodes + Catchpoints = "catchpointStoring" +) + +const operationTimeout = time.Second * 5 +const maxAdvertisementInterval = time.Hour * 22 + +// CapabilitiesDiscovery exposes Discovery interfaces and wraps underlying DHT methods to provide capabilities advertisement for the node +type CapabilitiesDiscovery struct { + disc discovery.Discovery + dht *dht.IpfsDHT + log logging.Logger + wg sync.WaitGroup +} + +// Advertise implements the discovery.Discovery/discovery.Advertiser interface +func (c *CapabilitiesDiscovery) Advertise(ctx context.Context, ns string, opts ...discovery.Option) (time.Duration, error) { + return c.disc.Advertise(ctx, ns, opts...) +} + +// FindPeers implements the discovery.Discovery/discovery.Discoverer interface +func (c *CapabilitiesDiscovery) FindPeers(ctx context.Context, ns string, opts ...discovery.Option) (<-chan peer.AddrInfo, error) { + return c.disc.FindPeers(ctx, ns, opts...) +} + +// Close should be called when fully shutting down the node +func (c *CapabilitiesDiscovery) Close() { + _ = c.dht.Close() + c.wg.Wait() +} + +// Host exposes the underlying libp2p host.Host object +func (c *CapabilitiesDiscovery) Host() host.Host { + return c.dht.Host() +} + +// AddPeer adds a given peer.AddrInfo to the Host's Peerstore, and the DHT's routing table +func (c *CapabilitiesDiscovery) AddPeer(p peer.AddrInfo) (bool, error) { + c.Host().Peerstore().AddAddrs(p.ID, p.Addrs, libpeerstore.AddressTTL) + return c.dht.RoutingTable().TryAddPeer(p.ID, true, true) +} + +// PeersForCapability returns a slice of peer.AddrInfo for a Capability +// Since CapabilitiesDiscovery uses a backoffcache, it will attempt to hit cache, then disk, then network +// in order to fetch n peers which are advertising the required capability. +func (c *CapabilitiesDiscovery) PeersForCapability(capability Capability, n int) ([]peer.AddrInfo, error) { + ctx, cancel := context.WithTimeout(context.Background(), operationTimeout) + defer cancel() + var peers []peer.AddrInfo + peersChan, err := c.FindPeers(ctx, string(capability), discovery.Limit(n)) + if err != nil { + return nil, err + } + for p := range peersChan { + if p.ID.Size() > 0 && p.ID != c.Host().ID() { + peers = append(peers, p) + } + if len(peers) >= n { + break + } + } + return peers, nil +} + +// AdvertiseCapabilities periodically runs the Advertiser interface on the DHT +// If a capability fails to advertise we will retry every 10 seconds until full success +// This gets rerun every at the minimum ttl or the maxAdvertisementInterval. +func (c *CapabilitiesDiscovery) AdvertiseCapabilities(capabilities ...Capability) { + c.wg.Add(1) + go func() { + // Run the initial Advertisement immediately + nextExecution := time.After(time.Second / 10000) + defer func() { + c.wg.Done() + }() + + for { + select { + case <-c.dht.Context().Done(): + return + case <-nextExecution: + var err error + advertisementInterval := maxAdvertisementInterval + for _, capa := range capabilities { + ttl, err0 := c.Advertise(c.dht.Context(), string(capa)) + if err0 != nil { + err = err0 + c.log.Errorf("failed to advertise for capability %s: %v", capa, err0) + break + } + if ttl < advertisementInterval { + advertisementInterval = ttl + } + c.log.Infof("advertised capability %s", capa) + } + // If we failed to advertise, retry every 10 seconds until successful + if err != nil { + nextExecution = time.After(time.Second * 10) + } else { + // Otherwise, ensure we're at the correct interval + nextExecution = time.After(advertisementInterval) + } + } + } + }() +} + +// MakeCapabilitiesDiscovery creates a new CapabilitiesDiscovery object which exposes peer discovery and capabilities advertisement +func MakeCapabilitiesDiscovery(ctx context.Context, cfg config.Local, datadir string, network string, log logging.Logger, bootstrapPeers []*peer.AddrInfo) (*CapabilitiesDiscovery, error) { + pstore, err := peerstore.NewPeerStore(bootstrapPeers) + if err != nil { + return nil, err + } + h, err := makeHost(cfg, datadir, pstore) + if err != nil { + return nil, err + } + discDht, err := algoDht.MakeDHT(ctx, h, network, cfg, bootstrapPeers) + if err != nil { + return nil, err + } + discImpl, err := algoDht.MakeDiscovery(discDht) + if err != nil { + return nil, err + } + return &CapabilitiesDiscovery{ + disc: discImpl, + dht: discDht, + log: log, + }, nil +} diff --git a/network/p2p/capabilities_test.go b/network/p2p/capabilities_test.go new file mode 100644 index 0000000000..170916dd65 --- /dev/null +++ b/network/p2p/capabilities_test.go @@ -0,0 +1,289 @@ +// Copyright (C) 2019-2023 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" + "testing" + "time" + + golog "github.com/ipfs/go-log" + "github.com/libp2p/go-libp2p" + dht "github.com/libp2p/go-libp2p-kad-dht" + "github.com/libp2p/go-libp2p/core/discovery" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/logging" + algodht "github.com/algorand/go-algorand/network/p2p/dht" + "github.com/algorand/go-algorand/network/p2p/peerstore" + "github.com/algorand/go-algorand/test/partitiontest" +) + +func TestCapabilitiesDiscovery(t *testing.T) { + partitiontest.PartitionTest(t) + + golog.SetDebugLogging() + var caps []*CapabilitiesDiscovery + var addrs []peer.AddrInfo + testSize := 3 + for i := 0; i < testSize; i++ { + tempdir := t.TempDir() + capD, err := MakeCapabilitiesDiscovery(context.Background(), config.GetDefaultLocal(), tempdir, "devtestnet", logging.Base(), []*peer.AddrInfo{}) + require.NoError(t, err) + caps = append(caps, capD) + addrs = append(addrs, peer.AddrInfo{ + ID: capD.Host().ID(), + Addrs: capD.Host().Addrs(), + }) + } + for _, capD := range caps { + peersAdded := 0 + for _, addr := range addrs { + added, err := capD.AddPeer(addr) + require.NoError(t, err) + require.True(t, added) + peersAdded++ + } + err := capD.dht.Bootstrap(context.Background()) + require.NoError(t, err) + capD.dht.ForceRefresh() + require.Equal(t, peersAdded, capD.dht.RoutingTable().Size()) + } +} + +func setupDHTHosts(t *testing.T, numHosts int) []*dht.IpfsDHT { + var hosts []host.Host + var bootstrapPeers []*peer.AddrInfo + var dhts []*dht.IpfsDHT + cfg := config.GetDefaultLocal() + for i := 0; i < numHosts; i++ { + tmpdir := t.TempDir() + pk, err := GetPrivKey(cfg, tmpdir) + require.NoError(t, err) + ps, err := peerstore.NewPeerStore([]*peer.AddrInfo{}) + require.NoError(t, err) + h, err := libp2p.New( + libp2p.ListenAddrStrings("/dns4/localhost/tcp/0"), + libp2p.Identity(pk), + libp2p.Peerstore(ps)) + require.NoError(t, err) + hosts = append(hosts, h) + bootstrapPeers = append(bootstrapPeers, &peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}) + } + for _, h := range hosts { + ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, bootstrapPeers) + require.NoError(t, err) + err = ht.Bootstrap(context.Background()) + require.NoError(t, err) + dhts = append(dhts, ht) + } + return dhts +} + +func waitForRouting(t *testing.T, disc *CapabilitiesDiscovery) { + refreshCtx, refCancel := context.WithTimeout(context.Background(), time.Second*5) + for { + select { + case <-refreshCtx.Done(): + refCancel() + require.Fail(t, "failed to populate routing table before timeout") + default: + if disc.dht.RoutingTable().Size() > 0 { + refCancel() + return + } + } + } +} + +func setupCapDiscovery(t *testing.T, numHosts int) []*CapabilitiesDiscovery { + var hosts []host.Host + var bootstrapPeers []*peer.AddrInfo + var capsDisc []*CapabilitiesDiscovery + cfg := config.GetDefaultLocal() + for i := 0; i < numHosts; i++ { + tmpdir := t.TempDir() + pk, err := GetPrivKey(cfg, tmpdir) + require.NoError(t, err) + ps, err := peerstore.NewPeerStore([]*peer.AddrInfo{}) + require.NoError(t, err) + h, err := libp2p.New( + libp2p.ListenAddrStrings("/dns4/localhost/tcp/0"), + libp2p.Identity(pk), + libp2p.Peerstore(ps)) + require.NoError(t, err) + hosts = append(hosts, h) + bootstrapPeers = append(bootstrapPeers, &peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}) + } + for _, h := range hosts { + ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, bootstrapPeers) + require.NoError(t, err) + disc, err := algodht.MakeDiscovery(ht) + require.NoError(t, err) + cd := &CapabilitiesDiscovery{ + disc: disc, + dht: ht, + log: logging.Base(), + } + capsDisc = append(capsDisc, cd) + } + return capsDisc +} + +func TestDHTTwoPeers(t *testing.T) { + partitiontest.PartitionTest(t) + + numAdvertisers := 2 + dhts := setupDHTHosts(t, numAdvertisers) + topic := "foobar" + for i, ht := range dhts { + disc, err := algodht.MakeDiscovery(ht) + require.NoError(t, err) + refreshCtx, refCancel := context.WithTimeout(context.Background(), time.Second*5) + peersPopulated: + for { + select { + case <-refreshCtx.Done(): + refCancel() + require.Fail(t, "failed to populate routing table before timeout") + default: + if ht.RoutingTable().Size() > 0 { + refCancel() + break peersPopulated + } + } + } + _, err = disc.Advertise(context.Background(), topic) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + var advertisers []peer.AddrInfo + peersChan, err := disc.FindPeers(ctx, topic, discovery.Limit(numAdvertisers)) + pollingForPeers: + for { + select { + case p, open := <-peersChan: + if p.ID.Size() > 0 { + advertisers = append(advertisers, p) + } + if !open { + break pollingForPeers + } + } + } + cancel() + // Returned peers will include the querying node's ID since it advertises for the topic as well + require.Equal(t, i+1, len(advertisers)) + } +} + +func TestVaryingCapabilities(t *testing.T) { + partitiontest.PartitionTest(t) + + numAdvertisers := 10 + capsDisc := setupCapDiscovery(t, numAdvertisers) + noCap := capsDisc[:3] + archOnly := capsDisc[3:5] + catchOnly := capsDisc[5:7] + archCatch := capsDisc[7:] + + for _, disc := range archOnly { + waitForRouting(t, disc) + disc.AdvertiseCapabilities(Archival) + } + for _, disc := range catchOnly { + waitForRouting(t, disc) + disc.AdvertiseCapabilities(Catchpoints) + } + for _, disc := range archCatch { + waitForRouting(t, disc) + disc.AdvertiseCapabilities(Archival, Catchpoints) + } + + for _, disc := range noCap { + require.Eventuallyf(t, + func() bool { + numArchPeers := len(archOnly) + len(archCatch) + peers, err := disc.PeersForCapability(Archival, numArchPeers) + if err == nil && len(peers) == numArchPeers { + return true + } + return false + }, + time.Minute, + time.Second, + "Not all expected archival peers were found", + ) + + require.Eventuallyf(t, + func() bool { + numCatchPeers := len(catchOnly) + len(archCatch) + peers, err := disc.PeersForCapability(Catchpoints, numCatchPeers) + if err == nil && len(peers) == numCatchPeers { + return true + } + return false + }, + time.Minute, + time.Second, + "Not all expected catchpoint peers were found", + ) + } + + for _, disc := range capsDisc[3:] { + disc.Close() + // Make sure it actually closes + disc.wg.Wait() + } +} + +func TestCapabilitiesExcludesSelf(t *testing.T) { + partitiontest.PartitionTest(t) + disc := setupCapDiscovery(t, 2) + + testPeersFound := func(disc *CapabilitiesDiscovery, n int, cap Capability) bool { + peers, err := disc.PeersForCapability(cap, n+1) + if err == nil && len(peers) == n { + return true + } + return false + } + + waitForRouting(t, disc[0]) + disc[0].AdvertiseCapabilities(Archival) + // disc[1] finds Archival + require.Eventuallyf(t, + func() bool { return testPeersFound(disc[1], 1, Archival) }, + time.Minute, + time.Second, + "Could not find archival peer", + ) + + // disc[0] doesn't find itself + require.Neverf(t, + func() bool { return testPeersFound(disc[0], 1, Archival) }, + time.Second*5, + time.Second, + "Found self when searching for capability", + ) + + disc[0].Close() + disc[0].wg.Wait() +} diff --git a/network/p2p/dht/dht.go b/network/p2p/dht/dht.go new file mode 100644 index 0000000000..e612f6062d --- /dev/null +++ b/network/p2p/dht/dht.go @@ -0,0 +1,100 @@ +// Copyright (C) 2019-2023 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 dht + +import ( + "context" + "fmt" + "math/rand" + "time" + + dht "github.com/libp2p/go-libp2p-kad-dht" + "github.com/libp2p/go-libp2p/core/discovery" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + crouting "github.com/libp2p/go-libp2p/core/routing" + "github.com/libp2p/go-libp2p/p2p/discovery/backoff" + "github.com/libp2p/go-libp2p/p2p/discovery/routing" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/network/p2p/dnsaddr" + algoproto "github.com/algorand/go-algorand/protocol" +) + +const minBackoff = time.Second * 5 +const maxBackoff = time.Second * 20 +const baseBackoff = float64(1) + +// getBootstrapPeersFunc looks up a list of Multiaddrs strings from the dnsaddr records at the primary +// SRV record domain. +func getBootstrapPeersFunc(cfg config.Local, network string) func() []peer.AddrInfo { + return func() []peer.AddrInfo { + var addrs []peer.AddrInfo + bootstraps := cfg.DNSBootstrapArray(algoproto.NetworkID(network)) + for _, dnsBootstrap := range bootstraps { + controller := dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecuritySRVEnforced(), "") + resolvedAddrs, err := dnsaddr.MultiaddrsFromResolver(dnsBootstrap.PrimarySRVBootstrap, controller) + if err != nil { + continue + } + for _, resolvedAddr := range resolvedAddrs { + info, err0 := peer.AddrInfoFromP2pAddr(resolvedAddr) + if err0 != nil { + continue + } + addrs = append(addrs, *info) + } + } + return addrs + } +} + +func dhtProtocolPrefix(network string) protocol.ID { + return protocol.ID(fmt.Sprintf("/algorand/kad/%s", network)) +} + +// MakeDHT creates the dht.IpfsDHT object +func MakeDHT(ctx context.Context, h host.Host, network string, cfg config.Local, bootstrapPeers []*peer.AddrInfo) (*dht.IpfsDHT, error) { + var peers []peer.AddrInfo + for _, bPeer := range bootstrapPeers { + if bPeer != nil { + peers = append(peers, *bPeer) + } + } + dhtCfg := []dht.Option{ + // Automatically determine server or client mode + dht.Mode(dht.ModeAutoServer), + // We don't need the value store right now + dht.DisableValues(), + dht.ProtocolPrefix(dhtProtocolPrefix(network)), + dht.BootstrapPeers(peers...), + } + if len(bootstrapPeers) == 0 { + dhtCfg = append(dhtCfg, dht.BootstrapPeersFunc(getBootstrapPeersFunc(cfg, network))) + } + return dht.New(ctx, h, dhtCfg...) +} + +func backoffFactory() backoff.BackoffFactory { + return backoff.NewExponentialDecorrelatedJitter(minBackoff, maxBackoff, baseBackoff, rand.New(rand.NewSource(rand.Int63()))) +} + +// MakeDiscovery creates a discovery.Discovery object using backoff and cacching +func MakeDiscovery(r crouting.ContentRouting) (discovery.Discovery, error) { + return backoff.NewBackoffDiscovery(routing.NewRoutingDiscovery(r), backoffFactory(), backoff.WithBackoffDiscoveryReturnedChannelSize(0), backoff.WithBackoffDiscoverySimultaneousQueryBufferSize(0)) +} diff --git a/network/p2p/dht/dht_test.go b/network/p2p/dht/dht_test.go new file mode 100644 index 0000000000..3d4eabb3bc --- /dev/null +++ b/network/p2p/dht/dht_test.go @@ -0,0 +1,106 @@ +// Copyright (C) 2019-2023 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 dht + +import ( + "context" + "testing" + + logging "github.com/ipfs/go-log" + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/test/partitiontest" +) + +func TestDHTBasic(t *testing.T) { + partitiontest.PartitionTest(t) + + h, err := libp2p.New() + require.NoError(t, err) + dht, err := MakeDHT( + context.Background(), + h, + "devtestnet", + config.GetDefaultLocal(), + []*peer.AddrInfo{{}}) + require.NoError(t, err) + _, err = MakeDiscovery(dht) + require.NoError(t, err) + err = dht.Bootstrap(context.Background()) + require.NoError(t, err) +} + +func TestDHTBasicAlgodev(t *testing.T) { + partitiontest.PartitionTest(t) + + logging.SetDebugLogging() + h, err := libp2p.New() + require.NoError(t, err) + cfg := config.GetDefaultLocal() + cfg.DNSBootstrapID = ".algodev.network" + dht, err := MakeDHT(context.Background(), h, "betanet", cfg, []*peer.AddrInfo{}) + require.NoError(t, err) + _, err = MakeDiscovery(dht) + require.NoError(t, err) + err = dht.Bootstrap(context.Background()) + require.NoError(t, err) +} + +func TestGetBootstrapPeers(t *testing.T) { + t.Parallel() + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.DNSBootstrapID = ".algodev.network" + cfg.DNSSecurityFlags = 0 + + addrs := getBootstrapPeersFunc(cfg, "test")() + + require.GreaterOrEqual(t, len(addrs), 1) + addr := addrs[0] + require.Equal(t, len(addr.Addrs), 1) + require.GreaterOrEqual(t, len(addr.Addrs), 1) +} + +func TestGetBootstrapPeersFailure(t *testing.T) { + t.Parallel() + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.DNSSecurityFlags = 0 + cfg.DNSBootstrapID = "non-existent.algodev.network" + + addrs := getBootstrapPeersFunc(cfg, "test")() + + require.Equal(t, 0, len(addrs)) +} + +func TestGetBootstrapPeersInvalidAddr(t *testing.T) { + t.Parallel() + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.DNSSecurityFlags = 0 + cfg.DNSBootstrapID = ".algodev.network" + + addrs := getBootstrapPeersFunc(cfg, "testInvalidAddr")() + + require.Equal(t, 0, len(addrs)) +} diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 70c9267731..42db4694c4 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "runtime" + "strings" "time" "github.com/algorand/go-algorand/config" @@ -69,8 +70,7 @@ const AlgorandWsProtocol = "/algorand-ws/1.0.0" const dialTimeout = 30 * time.Second -// MakeService creates a P2P service instance -func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, datadir string, pstore peerstore.Peerstore, wsStreamHandler StreamHandler) (*serviceImpl, error) { +func makeHost(cfg config.Local, datadir string, pstore peerstore.Peerstore) (host.Host, error) { // load stored peer ID, or make ephemeral peer ID privKey, err := GetPrivKey(cfg, datadir) if err != nil { @@ -83,14 +83,28 @@ func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, data version := config.GetCurrentVersion() ua := fmt.Sprintf("algod/%d.%d (%s; commit=%s; %d) %s(%s)", version.Major, version.Minor, version.Channel, version.CommitHash, version.BuildNumber, runtime.GOOS, runtime.GOARCH) - h, err := libp2p.New( + var listenAddr string + if cfg.NetAddress != "" { + if parsedListenAddr, perr := netAddressToListenAddress(cfg.NetAddress); perr == nil { + listenAddr = parsedListenAddr + } + } else { + listenAddr = "/ip4/0.0.0.0/tcp/0" + } + + return libp2p.New( libp2p.Identity(privKey), libp2p.UserAgent(ua), libp2p.Transport(tcp.NewTCPTransport), libp2p.Muxer("/yamux/1.0.0", &ymx), libp2p.Peerstore(pstore), - libp2p.ListenAddrStrings("/ip4/0.0.0.0/tcp/0"), + libp2p.ListenAddrStrings(listenAddr), ) +} + +// MakeService creates a P2P service instance +func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, datadir string, pstore peerstore.Peerstore, wsStreamHandler StreamHandler) (*serviceImpl, error) { + h, err := makeHost(cfg, datadir, pstore) if err != nil { return nil, err } @@ -173,3 +187,23 @@ func (s *serviceImpl) Conns() []network.Conn { func (s *serviceImpl) ClosePeer(peer peer.ID) error { return s.host.Network().ClosePeer(peer) } + +// 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) { + // split the string on ":" + // if there are more than 2 parts, return an error + parts := strings.Split(netAddress, ":") + if len(parts) != 2 { + return "", fmt.Errorf("invalid netAddress %s; required format is \"ip:port\"", netAddress) + } + ip := "0.0.0.0" + if parts[0] != "" { + ip = parts[0] + } + if parts[1] == "" { + return "", fmt.Errorf("invalid netAddress %s, port is required", netAddress) + } + + return fmt.Sprintf("/ip4/%s/tcp/%s", ip, parts[1]), nil +} diff --git a/network/p2p/p2p_test.go b/network/p2p/p2p_test.go new file mode 100644 index 0000000000..5095e0d4b5 --- /dev/null +++ b/network/p2p/p2p_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2019-2023 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 ( + "fmt" + "testing" + + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" +) + +// Tests the helper function netAddressToListenAddress which converts +// a config value netAddress to a multiaddress usable by libp2p. +func TestNetAddressToListenAddress(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + tests := []struct { + input string + output string + err bool + }{ + { + input: "192.168.1.1:8080", + output: "/ip4/192.168.1.1/tcp/8080", + err: false, + }, + { + input: ":8080", + output: "/ip4/0.0.0.0/tcp/8080", + err: false, + }, + { + input: "192.168.1.1:", + output: "", + err: true, + }, + { + input: "192.168.1.1", + output: "", + err: true, + }, + { + input: "192.168.1.1:8080:9090", + output: "", + err: true, + }, + } + + for _, test := range tests { //nolint:paralleltest + t.Run(fmt.Sprintf("input: %s", test.input), func(t *testing.T) { + res, err := netAddressToListenAddress(test.input) + if test.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.output, res) + } + }) + } +} diff --git a/network/p2p/peerstore/peerstore.go b/network/p2p/peerstore/peerstore.go index ee6bf90a65..63a88966ff 100644 --- a/network/p2p/peerstore/peerstore.go +++ b/network/p2p/peerstore/peerstore.go @@ -18,15 +18,52 @@ package peerstore import ( "fmt" + "math" + "math/rand" + "time" "github.com/libp2p/go-libp2p/core/peer" libp2p "github.com/libp2p/go-libp2p/core/peerstore" mempstore "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" + "golang.org/x/exp/slices" ) +// when using GetAddresses with getAllAddresses, all the addresses will be retrieved, regardless +// of how many addresses the phonebook actually has. ( with the retry-after logic applied ) +const getAllAddresses = math.MaxInt32 + +// PhoneBookEntryRoles defines the roles that a single entry on the phonebook can take. +// currently, we have two roles : relay role and archiver role, which are mutually exclusive. +// +//msgp:ignore PhoneBookEntryRoles +type PhoneBookEntryRoles int + +const addressDataKey string = "addressData" + // PeerStore implements Peerstore and CertifiedAddrBook. type PeerStore struct { peerStoreCAB + connectionsRateLimitingCount uint + connectionsRateLimitingWindow time.Duration +} + +// addressData: holds the information associated with each phonebook address. +type addressData struct { + // retryAfter is the time to wait before retrying to connect to the address. + retryAfter time.Time + + // recentConnectionTimes is the log of connection times used to observe the maximum + // connections to the address in a given time window. + recentConnectionTimes []time.Time + + // networkNames: lists the networks to which the given address belongs. + networkNames map[string]bool + + // role is the role that this address serves. + role PhoneBookEntryRoles + + // persistent is set true for peers whose record should not be removed for the peer list + persistent bool } // peerStoreCAB combines the libp2p Peerstore and CertifiedAddrBook interfaces. @@ -47,6 +84,294 @@ func NewPeerStore(addrInfo []*peer.AddrInfo) (*PeerStore, error) { info := addrInfo[i] ps.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) } - pstore := &PeerStore{ps} + pstore := &PeerStore{peerStoreCAB: ps} + return pstore, nil +} + +// MakePhonebook creates a phonebook with the passed configuration values +func MakePhonebook(connectionsRateLimitingCount uint, + connectionsRateLimitingWindow time.Duration) (*PeerStore, error) { + ps, err := mempstore.NewPeerstore() + if err != nil { + return &PeerStore{}, fmt.Errorf("cannot initialize a peerstore: %w", err) + } + pstore := &PeerStore{peerStoreCAB: ps, + connectionsRateLimitingCount: connectionsRateLimitingCount, + connectionsRateLimitingWindow: connectionsRateLimitingWindow, + } return pstore, nil } + +// GetAddresses returns up to N addresses, but may return fewer +func (ps *PeerStore) GetAddresses(n int, role PhoneBookEntryRoles) []string { + return shuffleSelect(ps.filterRetryTime(time.Now(), role), n) +} + +// UpdateRetryAfter updates the retryAfter time for the given address. +func (ps *PeerStore) UpdateRetryAfter(addr string, retryAfter time.Time) { + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return + } + metadata, _ := ps.Get(info.ID, addressDataKey) + if metadata != nil { + ad, ok := metadata.(addressData) + if !ok { + return + } + ad.retryAfter = retryAfter + _ = ps.Put(info.ID, addressDataKey, ad) + } + +} + +// GetConnectionWaitTime will calculate and return the wait +// time to prevent exceeding connectionsRateLimitingCount. +// The connection should be established when the waitTime is 0. +// It will register a provisional next connection time when the waitTime is 0. +// The provisional time should be updated after the connection with UpdateConnectionTime +func (ps *PeerStore) GetConnectionWaitTime(addr string) (bool, time.Duration, time.Time) { + curTime := time.Now() + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return false, 0 /* not used */, curTime /* not used */ + } + var timeSince time.Duration + var numElmtsToRemove int + metadata, err := ps.Get(info.ID, addressDataKey) + if err != nil { + return false, 0 /* not used */, curTime /* not used */ + } + ad, ok := metadata.(addressData) + if !ok { + return false, 0 /* not used */, curTime /* not used */ + } + // Remove from recentConnectionTimes the times later than ConnectionsRateLimitingWindowSeconds + for numElmtsToRemove < len(ad.recentConnectionTimes) { + timeSince = curTime.Sub(ad.recentConnectionTimes[numElmtsToRemove]) + if timeSince >= ps.connectionsRateLimitingWindow { + numElmtsToRemove++ + } else { + break // break the loop. The rest are earlier than 1 second + } + } + + // Remove the expired elements from e.data[addr].recentConnectionTimes + ps.popNElements(numElmtsToRemove, peer.ID(addr)) + // If there are max number of connections within the time window, wait + metadata, _ = ps.Get(info.ID, addressDataKey) + ad, ok = metadata.(addressData) + if !ok { + return false, 0 /* not used */, curTime /* not used */ + } + numElts := len(ad.recentConnectionTimes) + if uint(numElts) >= ps.connectionsRateLimitingCount { + return true, /* true */ + ps.connectionsRateLimitingWindow - timeSince, curTime /* not used */ + } + + // Else, there is space in connectionsRateLimitingCount. The + // connection request of the caller will proceed + // Update curTime, since it may have significantly changed if waited + provisionalTime := time.Now() + // Append the provisional time for the next connection request + ps.appendTime(info.ID, provisionalTime) + return true, 0 /* no wait. proceed */, provisionalTime +} + +// UpdateConnectionTime updates the connection time for the given address. +func (ps *PeerStore) UpdateConnectionTime(addr string, provisionalTime time.Time) bool { + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return false + } + metadata, err := ps.Get(info.ID, addressDataKey) + if err != nil { + return false + } + ad, ok := metadata.(addressData) + if !ok { + return false + } + defer func() { + _ = ps.Put(info.ID, addressDataKey, ad) + + }() + + // Find the provisionalTime and update it + entry := ad.recentConnectionTimes + for indx, val := range entry { + if provisionalTime == val { + entry[indx] = time.Now() + return true + } + } + + // Case where the time is not found: it was removed from the list. + // This may happen when the time expires before the connection was established with the server. + // The time should be added again. + entry = append(entry, time.Now()) + ad.recentConnectionTimes = entry + + return true +} + +// ReplacePeerList replaces the peer list for the given networkName and role. +func (ps *PeerStore) ReplacePeerList(addressesThey []string, networkName string, role PhoneBookEntryRoles) { + // prepare a map of items we'd like to remove. + removeItems := make(map[peer.ID]bool, 0) + peerIDs := ps.Peers() + for _, pid := range peerIDs { + data, _ := ps.Get(pid, addressDataKey) + if data != nil { + ad := data.(addressData) + if ad.networkNames[networkName] && ad.role == role && !ad.persistent { + removeItems[pid] = true + } + } + + } + for _, addr := range addressesThey { + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return + } + data, _ := ps.Get(info.ID, addressDataKey) + if data != nil { + // we already have this. + // Update the networkName + ad := data.(addressData) + ad.networkNames[networkName] = true + + // do not remove this entry + delete(removeItems, info.ID) + } else { + // we don't have this item. add it. + ps.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + entry := makePhonebookEntryData(networkName, role, false) + _ = ps.Put(info.ID, addressDataKey, entry) + } + } + + // remove items that were missing in addressesThey + for k := range removeItems { + ps.deletePhonebookEntry(k, networkName) + } +} + +// AddPersistentPeers stores addresses of peers which are persistent. +// i.e. they won't be replaced by ReplacePeerList calls +func (ps *PeerStore) AddPersistentPeers(dnsAddresses []string, networkName string, role PhoneBookEntryRoles) { + + for _, addr := range dnsAddresses { + info, err := PeerInfoFromDomainPort(addr) + if err != nil { + return + } + data, _ := ps.Get(info.ID, addressDataKey) + if data != nil { + // we already have this. + // Make sure the persistence field is set to true + ad := data.(addressData) + ad.persistent = true + _ = ps.Put(info.ID, addressDataKey, data) + + } else { + // we don't have this item. add it. + ps.AddAddrs(info.ID, info.Addrs, libp2p.PermanentAddrTTL) + entry := makePhonebookEntryData(networkName, role, true) + _ = ps.Put(info.ID, addressDataKey, entry) + } + } +} + +// Length returns the number of addrs in peerstore +func (ps *PeerStore) Length() int { + return len(ps.Peers()) +} + +// makePhonebookEntryData creates a new address entry for provided network name and role. +func makePhonebookEntryData(networkName string, role PhoneBookEntryRoles, persistent bool) addressData { + pbData := addressData{ + networkNames: make(map[string]bool), + recentConnectionTimes: make([]time.Time, 0), + role: role, + persistent: persistent, + } + pbData.networkNames[networkName] = true + return pbData +} + +func (ps *PeerStore) deletePhonebookEntry(peerID peer.ID, networkName string) { + data, err := ps.Get(peerID, addressDataKey) + if err != nil { + return + } + ad := data.(addressData) + delete(ad.networkNames, networkName) + if 0 == len(ad.networkNames) { + ps.ClearAddrs(peerID) + _ = ps.Put(peerID, addressDataKey, nil) + } +} + +// AppendTime adds the current time to recentConnectionTimes in +// addressData of addr +func (ps *PeerStore) appendTime(peerID peer.ID, t time.Time) { + data, _ := ps.Get(peerID, addressDataKey) + ad := data.(addressData) + ad.recentConnectionTimes = append(ad.recentConnectionTimes, t) + _ = ps.Put(peerID, addressDataKey, ad) +} + +// PopEarliestTime removes the earliest time from recentConnectionTimes in +// addressData for addr +// It is expected to be later than ConnectionsRateLimitingWindow +func (ps *PeerStore) popNElements(n int, peerID peer.ID) { + data, _ := ps.Get(peerID, addressDataKey) + ad := data.(addressData) + ad.recentConnectionTimes = ad.recentConnectionTimes[n:] + _ = ps.Put(peerID, addressDataKey, ad) +} + +func (ps *PeerStore) filterRetryTime(t time.Time, role PhoneBookEntryRoles) []string { + o := make([]string, 0, len(ps.Peers())) + for _, peerID := range ps.Peers() { + data, _ := ps.Get(peerID, addressDataKey) + if data != nil { + ad := data.(addressData) + if t.After(ad.retryAfter) && role == ad.role { + o = append(o, string(peerID)) + } + } + } + return o +} + +func shuffleSelect(set []string, n int) []string { + if n >= len(set) || n == getAllAddresses { + // return shuffled copy of everything + out := slices.Clone(set) + shuffleStrings(out) + return out + } + // Pick random indexes from the set + indexSample := make([]int, n) + for i := range indexSample { + indexSample[i] = rand.Intn(len(set)-i) + i + for oi, ois := range indexSample[:i] { + if ois == indexSample[i] { + indexSample[i] = oi + } + } + } + out := make([]string, n) + for i, index := range indexSample { + out[i] = set[index] + } + return out +} + +func shuffleStrings(set []string) { + rand.Shuffle(len(set), func(i, j int) { set[i], set[j] = set[j], set[i] }) +} diff --git a/network/p2p/peerstore/peerstore_test.go b/network/p2p/peerstore/peerstore_test.go index 2debcae255..47b43c9820 100644 --- a/network/p2p/peerstore/peerstore_test.go +++ b/network/p2p/peerstore/peerstore_test.go @@ -19,15 +19,25 @@ package peerstore import ( "crypto/rand" "fmt" + "math" "testing" + "time" - "github.com/algorand/go-algorand/test/partitiontest" libp2p_crypto "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" libp2p "github.com/libp2p/go-libp2p/core/peerstore" "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) +// PhoneBookEntryRelayRole used for all the relays that are provided either via the algobootstrap SRV record +// or via a configuration file. +const PhoneBookEntryRelayRole = 1 + +// PhoneBookEntryArchiverRole used for all the archivers that are provided via the archive SRV record. +const PhoneBookEntryArchiverRole = 2 + func TestPeerstore(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -77,3 +87,342 @@ func TestPeerstore(t *testing.T) { require.Equal(t, 7, len(peers)) } + +func testPhonebookAll(t *testing.T, set []string, ph *PeerStore) { + actual := ph.GetAddresses(len(set), PhoneBookEntryRelayRole) + for _, got := range actual { + ok := false + for _, known := range set { + if got == known { + ok = true + break + } + } + if !ok { + t.Errorf("get returned junk %#v", got) + } + } + for _, known := range set { + ok := false + for _, got := range actual { + if got == known { + ok = true + break + } + } + if !ok { + t.Errorf("get missed %#v; actual=%#v; set=%#v", known, actual, set) + } + } +} + +func testPhonebookUniform(t *testing.T, set []string, ph *PeerStore, getsize int) { + uniformityTestLength := 250000 / len(set) + expected := (uniformityTestLength * getsize) / len(set) + counts := make(map[string]int) + for i := 0; i < len(set); i++ { + counts[set[i]] = 0 + } + for i := 0; i < uniformityTestLength; i++ { + actual := ph.GetAddresses(getsize, PhoneBookEntryRelayRole) + for _, xa := range actual { + if _, ok := counts[xa]; ok { + counts[xa]++ + } + } + } + min, max := math.MaxInt, 0 + for _, count := range counts { + if count > max { + max = count + } + if count < min { + min = count + } + } + // TODO: what's a good probability-theoretic threshold for good enough? + if max-min > (expected / 5) { + t.Errorf("counts %#v", counts) + } +} + +func TestArrayPhonebookAll(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + for _, addr := range set { + entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) + info, _ := PeerInfoFromDomainPort(addr) + ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + ph.Put(info.ID, addressDataKey, entry) + } + testPhonebookAll(t, set, ph) +} + +func TestArrayPhonebookUniform1(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + for _, addr := range set { + entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) + info, _ := PeerInfoFromDomainPort(addr) + ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + ph.Put(info.ID, addressDataKey, entry) + } + testPhonebookUniform(t, set, ph, 1) +} + +func TestArrayPhonebookUniform3(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + for _, addr := range set { + entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) + info, _ := PeerInfoFromDomainPort(addr) + ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + ph.Put(info.ID, addressDataKey, entry) + } + testPhonebookUniform(t, set, ph, 3) +} + +func TestMultiPhonebook(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + pha := make([]string, 0) + for _, e := range set[:5] { + pha = append(pha, e) + } + phb := make([]string, 0) + for _, e := range set[5:] { + phb = append(phb, e) + } + + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) + ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) + + testPhonebookAll(t, set, ph) + testPhonebookUniform(t, set, ph, 1) + testPhonebookUniform(t, set, ph, 3) +} + +// TestMultiPhonebookPersistentPeers validates that the peers added via Phonebook.AddPersistentPeers +// are not replaced when Phonebook.ReplacePeerList is called +func TestMultiPhonebookPersistentPeers(t *testing.T) { + partitiontest.PartitionTest(t) + + persistentPeers := []string{"a:4041"} + set := []string{"b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + pha := make([]string, 0) + for _, e := range set[:5] { + pha = append(pha, e) + } + phb := make([]string, 0) + for _, e := range set[5:] { + phb = append(phb, e) + } + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + ph.AddPersistentPeers(persistentPeers, "pha", PhoneBookEntryRelayRole) + ph.AddPersistentPeers(persistentPeers, "phb", PhoneBookEntryRelayRole) + ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) + ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) + + testPhonebookAll(t, append(set, persistentPeers...), ph) + allAddresses := ph.GetAddresses(len(set)+len(persistentPeers), PhoneBookEntryRelayRole) + for _, pp := range persistentPeers { + require.Contains(t, allAddresses, pp) + } +} + +func TestMultiPhonebookDuplicateFiltering(t *testing.T) { + partitiontest.PartitionTest(t) + + set := []string{"b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + pha := make([]string, 0) + for _, e := range set[:7] { + pha = append(pha, e) + } + phb := make([]string, 0) + for _, e := range set[3:] { + phb = append(phb, e) + } + ph, err := MakePhonebook(1, 1*time.Millisecond) + require.NoError(t, err) + ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) + ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) + + testPhonebookAll(t, set, ph) + testPhonebookUniform(t, set, ph, 1) + testPhonebookUniform(t, set, ph, 3) +} + +func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { + partitiontest.PartitionTest(t) + + // make the connectionsRateLimitingWindow long enough to avoid triggering it when the + // test is running in a slow environment + // The test will artificially simulate time passing + timeUnit := 2000 * time.Second + connectionsRateLimitingWindow := 2 * timeUnit + entries, err := MakePhonebook(3, connectionsRateLimitingWindow) + require.NoError(t, err) + addr1 := "addrABC:4040" + addr2 := "addrXYZ:4041" + info1, _ := PeerInfoFromDomainPort(addr1) + info2, _ := PeerInfoFromDomainPort(addr2) + + // Address not in. Should return false + addrInPhonebook, _, provisionalTime := entries.GetConnectionWaitTime(addr1) + require.Equal(t, false, addrInPhonebook) + require.Equal(t, false, entries.UpdateConnectionTime(addr1, provisionalTime)) + + // Test the addresses are populated in the phonebook and a + // time can be added to one of them + entries.ReplacePeerList([]string{addr1, addr2}, "default", PhoneBookEntryRelayRole) + addrInPhonebook, waitTime, provisionalTime := entries.GetConnectionWaitTime(addr1) + require.Equal(t, true, addrInPhonebook) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + data, _ := entries.Get(info1.ID, addressDataKey) + require.NotNil(t, data) + ad := data.(addressData) + phBookData := ad.recentConnectionTimes + require.Equal(t, 1, len(phBookData)) + + // simulate passing a unit of time + for rct := range phBookData { + phBookData[rct] = phBookData[rct].Add(-1 * timeUnit) + } + + // add another value to addr + addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr1) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + data, _ = entries.Get(info1.ID, addressDataKey) + ad = data.(addressData) + phBookData = ad.recentConnectionTimes + require.Equal(t, 2, len(phBookData)) + + // simulate passing a unit of time + for rct := range phBookData { + phBookData[rct] = phBookData[rct].Add(-1 * timeUnit) + } + + // the first time should be removed and a new one added + // there should not be any wait + addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr1) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + data, _ = entries.Get(info1.ID, addressDataKey) + ad = data.(addressData) + phBookData2 := ad.recentConnectionTimes + require.Equal(t, 2, len(phBookData2)) + + // make sure the right time was removed + require.Equal(t, phBookData[1], phBookData2[0]) + require.Equal(t, true, phBookData2[0].Before(phBookData2[1])) + + // try requesting from another address, make sure + // a separate array is used for these new requests + + // add 3 values to another address. should not wait + // value 1 + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + + // introduce a gap between the two requests so that only the first will be removed later when waited + // simulate passing a unit of time + data2, _ := entries.Get(info2.ID, addressDataKey) + require.NotNil(t, data2) + ad2 := data2.(addressData) + for rct := range ad2.recentConnectionTimes { + ad2.recentConnectionTimes[rct] = ad2.recentConnectionTimes[rct].Add(-1 * timeUnit) + } + + // value 2 + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + // value 3 + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + + data2, _ = entries.Get(info2.ID, addressDataKey) + ad2 = data2.(addressData) + phBookData = ad2.recentConnectionTimes + // all three times should be queued + require.Equal(t, 3, len(phBookData)) + + // add another element to trigger wait + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Greater(t, int64(waitTime), int64(0)) + // no element should be removed + data2, _ = entries.Get(info2.ID, addressDataKey) + ad2 = data2.(addressData) + phBookData2 = ad2.recentConnectionTimes + require.Equal(t, phBookData[0], phBookData2[0]) + require.Equal(t, phBookData[1], phBookData2[1]) + require.Equal(t, phBookData[2], phBookData2[2]) + // simulate passing of the waitTime duration + for rct := range ad2.recentConnectionTimes { + ad2.recentConnectionTimes[rct] = ad2.recentConnectionTimes[rct].Add(-1 * waitTime) + } + + // The wait should be sufficient + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + require.Equal(t, time.Duration(0), waitTime) + require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + // only one element should be removed, and one added + data2, _ = entries.Get(info2.ID, addressDataKey) + ad2 = data2.(addressData) + phBookData2 = ad2.recentConnectionTimes + require.Equal(t, 3, len(phBookData2)) + + // make sure the right time was removed + require.Equal(t, phBookData[1], phBookData2[0]) + require.Equal(t, phBookData[2], phBookData2[1]) +} + +// TestPhonebookRoles tests that the filtering by roles for different +// phonebooks entries works as expected. +func TestPhonebookRoles(t *testing.T) { + partitiontest.PartitionTest(t) + + relaysSet := []string{"relay1:4040", "relay2:4041", "relay3:4042"} + archiverSet := []string{"archiver1:1111", "archiver2:1112", "archiver3:1113"} + + ph, err := MakePhonebook(1, 1) + require.NoError(t, err) + ph.ReplacePeerList(relaysSet, "default", PhoneBookEntryRelayRole) + ph.ReplacePeerList(archiverSet, "default", PhoneBookEntryArchiverRole) + require.Equal(t, len(relaysSet)+len(archiverSet), len(ph.Peers())) + require.Equal(t, len(relaysSet)+len(archiverSet), ph.Length()) + + for _, role := range []PhoneBookEntryRoles{PhoneBookEntryRelayRole, PhoneBookEntryArchiverRole} { + for k := 0; k < 100; k++ { + for l := 0; l < 3; l++ { + entries := ph.GetAddresses(l, role) + if role == PhoneBookEntryRelayRole { + for _, entry := range entries { + require.Contains(t, entry, "relay") + } + } else if role == PhoneBookEntryArchiverRole { + for _, entry := range entries { + require.Contains(t, entry, "archiver") + } + } + } + } + } +} diff --git a/network/p2p/peerstore/utils.go b/network/p2p/peerstore/utils.go index eabcccbdae..b96fc1c8e0 100644 --- a/network/p2p/peerstore/utils.go +++ b/network/p2p/peerstore/utils.go @@ -17,6 +17,9 @@ package peerstore import ( + "fmt" + "strings" + "github.com/libp2p/go-libp2p/core/peer" "github.com/multiformats/go-multiaddr" ) @@ -49,3 +52,18 @@ func PeerInfoFromAddr(addr string) (*peer.AddrInfo, error) { } return info, nil } + +// PeerInfoFromDomainPort converts a string of the form domain:port to AddrInfo +func PeerInfoFromDomainPort(domainPort string) (*peer.AddrInfo, error) { + parts := strings.Split(domainPort, ":") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return nil, fmt.Errorf("invalid domain port string %s, found %d colon-separated parts", domainPort, len(parts)) + } + maddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/dns4/%s/tcp/%s", parts[0], parts[1])) + if err != nil { + return nil, err + } + // These will never have peer IDs + transport, _ := peer.SplitAddr(maddr) + return &peer.AddrInfo{ID: peer.ID(domainPort), Addrs: []multiaddr.Multiaddr{transport}}, nil +} diff --git a/network/p2p/peerstore/utils_test.go b/network/p2p/peerstore/utils_test.go index c8927d27ee..39124a75e4 100644 --- a/network/p2p/peerstore/utils_test.go +++ b/network/p2p/peerstore/utils_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 TestPeerInfoFromAddr(t *testing.T) { diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index b689b1d4f0..f36b0d3280 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -35,6 +35,7 @@ import ( pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" + manet "github.com/multiformats/go-multiaddr/net" ) // P2PNetwork implements the GossipNode interface @@ -202,19 +203,29 @@ func (n *P2PNetwork) GetGenesisID() string { // Address returns a string and whether that is a 'final' address or guessed. func (n *P2PNetwork) Address() (string, bool) { addrInfo := n.service.AddrInfo() + if len(addrInfo.Addrs) == 0 { + return "", false + } addrs, err := peer.AddrInfoToP2pAddrs(&addrInfo) if err != nil { n.log.Warnf("Failed to generate valid multiaddr: %v", err) return "", false } - if len(addrs) == 0 { - return "", false + // loop through and see if we have a non loopback address available + for _, addr := range addrs { + if !manet.IsIPLoopback(addr) && !manet.IsIPUnspecified(addr) { + return addr.String(), true + + } } - if len(addrs) > 1 { - n.log.Infof("Multiple addresses found, using first one from %v", addrs) + // We don't have a non loopback address, so just return the first one if it contains an ip4 address or port + addr := addrs[0].String() + if strings.Contains(addr, "/ip4/") && strings.Contains(addr, "/tcp/") { + return addr, true + } + return "", false - return addrs[0].String(), true } // Broadcast sends a message. diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 86c6de1c40..585d9aaa17 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -28,7 +28,12 @@ import ( "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" peerstore "github.com/libp2p/go-libp2p/core/peer" + ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" ) @@ -177,3 +182,125 @@ func TestP2PSubmitWS(t *testing.T) { 50*time.Millisecond, ) } + +type mockService struct { + id peer.ID + addrs []ma.Multiaddr + peers map[peer.ID]peer.AddrInfo +} + +func (s *mockService) Close() error { + return nil +} + +func (s *mockService) ID() peer.ID { + return s.id +} + +func (s *mockService) AddrInfo() peer.AddrInfo { + return peer.AddrInfo{ + ID: s.id, + Addrs: s.addrs, + } +} + +func (s *mockService) DialNode(ctx context.Context, peer *peer.AddrInfo) error { + s.peers[peer.ID] = *peer + return nil +} + +func (s *mockService) DialPeersUntilTargetCount(targetConnCount int) { +} + +func (s *mockService) ClosePeer(peer peer.ID) error { + if _, ok := s.peers[peer]; ok { + delete(s.peers, peer) + } + return nil +} + +func (s *mockService) Conns() []network.Conn { + return nil +} + +func (s *mockService) ListPeersForTopic(topic string) []peer.ID { + return nil +} + +func (s *mockService) Subscribe(topic string, val pubsub.ValidatorEx) (*pubsub.Subscription, error) { + return nil, nil +} +func (s *mockService) Publish(ctx context.Context, topic string, data []byte) error { + return nil +} + +func (s *mockService) setAddrs(addrs []ma.Multiaddr) { + s.addrs = addrs +} + +func makeMockService(id peer.ID, addrs []ma.Multiaddr) *mockService { + return &mockService{ + id: id, + addrs: addrs, + } +} + +func TestP2PNetworkAddress(t *testing.T) { + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + log := logging.TestingLog(t) + netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet) + defer netA.Stop() + require.NoError(t, err) + addrInfo := netA.service.AddrInfo() + // close the real service since we will substitute a mock one + netA.service.Close() + + // define some multiaddrs we will use in the test + loopbackAddr, err := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/1234") + require.NoError(t, err) + unspecifiedAddr, err := ma.NewMultiaddr("/ip4/0.0.0.0/tcp/0") + require.NoError(t, err) + publicAddr, err := ma.NewMultiaddr("/ip4/12.86.192.5/tcp/5678") + require.NoError(t, err) + publicAddr2, err := ma.NewMultiaddr("/ip4/23.97.191.6/tcp/1564") + require.NoError(t, err) + + // first two are invalid so third one should be returned as the first public address + addrsA := []ma.Multiaddr{ + loopbackAddr, + unspecifiedAddr, + publicAddr, + publicAddr2, + } + mockService := makeMockService(addrInfo.ID, addrsA) + netA.service = mockService + + retAddr, ok := netA.Address() + require.True(t, ok) + // using Contains since the return of Address also includes the public peerID + require.Contains(t, retAddr, publicAddr.String()) + + // don't have a public address so return the first one + addrsB := []ma.Multiaddr{ + loopbackAddr, + unspecifiedAddr, + } + mockService.addrs = addrsB + retAddr, ok = netA.Address() + require.True(t, ok) + require.Contains(t, retAddr, loopbackAddr.String()) + + // confirm that we don't return an address if none is supplied + mockService.addrs = nil + retAddr, ok = netA.Address() + require.False(t, ok) + require.Empty(t, retAddr) + + mockService.addrs = addrsA // these are still valid addresses + mockService.id = "invalid peer ID" // this won't parse and encode properly + retAddr, ok = netA.Address() + require.False(t, ok) + require.Empty(t, retAddr) +} diff --git a/network/phonebook.go b/network/phonebook.go index 0ad7be1a0c..cf189eb6a4 100644 --- a/network/phonebook.go +++ b/network/phonebook.go @@ -25,7 +25,7 @@ import ( "golang.org/x/exp/slices" ) -// when using GetAddresses with getAllAddresses, all the addresses will be retrieved, regardless +// getAllAddresses when using GetAddresses with getAllAddresses, all the addresses will be retrieved, regardless // of how many addresses the phonebook actually has. ( with the retry-after logic applied ) const getAllAddresses = math.MaxInt32 diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index ba8c870ef1..31a4144dc0 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -4075,82 +4075,95 @@ func TestRefreshRelayArchivePhonebookAddresses(t *testing.T) { var netA *WebsocketNetwork var refreshRelayDNSBootstrapID = ".algorand.network?backup=.algorand.net&dedup=.algorand-.(network|net)" - rapid.Check(t, func(t1 *rapid.T) { - refreshTestConf := defaultConfig - refreshTestConf.DNSBootstrapID = refreshRelayDNSBootstrapID - netA = makeTestWebsocketNodeWithConfig(t, refreshTestConf) - netA.NetworkID = nonHardcodedNetworkIDGen().Draw(t1, "network") - - primarySRVBootstrap := strings.Replace(".algorand.network", "", string(netA.NetworkID), -1) - backupSRVBootstrap := strings.Replace(".algorand.net", "", string(netA.NetworkID), -1) - var primaryRelayResolvedRecords []string - var secondaryRelayResolvedRecords []string - var primaryArchiveResolvedRecords []string - var secondaryArchiveResolvedRecords []string - - for _, record := range []string{"r1.algorand-.network", - "r2.algorand-.network", "r3.algorand-.network"} { - var recordSub = strings.Replace(record, "", string(netA.NetworkID), -1) - primaryRelayResolvedRecords = append(primaryRelayResolvedRecords, recordSub) - secondaryRelayResolvedRecords = append(secondaryRelayResolvedRecords, strings.Replace(recordSub, "network", "net", -1)) - } - - for _, record := range []string{"r1archive.algorand-.network", - "r2archive.algorand-.network", "r3archive.algorand-.network"} { - var recordSub = strings.Replace(record, "", string(netA.NetworkID), -1) - primaryArchiveResolvedRecords = append(primaryArchiveResolvedRecords, recordSub) - secondaryArchiveResolvedRecords = append(secondaryArchiveResolvedRecords, strings.Replace(recordSub, "network", "net", -1)) - } + testRefreshWithConfig := func(refreshTestConf config.Local) { + rapid.Check(t, func(t1 *rapid.T) { + refreshTestConf.DNSBootstrapID = refreshRelayDNSBootstrapID + netA = makeTestWebsocketNodeWithConfig(t, refreshTestConf) + netA.NetworkID = nonHardcodedNetworkIDGen().Draw(t1, "network") + + primarySRVBootstrap := strings.Replace(".algorand.network", "", string(netA.NetworkID), -1) + backupSRVBootstrap := strings.Replace(".algorand.net", "", string(netA.NetworkID), -1) + var primaryRelayResolvedRecords []string + var secondaryRelayResolvedRecords []string + var primaryArchiveResolvedRecords []string + var secondaryArchiveResolvedRecords []string + + for _, record := range []string{"r1.algorand-.network", + "r2.algorand-.network", "r3.algorand-.network"} { + var recordSub = strings.Replace(record, "", string(netA.NetworkID), -1) + primaryRelayResolvedRecords = append(primaryRelayResolvedRecords, recordSub) + secondaryRelayResolvedRecords = append(secondaryRelayResolvedRecords, strings.Replace(recordSub, "network", "net", -1)) + } - // Mock the SRV record lookup - netA.resolveSRVRecords = func(service string, protocol string, name string, fallbackDNSResolverAddress string, - secure bool) (addrs []string, err error) { - if service == "algobootstrap" && protocol == "tcp" && name == primarySRVBootstrap { - return primaryRelayResolvedRecords, nil - } else if service == "algobootstrap" && protocol == "tcp" && name == backupSRVBootstrap { - return secondaryRelayResolvedRecords, nil + for _, record := range []string{"r1archive.algorand-.network", + "r2archive.algorand-.network", "r3archive.algorand-.network"} { + var recordSub = strings.Replace(record, "", string(netA.NetworkID), -1) + primaryArchiveResolvedRecords = append(primaryArchiveResolvedRecords, recordSub) + secondaryArchiveResolvedRecords = append(secondaryArchiveResolvedRecords, strings.Replace(recordSub, "network", "net", -1)) } - if service == "archive" && protocol == "tcp" && name == primarySRVBootstrap { - return primaryArchiveResolvedRecords, nil - } else if service == "archive" && protocol == "tcp" && name == backupSRVBootstrap { - return secondaryArchiveResolvedRecords, nil + // Mock the SRV record lookup + netA.resolveSRVRecords = func(service string, protocol string, name string, fallbackDNSResolverAddress string, + secure bool) (addrs []string, err error) { + if service == "algobootstrap" && protocol == "tcp" && name == primarySRVBootstrap { + return primaryRelayResolvedRecords, nil + } else if service == "algobootstrap" && protocol == "tcp" && name == backupSRVBootstrap { + return secondaryRelayResolvedRecords, nil + } + + if service == "archive" && protocol == "tcp" && name == primarySRVBootstrap { + return primaryArchiveResolvedRecords, nil + } else if service == "archive" && protocol == "tcp" && name == backupSRVBootstrap { + return secondaryArchiveResolvedRecords, nil + } + + return } - return - } + relayPeers := netA.GetPeers(PeersPhonebookRelays) + assert.Equal(t, 0, len(relayPeers)) - relayPeers := netA.GetPeers(PeersPhonebookRelays) - assert.Equal(t, 0, len(relayPeers)) + archivePeers := netA.GetPeers(PeersPhonebookArchivers) + assert.Equal(t, 0, len(archivePeers)) - archivePeers := netA.GetPeers(PeersPhonebookArchivers) - assert.Equal(t, 0, len(archivePeers)) + netA.refreshRelayArchivePhonebookAddresses() - netA.refreshRelayArchivePhonebookAddresses() + relayPeers = netA.GetPeers(PeersPhonebookRelays) - relayPeers = netA.GetPeers(PeersPhonebookRelays) + assert.Equal(t, 3, len(relayPeers)) + relayAddrs := make([]string, 0, len(relayPeers)) + for _, peer := range relayPeers { + relayAddrs = append(relayAddrs, peer.(HTTPPeer).GetAddress()) + } - assert.Equal(t, 3, len(relayPeers)) - relayAddrs := make([]string, 0, len(relayPeers)) - for _, peer := range relayPeers { - relayAddrs = append(relayAddrs, peer.(HTTPPeer).GetAddress()) - } + assert.ElementsMatch(t, primaryRelayResolvedRecords, relayAddrs) - assert.ElementsMatch(t, primaryRelayResolvedRecords, relayAddrs) + archivePeers = netA.GetPeers(PeersPhonebookArchivers) - archivePeers = netA.GetPeers(PeersPhonebookArchivers) + if refreshTestConf.EnableBlockServiceFallbackToArchiver { + // For the time being, we do not dedup resolved archive nodes + assert.Equal(t, len(primaryArchiveResolvedRecords)+len(secondaryArchiveResolvedRecords), len(archivePeers)) - // For the time being, we do not dedup resolved archive nodes - assert.Equal(t, 6, len(archivePeers)) + archiveAddrs := make([]string, 0, len(archivePeers)) + for _, peer := range archivePeers { + archiveAddrs = append(archiveAddrs, peer.(HTTPPeer).GetAddress()) + } - archiveAddrs := make([]string, 0, len(archivePeers)) - for _, peer := range archivePeers { - archiveAddrs = append(archiveAddrs, peer.(HTTPPeer).GetAddress()) - } + assert.ElementsMatch(t, append(primaryArchiveResolvedRecords, secondaryArchiveResolvedRecords...), archiveAddrs) - assert.ElementsMatch(t, append(primaryArchiveResolvedRecords, secondaryArchiveResolvedRecords...), archiveAddrs) + } else { + assert.Equal(t, 0, len(archivePeers)) + } - }) + }) + } + + testRefreshWithConfig(defaultConfig) + + configWithBlockServiceFallbackToArchiverEnabled := config.GetDefaultLocal() + configWithBlockServiceFallbackToArchiverEnabled.EnableBlockServiceFallbackToArchiver = true + + testRefreshWithConfig(configWithBlockServiceFallbackToArchiverEnabled) } /* diff --git a/node/node.go b/node/node.go index 4c18ad1d51..b637eda32e 100644 --- a/node/node.go +++ b/node/node.go @@ -28,6 +28,10 @@ import ( "sync" "time" + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/agreement/gossip" "github.com/algorand/go-algorand/catchup" @@ -47,6 +51,7 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/network/messagetracer" + "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/go-algorand/stateproof" @@ -54,7 +59,6 @@ import ( "github.com/algorand/go-algorand/util/execpool" "github.com/algorand/go-algorand/util/metrics" "github.com/algorand/go-algorand/util/timers" - "github.com/algorand/go-deadlock" ) const ( @@ -152,6 +156,8 @@ type AlgorandFullNode struct { tracer messagetracer.MessageTracer stateProofWorker *stateproof.Worker + + capabilitiesDiscovery *p2p.CapabilitiesDiscovery } // TxnWithStatus represents information about a single transaction, @@ -194,6 +200,15 @@ func MakeFull(log logging.Logger, rootDir string, cfg config.Local, phonebookAdd return nil, err } + if cfg.EnableDHTProviders { + caps, err0 := p2p.MakeCapabilitiesDiscovery(node.ctx, node.config, node.genesisDirs.RootGenesisDir, string(genesis.Network), node.log, []*peer.AddrInfo{}) + if err0 != nil { + log.Errorf("Failed to create dht node capabilities discovery: %v", err) + return nil, err + } + node.capabilitiesDiscovery = caps + } + // tie network, block fetcher, and agreement services together var p2pNode network.GossipNode if cfg.EnableP2P { @@ -372,9 +387,23 @@ func (node *AlgorandFullNode) Start() { node.startMonitoringRoutines() } + if node.capabilitiesDiscovery != nil { + node.capabilitiesDiscovery.AdvertiseCapabilities(node.capabilities()...) + } } +func (node *AlgorandFullNode) capabilities() []p2p.Capability { + var caps []p2p.Capability + if node.IsArchival() { + caps = append(caps, p2p.Archival) + } + if node.config.StoresCatchpoints() { + caps = append(caps, p2p.Catchpoints) + } + return caps +} + // startMonitoringRoutines starts the internal monitoring routines used by the node. func (node *AlgorandFullNode) startMonitoringRoutines() { node.monitoringRoutinesWaitGroup.Add(2) @@ -430,6 +459,9 @@ func (node *AlgorandFullNode) Stop() { node.lowPriorityCryptoVerificationPool.Shutdown() node.cryptoPool.Shutdown() node.cancelCtx() + if node.capabilitiesDiscovery != nil { + node.capabilitiesDiscovery.Close() + } } // note: unlike the other two functions, this accepts a whole filename diff --git a/rpcs/blockService_test.go b/rpcs/blockService_test.go index 98d86ae36d..a0ba919c98 100644 --- a/rpcs/blockService_test.go +++ b/rpcs/blockService_test.go @@ -143,6 +143,9 @@ func TestRedirectFallbackArchiver(t *testing.T) { net2 := &httpTestPeerSource{} config := config.GetDefaultLocal() + // Need to enable block service fallbacks + config.EnableBlockServiceFallbackToArchiver = true + bs1 := MakeBlockService(log, config, ledger1, net1, "test-genesis-ID") bs2 := MakeBlockService(log, config, ledger2, net2, "test-genesis-ID") @@ -311,6 +314,8 @@ func TestRedirectOnFullCapacity(t *testing.T) { net2 := &httpTestPeerSource{} config := config.GetDefaultLocal() + // Need to enable block service fallbacks + config.EnableBlockServiceFallbackToArchiver = true bs1 := MakeBlockService(log1, config, ledger1, net1, "test-genesis-ID") bs2 := MakeBlockService(log2, config, ledger2, net2, "test-genesis-ID") // set the memory cap so that it can serve only 1 block at a time @@ -487,6 +492,9 @@ func TestRedirectExceptions(t *testing.T) { net1 := &httpTestPeerSource{} config := config.GetDefaultLocal() + // Need to enable block service fallbacks + config.EnableBlockServiceFallbackToArchiver = true + bs1 := MakeBlockService(log, config, ledger1, net1, "{genesisID}") nodeA := &basicRPCNode{} diff --git a/scripts/configure_dev.sh b/scripts/configure_dev.sh index d40b551474..c7bd93a250 100755 --- a/scripts/configure_dev.sh +++ b/scripts/configure_dev.sh @@ -13,24 +13,30 @@ Options: FORCE=false while getopts ":sfh" opt; do - case ${opt} in - f ) FORCE=true - ;; - h ) echo "${HELP}" + case ${opt} in + f) + FORCE=true + ;; + h) + echo "${HELP}" exit 0 - ;; - \? ) echo "${HELP}" + ;; + \?) + echo "${HELP}" exit 2 - ;; - esac + ;; + esac done -SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" +SCRIPTPATH="$( + cd "$(dirname "$0")" + pwd -P +)" OS=$("$SCRIPTPATH"/ostype.sh) function install_or_upgrade { - if ${FORCE} ; then + if ${FORCE}; then BREW_FORCE="-f" fi if brew ls --versions "$1" >/dev/null; then @@ -43,30 +49,30 @@ function install_or_upgrade { function install_windows_shellcheck() { version="v0.7.1" if ! wget https://github.com/koalaman/shellcheck/releases/download/$version/shellcheck-$version.zip -O /tmp/shellcheck-$version.zip; then - rm /tmp/shellcheck-$version.zip &> /dev/null + rm /tmp/shellcheck-$version.zip &>/dev/null echo "Error downloading shellcheck $version" return 1 fi if ! unzip -o /tmp/shellcheck-$version.zip shellcheck-$version.exe -d /tmp; then - rm /tmp/shellcheck-$version.zip &> /dev/null + rm /tmp/shellcheck-$version.zip &>/dev/null echo "Unable to decompress shellcheck $version" return 1 fi if ! mv -f /tmp/shellcheck-$version.exe /usr/bin/shellcheck.exe; then - rm /tmp/shellcheck-$version.zip &> /dev/null + rm /tmp/shellcheck-$version.zip &>/dev/null echo "Unable to move shellcheck to /usr/bin" return 1 fi - rm /tmp/shellcheck-$version.zip &> /dev/null + rm /tmp/shellcheck-$version.zip &>/dev/null return 0 } if [ "${OS}" = "linux" ]; then - if ! which sudo > /dev/null; then + if ! which sudo >/dev/null; then "$SCRIPTPATH/install_linux_deps.sh" else sudo "$SCRIPTPATH/install_linux_deps.sh" @@ -74,7 +80,13 @@ if [ "${OS}" = "linux" ]; then elif [ "${OS}" = "darwin" ]; then if [ "${CIRCLECI}" != "true" ]; then brew update - brew tap homebrew/cask + brew_version=$(brew --version | head -1 | cut -d' ' -f2) + major_version=$(echo $brew_version | cut -d. -f1) + minor_version=$(echo $brew_version | cut -d. -f2) + version_decimal="$major_version.$minor_version" + if (($(echo "$version_decimal < 2.5" | bc -l))); then + brew tap homebrew/cask + fi fi install_or_upgrade pkg-config install_or_upgrade libtool diff --git a/test/e2e-go/features/privatenet/privatenet_test.go b/test/e2e-go/features/privatenet/privatenet_test.go new file mode 100644 index 0000000000..312abed618 --- /dev/null +++ b/test/e2e-go/features/privatenet/privatenet_test.go @@ -0,0 +1,62 @@ +// Copyright (C) 2019-2023 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 . + +// Check that private networks are started as designed. +package privatenet + +import ( + "testing" + + "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 +// imported when starting a private network. +func TestPrivateNetworkImportKeys(t *testing.T) { + partitiontest.PartitionTest(t) + + // This test takes 5~10 seconds. + if testing.Short() { + t.Skip() + } + + // First test that keys can be exported by using `goal network pregen ...` + // Don't start up network, just create genesis files. + var goalFixture fixtures.GoalFixture + tmpGenDir := t.TempDir() + tmpNetDir := t.TempDir() + defaultTemplate := "" // Use the default template by omitting the filepath. + + _, err := goalFixture.NetworkPregen(defaultTemplate, tmpGenDir) + require.NoError(t, err) + + // Check that if there is an existing directory with same name, test fails. + errStr, err := goalFixture.NetworkPregen(defaultTemplate, tmpGenDir) + require.Error(t, err) + require.Contains(t, errStr, "already exists and is not empty") + + // Then try importing files from same template. + err = goalFixture.NetworkCreate(tmpNetDir, "", defaultTemplate, tmpGenDir) + require.NoError(t, err) + + err = goalFixture.NetworkStart(tmpNetDir) + require.NoError(t, err) + + err = goalFixture.NetworkStop(tmpNetDir) + require.NoError(t, err) +} diff --git a/test/e2e-go/restAPI/helpers.go b/test/e2e-go/restAPI/helpers.go index b1067ecc93..3e85020e94 100644 --- a/test/e2e-go/restAPI/helpers.go +++ b/test/e2e-go/restAPI/helpers.go @@ -117,7 +117,7 @@ func WaitForRoundOne(t *testing.T, testClient libgoal.Client) { var errWaitForTransactionTimeout = errors.New("wait for transaction timed out") // WaitForTransaction waits for a transaction to be confirmed -func WaitForTransaction(t *testing.T, testClient libgoal.Client, fromAddress, txID string, timeout time.Duration) (tx v2.PreEncodedTxInfo, err error) { +func WaitForTransaction(t *testing.T, testClient libgoal.Client, txID string, timeout time.Duration) (tx v2.PreEncodedTxInfo, err error) { a := require.New(fixtures.SynchronizedTest(t)) rnd, err := testClient.Status() a.NoError(err) diff --git a/test/e2e-go/restAPI/other/appsRestAPI_test.go b/test/e2e-go/restAPI/other/appsRestAPI_test.go index 0fe44e3b28..8abe6a4dbc 100644 --- a/test/e2e-go/restAPI/other/appsRestAPI_test.go +++ b/test/e2e-go/restAPI/other/appsRestAPI_test.go @@ -101,7 +101,7 @@ return a.NoError(err) appCreateTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCreateTxn) a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, someAddress, appCreateTxID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, appCreateTxID, 30*time.Second) a.NoError(err) // get app ID @@ -115,7 +115,7 @@ return appFundTxn, err := testClient.SendPaymentFromWallet(wh, nil, someAddress, createdAppID.Address().String(), 0, 1_000_000, nil, "", 0, 0) a.NoError(err) appFundTxID := appFundTxn.ID() - _, err = helper.WaitForTransaction(t, testClient, someAddress, appFundTxID.String(), 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, appFundTxID.String(), 30*time.Second) a.NoError(err) // call app, which will issue an ASA create inner txn @@ -125,7 +125,7 @@ return a.NoError(err) appCallTxnTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCallTxn) a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, someAddress, appCallTxnTxID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, appCallTxnTxID, 30*time.Second) a.NoError(err) // verify pending txn info of outer txn @@ -240,7 +240,7 @@ end: a.NoError(err) appCreateTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCreateTxn) a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, someAddress, appCreateTxID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, appCreateTxID, 30*time.Second) a.NoError(err) // get app ID @@ -257,7 +257,7 @@ end: ) a.NoError(err) appFundTxID := appFundTxn.ID() - _, err = helper.WaitForTransaction(t, testClient, someAddress, appFundTxID.String(), 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, appFundTxID.String(), 30*time.Second) a.NoError(err) createdBoxName := map[string]bool{} @@ -306,7 +306,7 @@ end: err = testClient.BroadcastTransactionGroup(stxns) if len(errPrefix) == 0 { a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, someAddress, txns[0].ID().String(), 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, txns[0].ID().String(), 30*time.Second) a.NoError(err) } else { a.ErrorContains(err, errPrefix[0]) @@ -545,7 +545,7 @@ end: a.NoError(err) appDeleteTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appDeleteTxn) a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, someAddress, appDeleteTxID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, appDeleteTxID, 30*time.Second) a.NoError(err) _, err = testClient.ApplicationInformation(uint64(createdAppID)) diff --git a/test/e2e-go/restAPI/restClient_test.go b/test/e2e-go/restAPI/restClient_test.go index 3929939c8b..65292563da 100644 --- a/test/e2e-go/restAPI/restClient_test.go +++ b/test/e2e-go/restAPI/restClient_test.go @@ -285,7 +285,7 @@ func TestClientCanSendAndGetNote(t *testing.T) { note := make([]byte, maxTxnNoteBytes) tx, err := testClient.SendPaymentFromWallet(wh, nil, someAddress, toAddress, 10000, 100000, note, "", 0, 0) a.NoError(err) - txStatus, err := WaitForTransaction(t, testClient, someAddress, tx.ID().String(), 30*time.Second) + txStatus, err := WaitForTransaction(t, testClient, tx.ID().String(), 30*time.Second) a.NoError(err) a.Equal(note, txStatus.Txn.Txn.Note) } @@ -311,7 +311,7 @@ func TestClientCanGetTransactionStatus(t *testing.T) { t.Log(string(protocol.EncodeJSON(tx))) a.NoError(err) t.Log(tx.ID().String()) - _, err = WaitForTransaction(t, testClient, someAddress, tx.ID().String(), 30*time.Second) + _, err = WaitForTransaction(t, testClient, tx.ID().String(), 30*time.Second) a.NoError(err) } @@ -336,7 +336,7 @@ func TestAccountBalance(t *testing.T) { a.NoError(err) tx, err := testClient.SendPaymentFromWallet(wh, nil, someAddress, toAddress, 10000, 100000, nil, "", 0, 0) a.NoError(err) - _, err = WaitForTransaction(t, testClient, someAddress, tx.ID().String(), 30*time.Second) + _, err = WaitForTransaction(t, testClient, tx.ID().String(), 30*time.Second) a.NoError(err) account, err := testClient.AccountInformation(toAddress, false) @@ -403,7 +403,7 @@ func TestAccountParticipationInfo(t *testing.T) { } txID, err := testClient.SignAndBroadcastTransaction(wh, nil, tx) a.NoError(err) - _, err = WaitForTransaction(t, testClient, someAddress, txID, 30*time.Second) + _, err = WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) account, err := testClient.AccountInformation(someAddress, false) diff --git a/test/e2e-go/restAPI/simulate/simulateRestAPI_test.go b/test/e2e-go/restAPI/simulate/simulateRestAPI_test.go index 63c062e30c..c70767713d 100644 --- a/test/e2e-go/restAPI/simulate/simulateRestAPI_test.go +++ b/test/e2e-go/restAPI/simulate/simulateRestAPI_test.go @@ -468,7 +468,7 @@ int 1` // sign and broadcast appCreateTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCreateTxn) a.NoError(err) - submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, senderAddress, appCreateTxID, 30*time.Second) + submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, appCreateTxID, 30*time.Second) a.NoError(err) // get app ID @@ -483,7 +483,7 @@ int 1` ) a.NoError(err) appFundTxID := appFundTxn.ID() - _, err = helper.WaitForTransaction(t, testClient, senderAddress, appFundTxID.String(), 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, appFundTxID.String(), 30*time.Second) a.NoError(err) // construct app call @@ -596,7 +596,7 @@ int 1` // sign and broadcast appCreateTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCreateTxn) a.NoError(err) - submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, senderAddress, appCreateTxID, 30*time.Second) + submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, appCreateTxID, 30*time.Second) a.NoError(err) // get app ID @@ -611,7 +611,7 @@ int 1` ) a.NoError(err) appFundTxID := appFundTxn.ID() - _, err = helper.WaitForTransaction(t, testClient, senderAddress, appFundTxID.String(), 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, appFundTxID.String(), 30*time.Second) a.NoError(err) // construct app call @@ -862,7 +862,7 @@ func TestMaxDepthAppWithPCandStackTrace(t *testing.T) { appCreateTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCreateTxn) a.NoError(err) - submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, senderAddress, appCreateTxID, 30*time.Second) + submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, appCreateTxID, 30*time.Second) a.NoError(err) futureAppID := basics.AppIndex(*submittedAppCreateTxn.ApplicationIndex) @@ -1710,7 +1710,7 @@ func TestSimulateScratchSlotChange(t *testing.T) { appCreateTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCreateTxn) a.NoError(err) - submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, senderAddress, appCreateTxID, 30*time.Second) + submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, appCreateTxID, 30*time.Second) a.NoError(err) futureAppID := basics.AppIndex(*submittedAppCreateTxn.ApplicationIndex) @@ -1904,7 +1904,7 @@ end: appCreateTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCreateTxn) a.NoError(err) - submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, senderAddress, appCreateTxID, 30*time.Second) + submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, appCreateTxID, 30*time.Second) a.NoError(err) futureAppID := basics.AppIndex(*submittedAppCreateTxn.ApplicationIndex) @@ -2094,6 +2094,340 @@ end: }, *resp.TxnGroups[0].Txns[2].TransactionTrace.ApprovalProgramTrace) } +func TestSimulateExecTraceAppInitialState(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + a := require.New(fixtures.SynchronizedTest(t)) + var localFixture fixtures.RestClientFixture + localFixture.SetupNoStart(t, filepath.Join("nettemplates", "OneNodeFuture.json")) + + // Get primary node + primaryNode, err := localFixture.GetNodeController("Primary") + a.NoError(err) + + localFixture.Start() + defer primaryNode.FullStop() + + // get lib goal client + testClient := localFixture.LibGoalFixture.GetLibGoalClientFromNodeController(primaryNode) + + _, err = testClient.WaitForRound(1) + a.NoError(err) + + wh, err := testClient.GetUnencryptedWalletHandle() + a.NoError(err) + addresses, err := testClient.ListAddresses(wh) + a.NoError(err) + _, senderAddress := helper.GetMaxBalAddr(t, testClient, addresses) + a.NotEmpty(senderAddress, "no addr with funds") + + addressDigest, err := basics.UnmarshalChecksumAddress(senderAddress) + a.NoError(err) + + ops, err := logic.AssembleString( + `#pragma version 8 +txn ApplicationID +bz end // Do nothing during create + +txn OnCompletion +int OptIn +== +bnz end // Always allow optin + +byte "local" +byte "global" +txn ApplicationArgs 0 +match local global +err // Unknown command + +local: + txn Sender + byte "local-int-key" + int 0xcafeb0ba + app_local_put + int 0 + byte "local-bytes-key" + byte "xqcL" + app_local_put + b end + +global: + byte "global-int-key" + int 0xdeadbeef + app_global_put + byte "global-bytes-key" + byte "welt am draht" + app_global_put + b end + +end: + int 1`) + a.NoError(err) + approval := ops.Program + + ops, err = logic.AssembleString("#pragma version 8\nint 1") + a.NoError(err) + clearState := ops.Program + + gl := basics.StateSchema{NumByteSlice: 1, NumUint: 1} + lc := basics.StateSchema{NumByteSlice: 1, NumUint: 1} + + MinFee := config.Consensus[protocol.ConsensusFuture].MinTxnFee + MinBalance := config.Consensus[protocol.ConsensusFuture].MinBalance + + // create app and get the application ID + appCreateTxn, err := testClient.MakeUnsignedAppCreateTx( + transactions.NoOpOC, approval, clearState, gl, + lc, nil, nil, nil, nil, nil, 0) + a.NoError(err) + appCreateTxn, err = testClient.FillUnsignedTxTemplate(senderAddress, 0, 0, 0, appCreateTxn) + a.NoError(err) + + appCreateTxID, err := testClient.SignAndBroadcastTransaction(wh, nil, appCreateTxn) + a.NoError(err) + submittedAppCreateTxn, err := helper.WaitForTransaction(t, testClient, appCreateTxID, 30*time.Second) + a.NoError(err) + futureAppID := basics.AppIndex(*submittedAppCreateTxn.ApplicationIndex) + + // fund app account + _, err = testClient.ConstructPayment( + senderAddress, futureAppID.Address().String(), + 0, MinBalance*2, nil, "", [32]byte{}, 0, 0, + ) + a.NoError(err) + + // construct app call "global" + appCallGlobalTxn, err := testClient.MakeUnsignedAppNoOpTx( + uint64(futureAppID), [][]byte{[]byte("global")}, nil, nil, nil, nil, + ) + a.NoError(err) + appCallGlobalTxn, err = testClient.FillUnsignedTxTemplate(senderAddress, 0, 0, MinFee, appCallGlobalTxn) + a.NoError(err) + // construct app optin + appOptInTxn, err := testClient.MakeUnsignedAppOptInTx(uint64(futureAppID), nil, nil, nil, nil, nil) + a.NoError(err) + appOptInTxn, err = testClient.FillUnsignedTxTemplate(senderAddress, 0, 0, MinFee, appOptInTxn) + // construct app call "local" + appCallLocalTxn, err := testClient.MakeUnsignedAppNoOpTx( + uint64(futureAppID), [][]byte{[]byte("local")}, nil, nil, nil, nil, + ) + a.NoError(err) + appCallLocalTxn, err = testClient.FillUnsignedTxTemplate(senderAddress, 0, 0, MinFee, appCallLocalTxn) + a.NoError(err) + + gid, err := testClient.GroupID([]transactions.Transaction{appCallGlobalTxn, appOptInTxn, appCallLocalTxn}) + a.NoError(err) + appCallGlobalTxn.Group = gid + appOptInTxn.Group = gid + appCallLocalTxn.Group = gid + + appCallTxnGlobalSigned, err := testClient.SignTransactionWithWallet(wh, nil, appCallGlobalTxn) + a.NoError(err) + appOptInSigned, err := testClient.SignTransactionWithWallet(wh, nil, appOptInTxn) + a.NoError(err) + appCallTxnLocalSigned, err := testClient.SignTransactionWithWallet(wh, nil, appCallLocalTxn) + a.NoError(err) + + a.NoError(testClient.BroadcastTransactionGroup([]transactions.SignedTxn{ + appCallTxnGlobalSigned, + appOptInSigned, + appCallTxnLocalSigned, + })) + _, err = helper.WaitForTransaction(t, testClient, appCallTxnGlobalSigned.Txn.ID().String(), 30*time.Second) + a.NoError(err) + + // construct simulation request, with state change enabled + execTraceConfig := simulation.ExecTraceConfig{ + Enable: true, + State: true, + } + + appCallGlobalTxn.Note = []byte("note for global") + appCallGlobalTxn.Group = crypto.Digest{} + appCallLocalTxn.Note = []byte("note for local") + appCallLocalTxn.Group = crypto.Digest{} + + gid, err = testClient.GroupID([]transactions.Transaction{appCallGlobalTxn, appCallLocalTxn}) + a.NoError(err) + appCallGlobalTxn.Group = gid + appCallLocalTxn.Group = gid + + appCallTxnGlobalSigned, err = testClient.SignTransactionWithWallet(wh, nil, appCallGlobalTxn) + a.NoError(err) + appCallTxnLocalSigned, err = testClient.SignTransactionWithWallet(wh, nil, appCallLocalTxn) + a.NoError(err) + + simulateRequest := v2.PreEncodedSimulateRequest{ + TxnGroups: []v2.PreEncodedSimulateRequestTransactionGroup{ + {Txns: []transactions.SignedTxn{appCallTxnGlobalSigned, appCallTxnLocalSigned}}, + }, + ExecTraceConfig: execTraceConfig, + } + + // update the configuration file to enable EnableDeveloperAPI + err = primaryNode.FullStop() + a.NoError(err) + cfg, err := config.LoadConfigFromDisk(primaryNode.GetDataDir()) + a.NoError(err) + cfg.EnableDeveloperAPI = true + err = cfg.SaveToDisk(primaryNode.GetDataDir()) + require.NoError(t, err) + localFixture.Start() + + // start real simulating + resp, err := testClient.SimulateTransactions(simulateRequest) + a.NoError(err) + + // assertions + a.Len(resp.TxnGroups, 1) + a.Nil(resp.TxnGroups[0].FailureMessage) + a.Len(resp.TxnGroups[0].Txns, 2) + + a.Equal([]model.SimulationOpcodeTraceUnit{ + {Pc: 1}, + {Pc: 4}, + {Pc: 6}, + {Pc: 9}, + {Pc: 11}, + {Pc: 12}, + {Pc: 13}, + {Pc: 16}, + {Pc: 23}, + {Pc: 31}, + {Pc: 34}, + {Pc: 94}, + {Pc: 110}, + { + Pc: 116, + StateChanges: &[]model.ApplicationStateOperation{ + { + Operation: "w", + AppStateType: "g", + Key: []byte("global-int-key"), + NewValue: &model.AvmValue{ + Type: uint64(basics.TealUintType), + Uint: toPtr[uint64](0xdeadbeef), + }, + }, + }, + }, + {Pc: 117}, + {Pc: 135}, + { + Pc: 150, + StateChanges: &[]model.ApplicationStateOperation{ + { + Operation: "w", + AppStateType: "g", + Key: []byte("global-bytes-key"), + NewValue: &model.AvmValue{ + Type: uint64(basics.TealBytesType), + Bytes: toPtr([]byte("welt am draht")), + }, + }, + }, + }, + {Pc: 151}, + {Pc: 154}, + }, *resp.TxnGroups[0].Txns[0].TransactionTrace.ApprovalProgramTrace) + a.Equal([]model.SimulationOpcodeTraceUnit{ + {Pc: 1}, + {Pc: 4}, + {Pc: 6}, + {Pc: 9}, + {Pc: 11}, + {Pc: 12}, + {Pc: 13}, + {Pc: 16}, + {Pc: 23}, + {Pc: 31}, + {Pc: 34}, + {Pc: 41}, + {Pc: 43}, + {Pc: 58}, + { + Pc: 64, + StateChanges: &[]model.ApplicationStateOperation{ + { + Operation: "w", + AppStateType: "l", + Key: []byte("local-int-key"), + NewValue: &model.AvmValue{ + Type: uint64(basics.TealUintType), + Uint: toPtr[uint64](0xcafeb0ba), + }, + Account: toPtr(addressDigest.String()), + }, + }, + }, + {Pc: 65}, + {Pc: 67}, + {Pc: 84}, + { + Pc: 90, + StateChanges: &[]model.ApplicationStateOperation{ + { + Operation: "w", + AppStateType: "l", + Key: []byte("local-bytes-key"), + NewValue: &model.AvmValue{ + Type: uint64(basics.TealBytesType), + Bytes: toPtr([]byte("xqcL")), + }, + Account: toPtr(addressDigest.String()), + }, + }, + }, + {Pc: 91}, + {Pc: 154}, + }, *resp.TxnGroups[0].Txns[1].TransactionTrace.ApprovalProgramTrace) + + a.NotNil(resp.InitialStates) + a.Len(*resp.InitialStates.AppInitialStates, 1) + + a.Len((*resp.InitialStates.AppInitialStates)[0].AppGlobals.Kvs, 2) + + globalKVs := (*resp.InitialStates.AppInitialStates)[0].AppGlobals.Kvs + globalKVMap := make(map[string]model.AvmValue) + for _, kv := range globalKVs { + globalKVMap[string(kv.Key)] = kv.Value + } + expectedGlobalKVMap := map[string]model.AvmValue{ + "global-int-key": { + Type: 2, + Uint: toPtr[uint64](0xdeadbeef), + }, + "global-bytes-key": { + Type: 1, + Bytes: toPtr([]byte("welt am draht")), + }, + } + a.Equal(expectedGlobalKVMap, globalKVMap) + + a.Len(*(*resp.InitialStates.AppInitialStates)[0].AppLocals, 1) + + localKVs := (*(*resp.InitialStates.AppInitialStates)[0].AppLocals)[0] + a.NotNil(localKVs.Account) + a.Equal(senderAddress, *localKVs.Account) + + localKVMap := make(map[string]model.AvmValue) + for _, kv := range localKVs.Kvs { + localKVMap[string(kv.Key)] = kv.Value + } + expectedLocalKVMap := map[string]model.AvmValue{ + "local-int-key": { + Type: 2, + Uint: toPtr[uint64](0xcafeb0ba), + }, + "local-bytes-key": { + Type: 1, + Bytes: toPtr([]byte("xqcL")), + }, + } + a.Equal(expectedLocalKVMap, localKVMap) +} + func TestSimulateWithUnnamedResources(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -2125,7 +2459,7 @@ func TestSimulateWithUnnamedResources(t *testing.T) { ) a.NoError(err) txID := txn.ID().String() - _, err = helper.WaitForTransaction(t, testClient, senderAddress, txID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) // create asset @@ -2136,7 +2470,7 @@ func TestSimulateWithUnnamedResources(t *testing.T) { // sign and broadcast txID, err = testClient.SignAndBroadcastTransaction(wh, nil, txn) a.NoError(err) - confirmedTxn, err := helper.WaitForTransaction(t, testClient, senderAddress, txID, 30*time.Second) + confirmedTxn, err := helper.WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) // get asset ID a.NotNil(confirmedTxn.AssetIndex) @@ -2151,7 +2485,7 @@ func TestSimulateWithUnnamedResources(t *testing.T) { // sign and broadcast txID, err = testClient.SignAndBroadcastTransaction(wh, nil, txn) a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, otherAddress, txID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) // transfer asset @@ -2162,7 +2496,7 @@ func TestSimulateWithUnnamedResources(t *testing.T) { // sign and broadcast txID, err = testClient.SignAndBroadcastTransaction(wh, nil, txn) a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, senderAddress, txID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) ops, err := logic.AssembleString("#pragma version 9\n int 1") @@ -2180,7 +2514,7 @@ func TestSimulateWithUnnamedResources(t *testing.T) { // sign and broadcast txID, err = testClient.SignAndBroadcastTransaction(wh, nil, txn) a.NoError(err) - confirmedTxn, err = helper.WaitForTransaction(t, testClient, otherAddress, txID, 30*time.Second) + confirmedTxn, err = helper.WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) // get app ID a.NotNil(confirmedTxn.ApplicationIndex) @@ -2258,7 +2592,7 @@ int 1 // sign and broadcast txID, err = testClient.SignAndBroadcastTransaction(wh, nil, txn) a.NoError(err) - confirmedTxn, err = helper.WaitForTransaction(t, testClient, senderAddress, txID, 30*time.Second) + confirmedTxn, err = helper.WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) // get app ID a.NotNil(confirmedTxn.ApplicationIndex) @@ -2272,7 +2606,7 @@ int 1 ) a.NoError(err) txID = txn.ID().String() - _, err = helper.WaitForTransaction(t, testClient, senderAddress, txID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) // construct app call diff --git a/test/e2e-go/restAPI/stateproof/stateproofRestAPI_test.go b/test/e2e-go/restAPI/stateproof/stateproofRestAPI_test.go index 17427ff4f6..497d128643 100644 --- a/test/e2e-go/restAPI/stateproof/stateproofRestAPI_test.go +++ b/test/e2e-go/restAPI/stateproof/stateproofRestAPI_test.go @@ -100,7 +100,7 @@ func TestStateProofInParticipationInfo(t *testing.T) { } txID, err := testClient.SignAndBroadcastTransaction(wh, nil, tx) a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, someAddress, txID, 120*time.Second) + _, err = helper.WaitForTransaction(t, testClient, txID, 120*time.Second) a.NoError(err) account, err := testClient.AccountInformation(someAddress, false) @@ -197,7 +197,7 @@ func TestNilStateProofInParticipationInfo(t *testing.T) { } txID, err := testClient.SignAndBroadcastTransaction(wh, nil, tx) a.NoError(err) - _, err = helper.WaitForTransaction(t, testClient, someAddress, txID, 30*time.Second) + _, err = helper.WaitForTransaction(t, testClient, txID, 30*time.Second) a.NoError(err) account, err := testClient.AccountInformation(someAddress, false) diff --git a/test/framework/fixtures/goalFixture.go b/test/framework/fixtures/goalFixture.go index ef52b51b63..69e43e784a 100644 --- a/test/framework/fixtures/goalFixture.go +++ b/test/framework/fixtures/goalFixture.go @@ -58,19 +58,27 @@ const ( nodeCmd = "node" startCmd = "start" stopCmd = "stop" + + networkCmd = "network" + pregenCmd = "pregen" + createCmd = "create" ) -func (f *GoalFixture) executeCommand(args ...string) (retStdout string, retStderr string, err error) { +func (f *GoalFixture) executeRawCommand(args ...string) (retStdout string, retStderr string, err error) { + // Executes a command without a specified data directory cmd := filepath.Join(f.binDir, goalCmd) - // We always execute goal against the PrimaryDataDir() instance - args = append(args, "-d", f.PrimaryDataDir()) retStdout, retStderr, err = util.ExecAndCaptureOutput(cmd, args...) retStdout = strings.TrimRight(retStdout, "\n") retStderr = strings.TrimRight(retStderr, "\n") - //fmt.Printf("command: %v %v\nret: %v\n", cmd, args, ret) return } +func (f *GoalFixture) executeCommand(args ...string) (retStdout string, retStderr string, err error) { + // We always execute goal against the PrimaryDataDir() instance + args = append(args, "-d", f.PrimaryDataDir()) + return f.executeRawCommand(args...) +} + // combine the error and the output so that we could return it as a single error object. func combineExecuteError(retStdout string, retStderr string, err error) error { if err == nil { @@ -227,3 +235,63 @@ func (f *GoalFixture) AccountImportRootKey(wallet string, createDefaultUnencrypt _, _, err = f.executeCommand(args...) return } + +// NetworkPregen exposes the `goal network pregen` command +func (f *GoalFixture) NetworkPregen(template, pregendir string) (stdErr string, err error) { + args := []string{ + networkCmd, + pregenCmd, + "-p", + pregendir, + } + if template != "" { + args = append(args, "-t", template) + } + _, stdErr, err = f.executeRawCommand(args...) + return +} + +// NetworkCreate exposes the `goal network create` command +func (f *GoalFixture) NetworkCreate(networkdir, networkName, template, pregendir string) (err error) { + args := []string{ + networkCmd, + createCmd, + "-r", + networkdir, + } + if networkName != "" { + args = append(args, "-n", networkName) + } + if template != "" { + args = append(args, "-t", template) + } + if pregendir != "" { + args = append(args, "-p", pregendir) + } + _, _, err = f.executeRawCommand(args...) + return +} + +// NetworkStart exposes the `goal network start` command +func (f *GoalFixture) NetworkStart(networkdir string) (err error) { + args := []string{ + networkCmd, + startCmd, + "-r", + networkdir, + } + _, _, err = f.executeRawCommand(args...) + return +} + +// NetworkStop exposes the `goal network stop` command +func (f *GoalFixture) NetworkStop(networkdir string) (err error) { + args := []string{ + networkCmd, + stopCmd, + "-r", + networkdir, + } + _, _, err = f.executeRawCommand(args...) + return +} diff --git a/test/framework/fixtures/libgoalFixture.go b/test/framework/fixtures/libgoalFixture.go index c0527fdd62..1cc0b24fb8 100644 --- a/test/framework/fixtures/libgoalFixture.go +++ b/test/framework/fixtures/libgoalFixture.go @@ -534,7 +534,7 @@ func (f *LibGoalFixture) TransactionProof(txid string, round uint64, hashType cr return model.TransactionProofResponse{}, merklearray.SingleLeafProof{}, err } - proof, err := merklearray.ProofDataToSingleLeafProof(string(proofResp.Hashtype), proofResp.Treedepth, proofResp.Proof) + proof, err := merklearray.ProofDataToSingleLeafProof(string(proofResp.Hashtype), proofResp.Proof) if err != nil { return model.TransactionProofResponse{}, merklearray.SingleLeafProof{}, err } @@ -550,7 +550,7 @@ func (f *LibGoalFixture) LightBlockHeaderProof(round uint64) (model.LightBlockHe return model.LightBlockHeaderProofResponse{}, merklearray.SingleLeafProof{}, err } - proof, err := merklearray.ProofDataToSingleLeafProof(crypto.Sha256.String(), proofResp.Treedepth, proofResp.Proof) + proof, err := merklearray.ProofDataToSingleLeafProof(crypto.Sha256.String(), proofResp.Proof) if err != nil { return model.LightBlockHeaderProofResponse{}, merklearray.SingleLeafProof{}, err } diff --git a/test/scripts/test_private_network.sh b/test/scripts/test_private_network.sh index f1adc7f62b..72f0bd9160 100755 --- a/test/scripts/test_private_network.sh +++ b/test/scripts/test_private_network.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash + +set -euf -o pipefail + echo "######################################################################" echo " test_private_network" echo "######################################################################" -set -e # Suppress telemetry reporting for tests export ALGOTEST=1 diff --git a/test/testdata/configs/config-v30.json b/test/testdata/configs/config-v30.json index 5021c8fd40..1c71a55563 100644 --- a/test/testdata/configs/config-v30.json +++ b/test/testdata/configs/config-v30.json @@ -41,6 +41,7 @@ "EnableBlockService": false, "EnableBlockServiceFallbackToArchiver": true, "EnableCatchupFromArchiveServers": false, + "EnableDHTProviders": false, "EnableDeveloperAPI": false, "EnableExperimentalAPI": false, "EnableFollowMode": false, diff --git a/test/testdata/configs/config-v31.json b/test/testdata/configs/config-v31.json index 62cbe6427b..01192f4ecc 100644 --- a/test/testdata/configs/config-v31.json +++ b/test/testdata/configs/config-v31.json @@ -43,8 +43,9 @@ "EnableAgreementTimeMetrics": false, "EnableAssembleStats": false, "EnableBlockService": false, - "EnableBlockServiceFallbackToArchiver": true, + "EnableBlockServiceFallbackToArchiver": false, "EnableCatchupFromArchiveServers": false, + "EnableDHTProviders": false, "EnableDeveloperAPI": false, "EnableExperimentalAPI": false, "EnableFollowMode": false, diff --git a/tools/block-generator/go.mod b/tools/block-generator/go.mod index 040210731a..3c7c21d8c2 100644 --- a/tools/block-generator/go.mod +++ b/tools/block-generator/go.mod @@ -46,6 +46,8 @@ require ( github.com/elastic/gosigar v0.14.2 // indirect github.com/flynn/noise v1.0.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -57,13 +59,21 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect github.com/huin/goupnp v1.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/ipfs/boxo v0.10.0 // indirect github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-datastore v0.6.0 // indirect + github.com/ipfs/go-log v1.0.5 // indirect github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/ipld/go-ipld-prime v0.20.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/jbenet/goprocess v0.1.4 // indirect github.com/jmespath/go-jmespath v0.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.16.7 // indirect @@ -76,7 +86,10 @@ require ( github.com/libp2p/go-flow-metrics v0.1.0 // indirect github.com/libp2p/go-libp2p v0.29.1 // indirect github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.24.3 // indirect + github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect github.com/libp2p/go-libp2p-pubsub v0.9.3 // indirect + github.com/libp2p/go-libp2p-record v0.2.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-nat v0.2.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect @@ -106,14 +119,16 @@ require ( github.com/olivere/elastic v6.2.14+incompatible // indirect github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/polydawn/refmt v0.89.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-19 v0.3.3 // indirect github.com/quic-go/qtls-go1-20 v0.2.3 // indirect @@ -124,6 +139,11 @@ require ( github.com/sirupsen/logrus v1.8.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.16.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/fx v1.20.0 // indirect @@ -137,6 +157,7 @@ require ( golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect golang.org/x/tools v0.11.0 // indirect + gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/sohlich/elogrus.v3 v3.0.0-20180410122755-1fa29e2f2009 // indirect lukechampine.com/blake3 v1.2.1 // indirect diff --git a/tools/block-generator/go.sum b/tools/block-generator/go.sum index d680289e61..1f67ab139d 100644 --- a/tools/block-generator/go.sum +++ b/tools/block-generator/go.sum @@ -2,45 +2,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= @@ -49,11 +17,6 @@ github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKz github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/algorand/avm-abi v0.2.0 h1:bkjsG+BOEcxUcnGSALLosmltE0JZdg+ZisXKx0UDX2k= github.com/algorand/avm-abi v0.2.0/go.mod h1:+CgwM46dithy850bpTeHh9MC99zpn2Snirb3QTl2O/g= github.com/algorand/falcon v0.1.0 h1:xl832kfZ7hHG6B4p90DQynjfKFGbIUgUOnsRiMZXfAo= @@ -82,7 +45,6 @@ github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo= @@ -90,14 +52,9 @@ github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chrismcguire/gobberish v0.0.0-20150821175641-1d8adb509a0e h1:CHPYEbz71w8DqJ7DRIq+MXyCQsdibK08vdcQTY4ufas= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -171,6 +128,7 @@ github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwU github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= @@ -181,23 +139,16 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= @@ -213,25 +164,17 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -241,50 +184,38 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -292,9 +223,14 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -302,43 +238,49 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/ipfs/boxo v0.10.0 h1:tdDAxq8jrsbRkYoF+5Rcqyeb91hgWe2hp7iLu7ORZLY= +github.com/ipfs/boxo v0.10.0/go.mod h1:Fg+BnfxZ0RPzR0nOodzdIq3A7KgoWAOWsEIImrIQdBM= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= +github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g= +github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= +github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= @@ -354,11 +296,8 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -383,8 +322,14 @@ github.com/libp2p/go-libp2p v0.29.1 h1:yNeg6XgP8gbdc4YSrwiIt5T1TGOrVjH8dzl8h0GIO github.com/libp2p/go-libp2p v0.29.1/go.mod h1:20El+LLy3/YhdUYIvGbLnvVJN32nMdqY6KXBENRAfLY= github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= +github.com/libp2p/go-libp2p-kad-dht v0.24.3 h1:VjxtDVWaaf4UFjGBf+yl2JCiGaHx7+ctAUa9oJCR3QE= +github.com/libp2p/go-libp2p-kad-dht v0.24.3/go.mod h1:BShPzRbK6+fN3hk8a0WGAYKpb8m4k+DtchkqouGTrSg= +github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0= +github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0= github.com/libp2p/go-libp2p-pubsub v0.9.3 h1:ihcz9oIBMaCK9kcx+yHWm3mLAFBMAUsM4ux42aikDxo= github.com/libp2p/go-libp2p-pubsub v0.9.3/go.mod h1:RYA7aM9jIic5VV47WXu4GkcRxRhrdElWf8xtyli+Dzc= +github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= +github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= @@ -438,11 +383,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -471,8 +413,6 @@ github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqd github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= @@ -491,6 +431,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= @@ -500,41 +442,26 @@ github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCr github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= +github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-19 v0.3.3 h1:wznEHvJwd+2X3PqftRha0SUKmGsnb6dfArMhy9PeJVE= @@ -580,14 +507,15 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go. github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= @@ -603,20 +531,25 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= @@ -624,6 +557,10 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -632,18 +569,20 @@ github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmv github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -653,21 +592,22 @@ go.uber.org/fx v1.20.0 h1:ZMC/pnRvhsthOZh9MZjMq5U8Or3mA9zBSPaLnzs3ihQ= go.uber.org/fx v1.20.0/go.mod h1:qCUj0btiR3/JnanEr1TYEePfSw6o/4qYJscgvzQ5Ub0= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -678,36 +618,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -719,7 +639,6 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -727,48 +646,24 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -776,79 +671,43 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -859,8 +718,6 @@ golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -869,45 +726,15 @@ golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -918,83 +745,33 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM= +gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1003,13 +780,11 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1027,11 +802,9 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -1040,17 +813,11 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= pgregory.net/rapid v0.6.2 h1:ErW5sL+UKtfBfUTsWHDCoeB+eZKLKMxrSd1VJY6W4bw= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= diff --git a/util/set.go b/util/set.go new file mode 100644 index 0000000000..a23f543dd6 --- /dev/null +++ b/util/set.go @@ -0,0 +1,42 @@ +// Copyright (C) 2019-2023 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 + +// Set is a type alias for map with empty struct{}, where keys are comparable +// We don't attempt to move even forward for the generics, +// for keys being comparable should be sufficient for most cases. +// (Though we actually want compare byte slices, but seems not achievable at this moment) +type Set[T comparable] map[T]struct{} + +// Add adds variate number of elements to the set. +func (s Set[T]) Add(elems ...T) Set[T] { + for _, elem := range elems { + s[elem] = struct{}{} + } + return s +} + +// MakeSet constructs a set instance directly from elements. +func MakeSet[T comparable](elems ...T) Set[T] { + return make(Set[T]).Add(elems...) +} + +// Contains checks the membership of an element in the set. +func (s Set[T]) Contains(elem T) (exists bool) { + _, exists = s[elem] + return +} From 4349e79fde40b36e3fb3088cfc9b873e0441c4d3 Mon Sep 17 00:00:00 2001 From: chris erway Date: Thu, 2 Nov 2023 15:46:17 -0400 Subject: [PATCH 02/38] add back pregen short cmd description from bad merge --- cmd/goal/network.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/goal/network.go b/cmd/goal/network.go index ddd69a1897..a63bb13f58 100644 --- a/cmd/goal/network.go +++ b/cmd/goal/network.go @@ -281,9 +281,10 @@ var networkDeleteCmd = &cobra.Command{ } var networkPregenCmd = &cobra.Command{ - Use: "pregen", - Long: "Pregenerates the root and participation keys for a private network. The pregen directory can then be passed to the 'goal network create' to start the network more quickly.", - Args: validateNoPosArgsFn, + Use: "pregen", + Short: "Pregenerate private network", + Long: "Pregenerates the root and participation keys for a private network. The pregen directory can then be passed to the 'goal network create' to start the network more quickly.", + Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { var err error if networkRootDir != "" { From bcf71c1c628d3c8c07ad0e5a069b7b1408a4b1b7 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:14:07 -0500 Subject: [PATCH 03/38] p2p dht: more tests and minor fixes (#5827) --- daemon/algod/api/algod.oas3.yml | 3 +- .../api/server/v2/generated/data/routes.go | 4 +- .../v2/generated/experimental/routes.go | 408 ++++++++--------- .../nonparticipating/private/routes.go | 414 +++++++++--------- .../nonparticipating/public/routes.go | 3 +- .../generated/participating/private/routes.go | 2 +- .../generated/participating/public/routes.go | 3 +- network/p2p/capabilities_test.go | 157 ++++--- network/p2p/dht/dht.go | 22 +- 9 files changed, 533 insertions(+), 483 deletions(-) diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index c8c82e0d98..c4fb9394cf 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -7192,5 +7192,6 @@ { "name": "private" } - ] + ], + "x-original-swagger-version": "2.0" } \ No newline at end of file diff --git a/daemon/algod/api/server/v2/generated/data/routes.go b/daemon/algod/api/server/v2/generated/data/routes.go index 9fd389f273..1ffe766db6 100644 --- a/daemon/algod/api/server/v2/generated/data/routes.go +++ b/daemon/algod/api/server/v2/generated/data/routes.go @@ -316,8 +316,8 @@ var swaggerSpec = []string{ "j9FPwV7v6kP78f0f4n5/Qbk/z50dtzU+qCwZyIYKKB92lvgXF/hvwwVsixxq93VKNJSlCs++Fnj2rSvG", "1c3j1kU2kg90yhy2wnTn51NvQIjpkN03P3T+7KpOalnrQtwEs6Dp3fqNhlqGeVir/t+nN5TpbC6kq66H", "vbWHH2ug5alrpdH7ta1ePXiCJbmDH8M8tuivp9SpG7Fnle8+H33YV3ljT53Kl3jJB5H6x635KzQnIZ9t", - "DEnv3hsuh01zHQturSPPT08xq2AplD6dfJx+6FlOwofvG8Lyvd4mlWRrLGb+/uP/DwAA//+EcjtHRPcA", - "AA==", + "DEnv3hsuh01zHQturSPPT08xq2AplD6dfJx+6FlOwofvG8Lyvd4mlWRrLGb+fjrZZEKyBeO0zJxVou0H", + "NHly8mjy8f8HAAD//3CL32ln9wAA", } // 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 1888e71ef1..3fdcd13419 100644 --- a/daemon/algod/api/server/v2/generated/experimental/routes.go +++ b/daemon/algod/api/server/v2/generated/experimental/routes.go @@ -90,210 +90,210 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "JZpUkq0x7+7Hz/83AAD//8gL9x7v8QAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file 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 3d3c20de36..0e0d78aa42 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go +++ b/daemon/algod/api/server/v2/generated/nonparticipating/private/routes.go @@ -139,213 +139,213 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AAA=", + "H4sIAAAAAAAC/+x9/XMbN7Lgv4Livip/HEeSv7JrX229U+wkq4uTuCwle+9ZvgScaZJYDYEJgJHI+Py/", + "X6EBzGBmAHIoMXZS7/1ki4OPRqPR6C90f5jkYlUJDlyryYsPk4pKugINEv+ieS5qrjNWmL8KULlklWaC", + "T174b0RpyfhiMp0w82tF9XIynXC6graN6T+dSPi1ZhKKyQsta5hOVL6EFTUD601lWjcjrbOFyNwQp3aI", + "s1eTj1s+0KKQoNQQyh94uSGM52VdANGSckVz80mRG6aXRC+ZIq4zYZwIDkTMiV52GpM5g7JQR36Rv9Yg", + "N8Eq3eTpJX1sQcykKGEI50uxmjEOHipogGo2hGhBCphjoyXVxMxgYPUNtSAKqMyXZC7kDlAtECG8wOvV", + "5MW7iQJegMTdyoFd43/nEuA3yDSVC9CT99PY4uYaZKbZKrK0M4d9CaoutSLYFte4YNfAiel1RL6rlSYz", + "IJSTt1+/JE+ePHluFrKiWkPhiCy5qnb2cE22++TFpKAa/OchrdFyISTlRda0f/v1S5z/3C1wbCuqFMQP", + "y6n5Qs5epRbgO0ZIiHENC9yHDvWbHpFD0f48g7mQMHJPbOODbko4/2fdlZzqfFkJxnVkXwh+JfZzlIcF", + "3bfxsAaATvvKYEqaQd+dZM/ff3g0fXTy8S/vTrP/dH8+e/Jx5PJfNuPuwEC0YV5LCTzfZAsJFE/LkvIh", + "Pt46elBLUZcFWdJr3Hy6Qlbv+hLT17LOa1rWhk5YLsVpuRCKUEdGBcxpXWriJyY1Lw2bMqM5aidMkUqK", + "a1ZAMTXc92bJ8iXJqbJDYDtyw8rS0GCtoEjRWnx1Ww7TxxAlBq5b4QMX9MdFRruuHZiANXKDLC+FgkyL", + "HdeTv3EoL0h4obR3ldrvsiIXSyA4uflgL1vEHTc0XZYbonFfC0IVocRfTVPC5mQjanKDm1OyK+zvVmOw", + "tiIGabg5nXvUHN4U+gbIiCBvJkQJlCPy/LkboozP2aKWoMjNEvTS3XkSVCW4AiJm/4Jcm23/3+c/fE+E", + "JN+BUnQBb2h+RYDnooDiiJzNCRc6IA1HS4hD0zO1DgdX7JL/lxKGJlZqUdH8Kn6jl2zFIqv6jq7Zql4R", + "Xq9mIM2W+itECyJB15KnALIj7iDFFV0PJ72QNc9x/9tpO7KcoTamqpJuEGEruv77ydSBowgtS1IBLxhf", + "EL3mSTnOzL0bvEyKmhcjxBxt9jS4WFUFOZszKEgzyhZI3DS74GF8P3ha4SsAxw+SBKeZZQc4HNYRmjGn", + "23whFV1AQDJH5EfH3PCrFlfAG0Insw1+qiRcM1GrplMCRpx6uwTOhYaskjBnERo7d+gwDMa2cRx45WSg", + "XHBNGYfCMGcEWmiwzCoJUzDhdn1neIvPqIIvnqbu+PbryN2fi/6ub93xUbuNjTJ7JCNXp/nqDmxcsur0", + "H6EfhnMrtsjsz4ONZIsLc9vMWYk30b/M/nk01AqZQAcR/m5SbMGpriW8uOQPzV8kI+ea8oLKwvyysj99", + "V5eanbOF+am0P70WC5afs0UCmQ2sUYULu63sP2a8ODvW66he8VqIq7oKF5R3FNfZhpy9Sm2yHXNfwjxt", + "tN1Q8bhYe2Vk3x563WxkAsgk7ipqGl7BRoKBluZz/Gc9R3qic/mb+aeqStNbV/MYag0duysZzQfOrHBa", + "VSXLqUHiW/fZfDVMAKwiQdsWx3ihvvgQgFhJUYHUzA5KqyorRU7LTGmqcaR/kzCfvJj85bi1vxzb7uo4", + "mPy16XWOnYzIasWgjFbVHmO8MaKP2sIsDIPGT8gmLNtDoYlxu4mGlJhhwSVcU66PWpWlww+aA/zOzdTi", + "20o7Ft89FSyJcGIbzkBZCdg2vKdIgHqCaCWIVhRIF6WYNT/cP62qFoP4/bSqLD5QegSGghmsmdLqAS6f", + "ticpnOfs1RH5JhwbRXHBy425HKyoYe6Gubu13C3W2JbcGtoR7ymC2ynkkdkajwYj5h+C4lCtWIrSSD07", + "acU0/odrG5KZ+X1U5z8HiYW4TRMXKloOc1bHwV8C5eZ+j3KGhOPMPUfktN/3dmRjRokTzK1oZet+2nG3", + "4LFB4Y2klQXQfbF3KeOopNlGFtY7ctORjC4Kc3CGA1pDqG591naehygkSAo9GL4sRX71D6qWBzjzMz/W", + "8PjhNGQJtABJllQtjyYxKSM8Xu1oY46YaYgKPpkFUx01SzzU8nYsraCaBktz8MbFEot67IdMD2REd/kB", + "/0NLYj6bs21Yvx32iFwgA1P2ODsnQ2G0fasg2JlMA7RCCLKyCj4xWvdeUL5sJ4/v06g9+sraFNwOuUU0", + "O3SxZoU61DbhYKm9CgXUs1dWo9OwUhGtrVkVlZJu4mu3c41BwIWoSAnXUPZBsCwLR7MIEeuD84UvxToG", + "05diPeAJYg0H2QkzDsrVHrs74HvlIBNyN+Zx7DFINws0srxC9sBDEcjM0lqrT2dC3o4d9/gsJ60NnlAz", + "anAbTXtIwqZ1lbmzGbHj2Qa9gVq353Yu2h8+hrEOFs41/R2woMyoh8BCd6BDY0GsKlbCAUh/Gb0FZ1TB", + "k8fk/B+nzx49/vnxsy8MSVZSLCRdkdlGgyL3nbJKlN6U8GC4MlQX61LHR//iqbfcdseNjaNELXNY0Wo4", + "lLUIW5nQNiOm3RBrXTTjqhsAR3FEMFebRTuxzg4D2iumjMi5mh1kM1IIK9pZCuIgKWAnMe27vHaaTbhE", + "uZH1IXR7kFLI6NVVSaFFLsrsGqRiIuJeeuNaENfCy/tV/3cLLbmhipi50RZec5SwIpSl13w837dDX6x5", + "i5utnN+uN7I6N++Yfeki35tWFalAZnrNSQGzetFRDedSrAglBXbEO/ob0FZuYSs413RV/TCfH0Z3FjhQ", + "RIdlK1BmJmJbGKlBQS64DQ3Zoa66Ucegp48Yb7PUaQAcRs43PEfD6yGObVqTXzGOXiC14Xmg1hsYSygW", + "HbK8u/qeQoed6p6KgGPQ8Ro/o+XnFZSafi3kRSv2fSNFXR1cyOvPOXY51C3G2ZYK09cbFRhflN1wpIWB", + "/Si2xs+yoJf++Lo1IPRIka/ZYqkDPeuNFGJ+eBhjs8QAxQ9WSy1Nn6Gu+r0oDDPRtTqACNYO1nI4Q7ch", + "X6MzUWtCCRcF4ObXKi6cJQJY0HOODn8dynt6aRXPGRjqymltVltXBN3Zg/ui7ZjR3J7QDFGjEs68xgtr", + "W9npbHBEKYEWGzID4ETMnMfM+fJwkRR98dqLN040jPCLDlyVFDkoBUXmLHU7QfPt7NWht+AJAUeAm1mI", + "EmRO5Z2BvbreCecVbDKMHFHk/rc/qQefAV4tNC13IBbbxNDb2D2cW3QI9bjptxFcf/KQ7KgE4u8VogVK", + "syVoSKFwL5wk968P0WAX746Wa5DooPxdKd5PcjcCakD9nen9rtDWVSIe0qm3RsIzG8YpF16wig1WUqWz", + "XWzZNOro4GYFASeMcWIcOCF4vaZKW6c64wXaAu11gvNYIcxMkQY4qYaYkX/yGshw7Nzcg1zVqlFHVF1V", + "QmooYmvgsN4y1/ewbuYS82DsRufRgtQKdo2cwlIwvkOWXYlFENWN78lFnQwXhx4ac89voqjsANEiYhsg", + "575VgN0wJiwBCFMtoi3hMNWjnCYQbTpRWlSV4RY6q3nTL4Wmc9v6VP/Yth0SF9XtvV0IUBiK5to7yG8s", + "Zm004JIq4uAgK3plZA80g1jv/xBmcxgzxXgO2TbKRxXPtAqPwM5DWlcLSQvICijpZjjoj/YzsZ+3DYA7", + "3qq7QkNmw7rim95Sso+i2TK0wPFUTHgk+IXk5ggaVaAlENd7x8gF4Ngx5uTo6F4zFM4V3SI/Hi7bbnVk", + "RLwNr4U2O+7oAUF2HH0MwAk8NEPfHhXYOWt1z/4U/wHKTdDIEftPsgGVWkI7/l4LSNhQXcR8cF567L3H", + "gaNsM8nGdvCR1JFNGHTfUKlZzirUdb6FzcFVv/4EUb8rKUBTVkJBgg9WDazC/sQGJPXHvJ0qOMr2NgR/", + "YHyLLKdkCkWeLvBXsEGd+42NdA1MHYfQZSOjmvuJcoKA+vg5I4KHTWBNc11ujKCml7AhNyCBqHq2Ylrb", + "CPauqqtFlYUDRP0aW2Z0Xs2oT3Grm/UchwqWN9yK6cTqBNvhu+gpBh10OF2gEqIcYSEbICMKwagAGFIJ", + "s+vMBdP7cGpPSR0gHdNGl3Zz/d9THTTjCsh/iJrklKPKVWtoZBohUVBAAdLMYESwZk4X6tJiCEpYgdUk", + "8cvDh/2FP3zo9pwpMocb/wLFNOyj4+FDtOO8EUp3DtcB7KHmuJ1Frg90+JiLz2khfZ6yO9TCjTxmJ9/0", + "Bm+8ROZMKeUI1yz/zgygdzLXY9Ye0si4MBMcd5Qvp+OyH64b9/2creqS6kN4reCalpm4BilZATs5uZuY", + "Cf7VNS1/aLrh6xrIDY3mkOX4JmTkWHBh+thnJGYcxpk5wDaEdCxAcGZ7ndtOO1TMNkqPrVZQMKqh3JBK", + "Qg729YSRHFWz1CNi4yrzJeULVBikqBcusM+Ogwy/VtY0I2s+GCIqVOk1z9DIHbsAXDC3f0BjxCmgRqXr", + "W8itAnNDm/ncm6kxN3OwB32PQdRJNp0kNV6D1OtW47XI6b4CGnEZdOS9AD/txCNdKYg6I/sM8RVuizlM", + "ZnN/H5N9O3QMyuHEQahh+zEVbWjU7XJzAKHHDkQkVBIUXlGhmUrZr2Ievvhzd5jaKA2roSXfdv05cfze", + "JvVFwUvGIVsJDpvoI3fG4Tv8GD1OeE0mOqPAkurb10E68PfA6s4zhhrvil/c7f4J7Xus1NdCHsolagcc", + "Ld6P8EDudLe7KW/rJ6VlGXEtuvdAfQagpk3+ASYJVUrkDGW2s0JN7UFz3kj3eKiL/jdNlPMBzl5/3J4P", + "LXxqijZiKCtCSV4ytCALrrSsc33JKdqogqVGgp+8Mp62Wr70TeJm0ogV0w11ySkGvjWWq2jAxhwiZpqv", + "AbzxUtWLBSjd03XmAJfctWKc1JxpnGtljktmz0sFEiOQjmzLFd2QuaEJLchvIAWZ1bor/eNzN6VZWTqH", + "npmGiPklp5qUQJUm3zF+scbhvNPfH1kO+kbIqwYL8dt9ARwUU1k8SOsb+xUDit3yly64GNMT2M8+WLN9", + "fzsxy+w8uf+/9//9xbvT7D9p9ttJ9vx/HL//8PTjg4eDHx9//Pvf/1/3pycf//7g3/8ttlMe9thjLAf5", + "2SunGZ+9QvWn9QENYP9k9v8V41mUyMJojh5tkfv48NgR0IOucUwv4ZLrNTeEdE1LVhjechty6N8wg7No", + "T0ePajob0TOG+bXuqVTcgcuQCJPpscZbS1HDuMb4s0d0SrqXjHhe5jW3W+mlb/uqx8eXifm0edpqs968", + "IPjucUl9cKT78/GzLybT9r1i830ynbiv7yOUzIp17FVqAeuYrugOCB6Me4pUdKNAx7kHwh4NpbOxHeGw", + "K1jNQKolqz49p1CazeIczr+VcDanNT/jNjDenB90cW6c50TMPz3cWgIUUOllLBtGR1DDVu1uAvTCTiop", + "roFPCTuCo77NpzD6ogvqK4HOMSsDap9ijDbUnANLaJ4qAqyHCxllWInRT+9ZgLv81cHVITdwDK7+nI0/", + "0/+tBbn3zVcX5NgxTHXPPpC2QwdPWiOqtHu11QlIMtzM5gCyQt4lv+SvYI7WB8FfXPKCano8o4rl6rhW", + "IL+kJeU5HC0EeeEfgr2iml7ygaSVTNMVPMEjVT0rWU6uQoWkJU+bemU4wuXlO1ouxOXl+0FsxlB9cFNF", + "+YudIDOCsKh15hJHZBJuqIz5vlSTOABHtplhts1qhWxRWwOpT0zhxo/zPFpVqv+AeLj8qirN8gMyVO55", + "rNkyorSQXhYxAoqFBvf3e+EuBklvvF2lVqDILytavWNcvyfZZX1y8gRI50XtL+7KNzS5qWC0dSX5wLlv", + "VMGFW7US1lrSrKKLmIvt8vKdBlrh7qO8vEIbR1kS7NZ5yesD83GodgEeH+kNsHDs/SoRF3due/kkYfEl", + "4CfcQmxjxI3W8X/b/Qre9t56u3rvgwe7VOtlZs52dFXKkLjfmSZ30MIIWT4aQ7EFaqsuzdIMSL6E/Mrl", + "v4FVpTfTTncf8OMETc86mLKZkezLPMzNgQ6KGZC6KqgTxSnf9JMkKNDahxW/hSvYXIg2tcc+WRG6j/RV", + "6qAipQbSpSHW8Ni6Mfqb76LKULGvKv/WHR89erJ40dCF75M+yFbkPcAhjhFF5xF5ChFURhBhiT+Bglss", + "1Ix3J9KPLc9oGTN780WyJHneT1yTVnlyAWDhatDqbr+vANOsiRtFZtTI7cJlCLMP0QMuViu6gISEHPqI", + "Rj737viVcJBd9170phPz/oU2uG+iINvGmVlzlFLAfDGkgspML+zPz2TdkM4zgYk/HcJmJYpJTXykZTpU", + "dnx1NpNhCrQ4AYPkrcDhwehiJJRsllT55GWY482f5VEywO+YWGFbOp2zIGItSOTWJMvxPLd/TgfapUuq", + "4zPp+PQ5oWo5IhWOkfAxSD62HYKjAFRACQu7cNvYE0qb5KHdIAPHD/N5yTiQLBb8FphBg2vGzQFGPn5I", + "iLXAk9EjxMg4ABvd6zgw+V6EZ5Mv9gGSuyQV1I+Njvngb4g/H7Ph4EbkEZVh4Szh1co9B6AuYrK5v3px", + "uzgMYXxKDJu7pqVhc07jawcZZHVBsbWXw8UFeDxIibNbHCD2YtlrTfYqus1qQpnJAx0X6LZAPBPrzL4f", + "jUq8s/XM0Hs0Qh5fs8YOps2fc0+RmVhj0BBeLTYiewcsaTg8GIGGv2YK6RX7pW5zC8y2abdLUzEqVEgy", + "zpzXkEtKnBgzdUKCSZHL/SAlzq0A6Bk72vzSTvndqaR2xZPhZd7eatM21Zt/fBQ7/qkjFN2lBP6GVpgm", + "ic2bvsQStVN0Y1+6+XsCETJG9IZNDJ00Q1eQghJQKcg6QlR2FfOcGt0G8MY5990C4wVmCaJ88yAIqJKw", + "YEpDa0T3cRKfwzxJMTmhEPP06nQl52Z9b4VorinrRsSOnWV+8hVgRPKcSaUz9EBEl2Aafa1Qqf7aNI3L", + "St2QLZvKlxVx3oDTXsEmK1hZx+nVzfvtKzPt9w1LVPUM+S3jNmBlhqmno4GcW6a2sb5bF/zaLvg1Pdh6", + "x50G09RMLA25dOf4k5yLHufdxg4iBBgjjuGuJVG6hUEGD3CH3DGQmwIf/9E26+vgMBV+7J1RO/4ZcOqO", + "siNF1xIYDLaugqGbyIglTAeZm4cvYxNngFYVK9Y9W6gdNakx070MHj7fXQ8LuLtusB0Y6MblRcOcO7kC", + "XfSfs/kco4B8bEQ4Gw7oYt1AopZj34QWtUSjWifYbpiYshHsRq7925/OtZB0Ac4wmlmQ7jQELmcfNARp", + "HxXRzHo4CzafQ2gQVLcxZnWA65t9osUdRhBZ3GpYM66/eBojox3U08K4G2VxionQQspNdDE0vHqxKtA7", + "m8olwdbcwnoafUH6LWyyn4yGQirKpGojxpwltMv/9tj169W3sMGRdwZiGcB27AqqqW8BaTBmFmw+2YcT", + "jQoU5jDFpA+dLdxjp07ju3SgrXFZZ9PE34Zld7Kydpdyl4PR+u0MLGN24zzuLjOnB7qI75Pyrk1gCWNc", + "SI6ByBVOxZSv0TO8iprn0bto9wJo6YkXlzP5OJ3czTkVu83ciDtw/aa5QKN4xuAn66zo+Jr3RDmtKimu", + "aZk5F17q8pfi2l3+2Nx7/D6xMBmn7IuvTl+/ceB/nE7yEqjMGmUsuSpsV/1pVmXz1G6/SlBi8VYRq6wH", + "m98k1wzdfjdLcMUUAn1/kPW5dekGR9G5AefxGMydvM95n+0St3ihoWqc0K2DxPqgu35nek1Z6T0THtpE", + "vCQublzq8ChXCAe4s/86CEPIDspuBqc7fjpa6trBk3CuHzBbWlzj4C6XGrIi54+mB5eevhayw/zdY5mo", + "P/v3E6uMkG3xmAgf9AV6+sLUEbGC1y+LX8xpfPgwPGoPH07JL6X7EACIv8/c76hfPHwYdTVELQmGSaCh", + "gNMVPGgCf5Mb8WnNThxuxl3Qp9erRrIUaTJsKNQ6pj26bxz2biRz+CzcLwWUYH7a/baut+kW3SEwY07Q", + "eepxTBP3tLI1gRQRvB/mh++yDGkhs19RzHpuPTfDI8TrFXo7MlWyPO4H5jNl2Cu38T2mMcHGCYOZGbFm", + "iXAxXrNgLNNsTBq/HpDBHFFkqmgmwRZ3M+GOd83ZrzUQVhitZs5A4r3Wu+q8coCjDgRSo3oO53ID2yiC", + "dvi72EHCjP99mRGB2G4ECaOJBuC+asz6fqGN16zVmfYNSgxnHDDuLQGFjj4cNdsHFstuVNA4PWZMbUjP", + "6FzpgcQc0VqPTGVzKX6DuC0aTfiRt9m+xgHDSNzfIFTPwgpnHZbSeKDakpXt7Lu2e7xunNr4O+vCftFN", + "WYXbXKbxU73fRt5G6VXxDKIOySklLHRHdqNVE6wFj1cQn4UZ7X2oAuX2PNmHyZ1HD/FTGT4vOrbjt6fS", + "wTx4klXSmxmNpfs3upCBKdjeTlCFFsR39hugmme3dnYSBBU2bZlNblSBbHNTDBMl3lKvsdOO1mhaBQYp", + "KlRdpjYQrFQiMkzNbyi3ZRJNP8uvXG8F1gtqet0IianJVDz+o4CcraLm2MvLd0U+9PUXbMFsBcBaQVBi", + "zg1kq6taKnJl+prH5A41Z3NyMg3qXLrdKNg1U2xWArZ4ZFvMqMLrsvFINl3M8oDrpcLmj0c0X9a8kFDo", + "pbKIVYI0uicKeU0U0wz0DQAnJ9ju0XNyH+O3FLuGBwaLTgiavHj0HL3v9o+T2C3rKjhuY9kF8ux/Op4d", + "p2MMYLNjGCbpRj2KZnGyJZzTt8OW02S7jjlL2NJdKLvP0opyuoB4yPBqB0y2L+4melR7eOHWGwBKS7Eh", + "TMfnB00Nf0o8QzTsz4JBcrFaMb1yUT5KrAw9tfXj7KR+OFvM1JX+8HD5jxgsV/lYoZ6t6xOrMXSVeEaA", + "IY3f0xV00Tol1OajK1kbxuoLEpEzn+4Sa6E0JVAsbsxcZukoS2JU65xUknGN9o9az7O/GbVY0tywv6MU", + "uNnsi6eRmiLdtPt8P8A/Od4lKJDXcdTLBNl7mcX1Jfe54NnKcJTiQfvsNziVyai+ePxWKohs+9BjJV8z", + "SpYkt7pDbjTg1HciPL5lwDuSYrOevehx75V9csqsZZw8aG126Me3r52UsRIylsO6Pe5O4pCgJYNrfMQR", + "3yQz5h33QpajduEu0H/eEBQvcgZimT/LUUUg8Ghue79ppPifvmuT8aJj1T6O6dkAhYxYO53d7hMHfO1n", + "dev7b23MDn5LYG402myl9wFWEqG6Nha36fOJn/NGzb12zzsGx0e/EGl0cJTjHz5EoB8+nDox+JfH3c+W", + "vT98GM+JGTW5mV9bLNxFI8a+sT38UkQMYL4AVRNQ5J7sRgyQqUvKfDBMcOaGmpJusZ9PL0Uc5jFIPOAv", + "fgouL9/hF48H/KOPiM/MLHED25Dm9GHvFjuLkkzRfA9CjSn5UqzHEk7vDvLE8wdAUQIlI81zuJJBMbeo", + "u35nvEhAo2bUGZTCKJlhnYrQnv/nwbNZ/HQLtmtWFj+16YZ6F4mkPF9GAzVnpuPPbdH1ZomWVUZT3y8p", + "51BGh7O67c9eB45o6f8SY+dZMT6ybb+YoF1ub3Et4F0wPVB+QoNepkszQYjVbiaX5qVwuRAFwXnaPOst", + "cxxW5QxKhf1ag9Kxo4Ef7GsldHYZ5msrVRHgBVq/jsg3mFPBwNJJootWJ5+esJuqq65KQYsppk28+Or0", + "NbGz2j62dLCtlLVAo0t3FVEr+fjUZU0V4Pib/PHjbH8kbFatdNYUtoplPTIt2tJbrBc6geaYEDtH5JW1", + "hClvZ7GTEEy+KVdQBHW0rC6GNGH+ozXNl2hi6lxkaZIfX+LNU2VrgA/qRTd1FfDcGbhdlTdb5G1KhF6C", + "vGEK8BUmXEM30VKTdcyZOH3ipe7yZM25pZSjPWSKporCvmj3wFmBxPuGo5D1EL+ngcFWSNy34t059oqm", + "ee6Xz+s5b33anqYO8HfORpxTLjjLMclyTCDCpDDjvE0j8lHH3URq4k5o5HBFi/Y1778cFpNl/DwjdIgb", + "em6Dr2ZTLXXYPzWsXTGXBWjlOBsUU1970vk1GFfg6mQYIgr5pJCR2JRoPHvjB9+TjDDfQ8JQ9bX59r0z", + "Y+JD6CvG0WDh0ObEbOt5KBVDByMnTJOFAOXW0016pd6ZPkeY/6mA9fuj12LB8nO2wDFsNJRZtg39Gw51", + "6gMBXeCdafvStHVZeZufO1E9dtLTqnKTpiuTxssxr3kSwbHwEx8PECC3GT8cbQu5bY3gxfvUEBpcY/AR", + "VHgPDwijqdLZK4ltVARLUdiC2LdJ0dR8jEfAeM2494TFL4g8eiXgxuB5TfRTuaTaioCjeNoF0DIRx45v", + "/awr9a5D9XMSG5TgGv0c6W1sC4wmGEfToBXcKN8QfygMdQfCxEtaNhGwkXKhKFU5IarANyK9AqIxxmEY", + "ty9R3L0AdlQln7bdMc/3vjdRKvvRrC4WoDNaFLGyJV/iV4Jf/VsfWENeN+UtqorkmOyzm/10SG1uolxw", + "Va+2zOUb3HG6oCJvhBrCqsB+hzG7wmyD/+5TL76Jfd37fZsPdC32S/k7fK8Xk3oNTWeKLbLxmMA75e7o", + "aKe+HaG3/Q9K6aVYdAH5HEbSBJcL9yjG374yF0eYEnAQZmyvliZjH4b0Cvzuk1w0uaa6XAmvskEFE3Re", + "N3Xat5sh0hXXp3j5Jd6UhiZve79aM3DqZWmefAhNtUvJoinZyoKSaS5syGfPiD70BKXCPG2U5+GMz26t", + "WxGadsF823G42FCfllkkHS2384W0G7yvM+Tb69RjY58BHL/3KzJfgcvTVkm4ZqL2QTQ+lNWrhPbXTn3j", + "5rl3dP3RAPHPbXxOmsovXGU8u0ynk3/7k3WmEeBabv4AhvPBpg9qPQ+lXWueapuQpqjSqCJLnVtxTHb8", + "WCJ2Jxt2qk3vqJU9IKtXY8SBYe3r6eSs2OvCjCXzn9hRYscuXsk6neu4zW+MR6wSirW1zWIlrkfGjF9g", + "leogV/NwLB9LeA25xoJ2bYyUBNgnc7OZzNvu/zvncVqdbkLrXarjbfmNh1XsdtzxgxQkQRodWwHsaHw2", + "39MmEtY+5LmhCnPfS7Rxd5++jn6AN59Drtn1jpQv/1wCD9KJTL1dBmGZBxlgWPMcBTOG7m91bAHalpFl", + "KzxB5v47g5N6jnwFm3uKdKghWpKseYt1m2SRiAHkDpkhEaFikWbWkOyCf5hqKAOx4CM7bXdo024nqxkH", + "CYxuOZcnSXNxtEmNtkwZL6c6ai7Tda9UX/iyIpUVZliNMa1/vMLil8rFOdEm2WSopZOzYUr+G5esEhP0", + "NL4Tn7YSlP/NZ+Oys5TsCsJ6y+ipuqGy8C2iphdv1cm23EeDVC6+kmAf6HkzM2vj8Ie+6kiSZ3zSkpfC", + "iBFZ6l1QN/S9iRu7p2yAX5uHBeGag3R16VH+LYWCTAsft78Njm2osFGMt0KCShZWsMAl052+bfO5YoEZ", + "iulNqQteDBdIJKyogU4GWVfTc25D9kv73b+l9gVGdlqYGnrdXenOv8BgaoDEkOrnxN2Wu99o38bYxDgH", + "mXnPUz8FKwfZ9YZUUhR1bi/o8GA0BrnRKVC2sJKonSYfrrKnIwRvna9gc2yVIF8i0O9gCLSVnCzoQeq+", + "3iYf1PymYnAvDgLe57RcTSeVEGWWcHacDfPG9in+iuVXUBBzU/hI5UT1V3IfbeyNN/tmufF5UqsKOBQP", + "jgg55fZtiHdsdwsX9Sbn9/S2+dc4a1HbVM7OqHZ0yeNB9phkWd6Rm/lhtvMwBYbV3XEqO8iOrKTrRM5a", + "SW8itZCPxmrlQ1dzvz5tS1QWiphMcm49Vi/xoMcMR/iSPUi5gI5MSpyni6hSxEIyb/Pa3gwVx1Q4GQKk", + "gY959N1A4QaPIiBacTVyCm0GM5e7TMyJhNaJfNskbsPisDGNvj9zM0uX382FhE6ZV9NbyMKLPEy19Zip", + "nDEtqdzcJtXaoDjtwHqSxPLOcKwmEqtdSBuNNcRhWYqbDJlV1uQ2j6m2pp3qXsa+nEvbz5zqGQRxXVQ5", + "QW1DlrQguZAS8rBH/NmehWolJGSlwDCvmAd6ro3cvcK3OpyUYkFElYsCbI2AOAWl5qo5pyg2QRBVE0WB", + "pR189Gn7BHQ8cspDVUa2yXnsojPry0wEnoJyyXgchmzjIbxbqgrvlZ3/bI4WIYaxLt2311b6DGsrw56l", + "lVlZeoNBqroy+VHVGI6ED2/MFE/JSijtNDs7kmqGakO87ueCaynKsmsEsiLxwlm2v6Pr0zzXr4W4mtH8", + "6gHqkVzoZqXF1D9L7QfjtTPJXkamkWWgL5YROy/O4k/d3rWeHefYu0RrAOb73Rxrt437NFbKuruufm12", + "nsidqcWK5XEa/nNFtyVj0mIsIZrqyVZJso/zsRky6vByaIIZkCUN0QzcEGxsvxxPc05dZB7mvyjx9scl", + "c3CXROJiGvJJJ7VkeVK26gGAkNoXo7qWtrRSKPk0XEUs7AtzdEn3AR3JxTHy526wmREODpSGOwE1iDZs", + "ALxvlf2pTcllIxdnYu2/P2hzdt0K+I/bqTxWjj5yihvSctXyfX6PBEeIZwbeGn+EhcP9Dbo7Cqkpgzfy", + "Rg0ASMcldWAYFZ20LxhzykooMqoTlzvahKaBZutetPSLmzLlOHlO7YW9BGLGriW4fBNWpO4VQ6+oISXR", + "NB9abnkBa1CYDMJWdKbK+hm8vwNKW1aqp3yLKivhGjrhWi4JRo2iHbsG31c1nUkBUKH3r2+TisUhhXd5", + "z1Dh1p4FkSxjsBu1XFjE2p0iO8wSUSPKmmf2mKixR8lAdM2Kmnbwp/YVObpmN3OUI6gayOSZ19vGTvOj", + "HeGtH+DU94+JMh4T78fxob1ZUBx12xjQzrjEWqVOPY+HJYYZXhqHBs5WNI5PS+It31AVveFpA+CQ5Fv1", + "ZuQ+McEDxH61hhylmm7c3d1xQnAwonrZm5IiuGx2+PaG5M9Cw1tJODleTNVQgAx2q6XG04UT2LEBlrPk", + "Ruw1UjOWkHL83/G/KVbgtwMZvdpWtAo1uFfgPXaYULpxVjiBljUXmo8vnLp8gn2lnAWR1Su6IULiP0Zf", + "+7WmJZtv8IRa8H03opbUkJBzEVrftYtXNBNvF0ymHjBvFxB+KrtuNnbMYLiNGSUA2lyBzjiFmYGuINwG", + "dMtbzpNrw3JUPVsxpfCy623nEAtu8T4nxIoWoY6Mmem6pUR9rlLT+3+2r7bCqXxCqaqkua9fBkTRVc8g", + "bmsUeuLSS1htf9Y3VI89CTR1D1uilf45b3EL496ekRuxWPlUvYcO2IN6cINSF3daxj4FituX0VseRI5a", + "yqF3YWx8yABodDL7rF47wLfZGH0GsE+B/2jSyNQyxoD/R8F7ooxeCK+tmPcJsNx58h+B1dpVZ2KdSZir", + "XaEQ1rBqFGHZJgvwxknGcwlU2diQsx+cytbmRGTcqJA2erHxvjWjFDBnvGWWjFe1jmgAmBqRbwKEheZp", + "RGvC2ZOSEowYdk3LH65BSlakNs6cDlvGK8xJ703yrm9E+W/u1OEATLXaD74khPalWtDMXOC26o0NLFSa", + "8oLKImzOOMlBmnuf3NCNur3vw0ArayNf7PB+0ECa6b5vD/wgSNoWkHLj3Jd39Ew0ANIDuihGuBYwgjXi", + "VrBGES0SnoQhDPG0CnSdlWKB78sSBOiST6LvxyorgqPB1spD+82j2G+wfRrMu+0OvhY465gptp+zHxB1", + "qPD8yJneetKsNa3/4M9GZNqD4OmfL9qwcLs5Q/qPvdG8wEcMnXea/aLzfq9teIidDxKejK4FN7GL6CB3", + "D3xDc+34ekZdH3zsJajVYTPUbdWWwG9QbZAzzV3gztDoM1CKLVKm7h3tnjYha0n290ACPFup1p2t7rRN", + "MIUZZ58iUNtfzmaVqLJ8TDSgTc1fOIO2g7QLY4I+AnN1Yt1N4IRqilV0Ept0qlbsWwcrWTVjl1+myrcp", + "2SmDRoKDdo3lYo68DI+wNePgG4/GeDHtvz7qGmwaJkEokZDXEg2aN3Szu65QIiXs+T9Onz16/PPjZ18Q", + "04AUbAGqTSvcq8vTRowx3rezfNoYscHydHwT/Lt0izjvKfPPbZpNcWfNclvV5gwcVCXaxxIauQAixzFS", + "D+ZWe4XjtEHff6ztii3y4DsWQ8Hvs2cusjW+gFPu9BcxJ9t5Rrfmn47zCyP8Ry4pv7W3WGDKHpt+F30b", + "emwNsn8YKow89D4Y7TXL/T0oLipl3q587ijQho9+I+SBACRe83XeYYXVtdt8ldLadtEK7B1m/Uvsu9aR", + "tjPsHCHxHXaAFz7Pa9s1kdIOnM+c+PG7BinBUt6nKKGz/F0v/twCW89jsEVO1dUalGVLYihcBM851cvm", + "lWRCth08psRS2ka/KcvII0yrfeOZCgnHCJbympafnmtgjfVTxAcUb9NPL8KXeCGSLSrV7fKAvaaj5g5e", + "3R1uav4GH37+E8weRe85N5RzOg5uM7SdYGHjhb8V7FtScoNj2qCSR1+QmcvJXknImeo7M63HKYgKvAbJ", + "5i6AD9Z6x0u3Xev8Seg7kPHcRx6Q7wOnhEDjTwthe0Q/M1NJnNwolceob0AWEfzFeFRYw3HHdXHH/N23", + "SysRJIjaM63EsDrl2OXZ1Anm0qkVDNc5+rbu4DZyUbdrG5sTZXQa8MvLd3o2JpVJPGW36Y65VA6Su3uv", + "zN2/QxYViyM3hps3RjE/pfJq2tyRiRSuvf2oWbkzzKCTkPfjdLIADoopTDn7sysx8GnvUg+Bfdk9PKoW", + "1ruko7CIiay1M3kwVZBqd0SWXdctklMXX03ltWR6g+UlvRmG/RzN9/JNkzvA5Z5oPCDu7tPiCpoSv22m", + "gVr52/UbQUu8j6xjhptbSJRH5Ks1XVWlMyqSv9+b/RWe/O1pcfLk0V9nfzt5dpLD02fPT07o86f00fMn", + "j+Dx3549PYFH8y+ezx4Xj58+nj19/PSLZ8/zJ08fzZ5+8fyv9wwfMiBbQH0G6BeT/5OdlguRnb45yy4M", + "sC1OaMW+BbM3qCvPBZY/M0jN8STCirJy8sL/9L/8CTvKxaod3v86cWU8JkutK/Xi+Pjm5uYo7HK8wKfF", + "mRZ1vjz282BRqo688uasiUm20RO4o60NEjfVkcIpfnv71fkFOX1zdtQSzOTF5OTo5OiRq4DKacUmLyZP", + "8Cc8PUvc92NHbJMXHz5OJ8dLoCVm4jB/rEBLlvtPEmixcf9XN3SxAHmEYef2p+vHx16sOP7gnlh/3Pbt", + "OHTMH3/ovEQvdvREp/LxB18HcXvrTg08F88TdBgJxbZmxzOsfTC2KaigcXopqGyo4w8oLid/P3Y2j/hH", + "VFvseTj26RriLTtY+qDXBtYdPdasCFaSU50v6+r4A/4HqfejZSclxFI32JzclLTNp4RpQmdCYuU8nS8N", + "B/Elu5gKWoaFdM8KcwxMr5cWAl8BFb20kxfvhgHoOBDxIyHPMAeiPdKdmVqujQ7OoM5/cyd12rc307uT", + "7Pn7D4+mj04+/sXcPO7PZ08+jnyr8bIZl5w318rIhu+x3hVGpeFJf3xy4tmbUx4C0jx2JzlY3ECJahdp", + "N6kJehve+o4W0gHGbqt6A5EGGTvq8vSGHwovyNGf7rnirZamTqJBHL5fCKEg/l0kzv3o0819xm2onbk5", + "7A33cTp59ilXf8YNydOSYMug0OJw63/kV1zccN/SiCP1akXlxh9j1WEKxG02Xnp0odDxJdk1RSmQCx5k", + "T+KLyXt8hx97m5rgN0rTW/Cbc9Prv/lNp2G80LY1f7iinIG71l4mTQ0S8CnlfIgmLa4pz300eBtkivtl", + "BV5HGE0cU61gXpf+3XFVsrmtdSpE6SdSdVUZjjOnqqEsF9lqJFj7jLMZmtQ8F9x6xDGI2OdSxOeY+HBT", + "XbGq04XNDVW5KpwcwL3Uw03/tQa5aXd9xYwo2m7vIGbj92ThFo8HYOHdgQ7Mwh/vyUb//Cv+r31pPT35", + "26eDwGcruGArELX+s16a5/YGu9Ol6WR4m3D7WK/5MUbJHX/oaCTu80Aj6f7edg9bXK9EAV6FEPO5reK+", + "7fPxB/tvMBGsK5BsBdyWU3W/2pvjGIt5boY/b3ge/XG4jk4ixsTPx97EEdNyuy0/dP7sKndqWetC3Nj6", + "U1F5Ba9PWrp6y2jJb6wC5h50A7Q5IskPVXNRucQHhGK9HVHr1mxjI4Ldi8LGsYY3mlo6D8aCcZwAPSQ4", + "iy0sToMLXIG5G9EY0ZONHGTfiwKGslHsInQwdi7D5ihEynjf+WIcMt6P+x0U9ORYN+SQjMzHWvX/Pr6h", + "TBsJyiVrRIwOO2ug5bGrzNL7tU2GPviCGd6DH8NnkdFfj2n3XHSNJGbLUh0HFpTYV2dBSDTyMcn+c2tN", + "Da2TSC6NXfLde7PrWIPZUVJrbHtxfIyPVJZC6WOURLuGuPDj+2ajfenAZsPNt3UmJFswTsvMGbna8lKT", + "x0cnk4//PwAA///7V+betvkAAA==", } // 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 89202c3fb1..6d7d2c4177 100644 --- a/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go +++ b/daemon/algod/api/server/v2/generated/nonparticipating/public/routes.go @@ -1003,7 +1003,8 @@ var swaggerSpec = []string{ "jWPwgjM6uyeorNGTauAeNEqO9inp41GvoG2QuqpD5yxymmxmgBTRkAcC/NQTH6JG6y3R3xL9l070sZKK", "iLpZy1ph8RVuyzWbta67gOgNWsk+SXXh2xL9f/YS/Z4DKUKJpA0dJN4bjirCNLnAskhTIOb+KtE67xru", "OX0dM+2Co+4qbSrXni9dUMZdTZ0qrwHh0K5bvPbtaa/FsGmZGVo0DTogLSXTG9RaaMF+Pwfz/3dG7Fcg", - "V16hKWU+ejpaaF08PTrKRUrzhVD6aPRxHD5TrYfvKvg/eF2kkGxl9KuP7z7+3wAAAP//BgzyFgmnAQA=", + "V16hKWU+ejpaaF08PTrKRUrzhVD6aPRxHD5TrYfvKvg/eF2kkGxl9KuPCLaQbM64uXMv6HwOsjYhjh5N", + "How+/t8AAAD//0RubYkspwEA", } // 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 79a65f5721..f352c24c7e 100644 --- a/daemon/algod/api/server/v2/generated/participating/private/routes.go +++ b/daemon/algod/api/server/v2/generated/participating/private/routes.go @@ -412,7 +412,7 @@ var swaggerSpec = []string{ "tc+EfYYjxlXBP4RrfGqDXRRX1l5HOYENsxFskQ08rg3vT5b3J8v712F5Z/sZTVcwubPV6xq2K1o1ti61", "rHUhbgIPN8Jio0+HPj6r+Pf/Pr2hTGdzIV2NfjrXIIcfa6DlqWvI2fu17YE1eIKNvYIfw2o40V9Paddp", "2fWNG9ab+nDgOI89dY7jxEs+FdU/boNowqAUZPtNOMq794ZlK5BrfyO0MRYvTk+xNsFSKH06+Tj90Iu/", - "CB++b8jjQ3OPODL5+P7j/wsAAP//bjFnIYoHAQA=", + "CB++b8jjQ3OPODL5iHQhJFswTsvMxTa0XYUnT04eTT7+vwAAAP//tpMWzK0HAQA=", } // 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 67e59f47b8..107ad4eba1 100644 --- a/daemon/algod/api/server/v2/generated/participating/public/routes.go +++ b/daemon/algod/api/server/v2/generated/participating/public/routes.go @@ -391,7 +391,8 @@ var swaggerSpec = []string{ "kO1XO4f9X/W4r3sV1DGJGEuT+BpJRFXzOUuZRXku+ILQhWgioLESJBf4BKQBzvahIUxPXa8n5opKujbV", "7XrNbcm9LwGcNVu4M2qgQy7xgAFDeHtGC/zbmFCB/9FS+k2LAd2WkW4du8dV/+Qqn4KrfHa+8kf3wwam", "xf+WYuaT4yd/2AWFhujvhSbfYHT/7cSxuvV/rB3PTQUtX2fDm/uaCOEw4hZv0TrW9t17cxEokCt/wTYB", - "pCdHR1h4aSmUPpqY668dXBo+fF/D/MHfTqVkK+z3+v7j/w8AAP//4/5pcmcQAQA=", + "pCdHR1h4aSmUPpqY668dXBo+fF/D/MHfTqVkK+z3itZNIdmCcZonLnAzaYJEHx0eTz7+/wAAAP//5fUK", + "SIoQAQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/network/p2p/capabilities_test.go b/network/p2p/capabilities_test.go index 170916dd65..82fee84d03 100644 --- a/network/p2p/capabilities_test.go +++ b/network/p2p/capabilities_test.go @@ -18,6 +18,8 @@ package p2p import ( "context" + "math/rand" + "sync" "testing" "time" @@ -36,7 +38,7 @@ import ( "github.com/algorand/go-algorand/test/partitiontest" ) -func TestCapabilitiesDiscovery(t *testing.T) { +func TestCapabilities_Discovery(t *testing.T) { partitiontest.PartitionTest(t) golog.SetDebugLogging() @@ -113,7 +115,7 @@ func waitForRouting(t *testing.T, disc *CapabilitiesDiscovery) { } } -func setupCapDiscovery(t *testing.T, numHosts int) []*CapabilitiesDiscovery { +func setupCapDiscovery(t *testing.T, numHosts int, numBootstrapPeers int) []*CapabilitiesDiscovery { var hosts []host.Host var bootstrapPeers []*peer.AddrInfo var capsDisc []*CapabilitiesDiscovery @@ -133,7 +135,14 @@ func setupCapDiscovery(t *testing.T, numHosts int) []*CapabilitiesDiscovery { bootstrapPeers = append(bootstrapPeers, &peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}) } for _, h := range hosts { - ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, bootstrapPeers) + bp := bootstrapPeers + if numBootstrapPeers != 0 && numBootstrapPeers != numHosts { + rand.Shuffle(len(bootstrapPeers), func(i, j int) { + bp[i], bp[j] = bp[j], bp[i] + }) + bp = bp[:numBootstrapPeers] + } + ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, bp) require.NoError(t, err) disc, err := algodht.MakeDiscovery(ht) require.NoError(t, err) @@ -147,7 +156,7 @@ func setupCapDiscovery(t *testing.T, numHosts int) []*CapabilitiesDiscovery { return capsDisc } -func TestDHTTwoPeers(t *testing.T) { +func TestCapabilities_DHTTwoPeers(t *testing.T) { partitiontest.PartitionTest(t) numAdvertisers := 2 @@ -176,6 +185,7 @@ func TestDHTTwoPeers(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) var advertisers []peer.AddrInfo peersChan, err := disc.FindPeers(ctx, topic, discovery.Limit(numAdvertisers)) + require.NoError(t, err) pollingForPeers: for { select { @@ -194,69 +204,104 @@ func TestDHTTwoPeers(t *testing.T) { } } -func TestVaryingCapabilities(t *testing.T) { +func TestCapabilities_Varying(t *testing.T) { partitiontest.PartitionTest(t) - numAdvertisers := 10 - capsDisc := setupCapDiscovery(t, numAdvertisers) - noCap := capsDisc[:3] - archOnly := capsDisc[3:5] - catchOnly := capsDisc[5:7] - archCatch := capsDisc[7:] + const numAdvertisers = 10 - for _, disc := range archOnly { - waitForRouting(t, disc) - disc.AdvertiseCapabilities(Archival) - } - for _, disc := range catchOnly { - waitForRouting(t, disc) - disc.AdvertiseCapabilities(Catchpoints) - } - for _, disc := range archCatch { - waitForRouting(t, disc) - disc.AdvertiseCapabilities(Archival, Catchpoints) + var tests = []struct { + name string + numBootstrap int + }{ + {"bootstrap=all", numAdvertisers}, + {"bootstrap=2", 2}, } - for _, disc := range noCap { - require.Eventuallyf(t, - func() bool { - numArchPeers := len(archOnly) + len(archCatch) - peers, err := disc.PeersForCapability(Archival, numArchPeers) - if err == nil && len(peers) == numArchPeers { - return true - } - return false - }, - time.Minute, - time.Second, - "Not all expected archival peers were found", - ) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + capsDisc := setupCapDiscovery(t, numAdvertisers, test.numBootstrap) + noCap := capsDisc[:3] + archOnly := capsDisc[3:5] + catchOnly := capsDisc[5:7] + archCatch := capsDisc[7:] - require.Eventuallyf(t, - func() bool { - numCatchPeers := len(catchOnly) + len(archCatch) - peers, err := disc.PeersForCapability(Catchpoints, numCatchPeers) - if err == nil && len(peers) == numCatchPeers { - return true - } - return false - }, - time.Minute, - time.Second, - "Not all expected catchpoint peers were found", - ) - } + var wg sync.WaitGroup + wg.Add(len(archOnly) + len(catchOnly) + len(archCatch)) + for _, disc := range archOnly { + go func(disc *CapabilitiesDiscovery) { + defer wg.Done() + waitForRouting(t, disc) + disc.AdvertiseCapabilities(Archival) + }(disc) + } + for _, disc := range catchOnly { + go func(disc *CapabilitiesDiscovery) { + defer wg.Done() + waitForRouting(t, disc) + disc.AdvertiseCapabilities(Catchpoints) + }(disc) + } + for _, disc := range archCatch { + go func(disc *CapabilitiesDiscovery) { + defer wg.Done() + waitForRouting(t, disc) + disc.AdvertiseCapabilities(Archival, Catchpoints) + }(disc) + } + + wg.Wait() - for _, disc := range capsDisc[3:] { - disc.Close() - // Make sure it actually closes - disc.wg.Wait() + wg.Add(len(noCap) * 2) + for _, disc := range noCap { + go func(disc *CapabilitiesDiscovery) { + defer wg.Done() + require.Eventuallyf(t, + func() bool { + numArchPeers := len(archOnly) + len(archCatch) + peers, err := disc.PeersForCapability(Archival, numArchPeers) + if err == nil && len(peers) == numArchPeers { + return true + } + return false + }, + time.Minute, + time.Second, + "Not all expected archival peers were found", + ) + }(disc) + + go func(disc *CapabilitiesDiscovery) { + defer wg.Done() + require.Eventuallyf(t, + func() bool { + numCatchPeers := len(catchOnly) + len(archCatch) + peers, err := disc.PeersForCapability(Catchpoints, numCatchPeers) + if err == nil && len(peers) == numCatchPeers { + return true + } + return false + }, + time.Minute, + time.Second, + "Not all expected catchpoint peers were found", + ) + }(disc) + } + + wg.Wait() + + for _, disc := range capsDisc[3:] { + disc.Close() + // Make sure it actually closes + disc.wg.Wait() + } + }) } } -func TestCapabilitiesExcludesSelf(t *testing.T) { +func TestCapabilities_ExcludesSelf(t *testing.T) { partitiontest.PartitionTest(t) - disc := setupCapDiscovery(t, 2) + disc := setupCapDiscovery(t, 2, 2) testPeersFound := func(disc *CapabilitiesDiscovery, n int, cap Capability) bool { peers, err := disc.PeersForCapability(cap, n+1) diff --git a/network/p2p/dht/dht.go b/network/p2p/dht/dht.go index e612f6062d..9266605af6 100644 --- a/network/p2p/dht/dht.go +++ b/network/p2p/dht/dht.go @@ -38,7 +38,7 @@ import ( const minBackoff = time.Second * 5 const maxBackoff = time.Second * 20 -const baseBackoff = float64(1) +const baseBackoff = float64(1.1) // getBootstrapPeersFunc looks up a list of Multiaddrs strings from the dnsaddr records at the primary // SRV record domain. @@ -70,28 +70,30 @@ func dhtProtocolPrefix(network string) protocol.ID { // MakeDHT creates the dht.IpfsDHT object func MakeDHT(ctx context.Context, h host.Host, network string, cfg config.Local, bootstrapPeers []*peer.AddrInfo) (*dht.IpfsDHT, error) { - var peers []peer.AddrInfo - for _, bPeer := range bootstrapPeers { - if bPeer != nil { - peers = append(peers, *bPeer) - } - } dhtCfg := []dht.Option{ // Automatically determine server or client mode dht.Mode(dht.ModeAutoServer), // We don't need the value store right now dht.DisableValues(), dht.ProtocolPrefix(dhtProtocolPrefix(network)), - dht.BootstrapPeers(peers...), } - if len(bootstrapPeers) == 0 { + if len(bootstrapPeers) > 0 { + var peers []peer.AddrInfo + for _, bPeer := range bootstrapPeers { + if bPeer != nil { + peers = append(peers, *bPeer) + } + } + dhtCfg = append(dhtCfg, dht.BootstrapPeers(peers...)) + + } else { dhtCfg = append(dhtCfg, dht.BootstrapPeersFunc(getBootstrapPeersFunc(cfg, network))) } return dht.New(ctx, h, dhtCfg...) } func backoffFactory() backoff.BackoffFactory { - return backoff.NewExponentialDecorrelatedJitter(minBackoff, maxBackoff, baseBackoff, rand.New(rand.NewSource(rand.Int63()))) + return backoff.NewExponentialDecorrelatedJitter(minBackoff, maxBackoff, baseBackoff, rand.NewSource(rand.Int63())) } // MakeDiscovery creates a discovery.Discovery object using backoff and cacching From b6f8a69a7494464f1b6aaaa498a4c4576a69c9a3 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Tue, 14 Nov 2023 18:55:59 -0500 Subject: [PATCH 04/38] network: hybrid networking (#5800) --- agreement/fuzzer/networkFacade_test.go | 19 ++- agreement/gossip/network_test.go | 2 +- catchup/fetcher_test.go | 11 +- catchup/ledgerFetcher.go | 2 +- components/mocks/mockNetwork.go | 14 +- config/localTemplate.go | 6 + config/local_defaults.go | 2 + data/txHandler_test.go | 3 +- installer/config.json.example | 2 + network/connPerfMon_test.go | 2 +- network/gossipNode.go | 21 ++- network/hybridNetwork.go | 221 +++++++++++++++++++++++++ network/netidentity.go | 51 +++++- network/netidentity_test.go | 4 +- network/p2p/p2p.go | 13 +- network/p2p/peerID.go | 47 ++++++ network/p2pNetwork.go | 83 ++++++---- network/p2pNetwork_test.go | 4 + network/wsNetwork.go | 47 +++--- network/wsNetwork_test.go | 8 +- network/wsPeer.go | 4 + node/follower_node.go | 2 +- node/node.go | 10 +- rpcs/blockService.go | 2 +- rpcs/blockService_test.go | 17 +- rpcs/httpTxSync.go | 4 +- rpcs/txService_test.go | 5 +- rpcs/txSyncer_test.go | 8 +- test/testdata/configs/config-v31.json | 2 + 29 files changed, 508 insertions(+), 108 deletions(-) create mode 100644 network/hybridNetwork.go diff --git a/agreement/fuzzer/networkFacade_test.go b/agreement/fuzzer/networkFacade_test.go index d18f8251dd..35eba4a273 100644 --- a/agreement/fuzzer/networkFacade_test.go +++ b/agreement/fuzzer/networkFacade_test.go @@ -70,9 +70,16 @@ type NetworkFacade struct { rand *rand.Rand timeoutAtInitOnce sync.Once timeoutAtInitWait sync.WaitGroup - peerToNode map[network.Peer]int + peerToNode map[*facadePeer]int } +type facadePeer struct { + id int + net network.GossipNode +} + +func (p *facadePeer) GetNetwork() network.GossipNode { return p.net } + // MakeNetworkFacade creates a facade with a given nodeID. func MakeNetworkFacade(fuzzer *Fuzzer, nodeID int) *NetworkFacade { n := &NetworkFacade{ @@ -83,12 +90,12 @@ func MakeNetworkFacade(fuzzer *Fuzzer, nodeID int) *NetworkFacade { eventsQueues: make(map[string]int), eventsQueuesCh: make(chan int, 1000), rand: rand.New(rand.NewSource(int64(nodeID))), - peerToNode: make(map[network.Peer]int, fuzzer.nodesCount), + peerToNode: make(map[*facadePeer]int, fuzzer.nodesCount), debugMessages: false, } n.timeoutAtInitWait.Add(1) for i := 0; i < fuzzer.nodesCount; i++ { - n.peerToNode[network.Peer(new(int))] = i + n.peerToNode[&facadePeer{id: i, net: n}] = i } return n } @@ -179,7 +186,7 @@ func (n *NetworkFacade) WaitForEventsQueue(cleared bool) { func (n *NetworkFacade) Broadcast(ctx context.Context, tag protocol.Tag, data []byte, wait bool, exclude network.Peer) error { excludeNode := -1 if exclude != nil { - excludeNode = n.peerToNode[exclude] + excludeNode = n.peerToNode[exclude.(*facadePeer)] } return n.broadcast(tag, data, excludeNode, "NetworkFacade service-%v Broadcast %v %v\n") } @@ -341,8 +348,8 @@ func (n *NetworkFacade) ReceiveMessage(sourceNode int, tag protocol.Tag, data [] n.pushPendingReceivedMessage() } -func (n *NetworkFacade) Disconnect(sender network.Peer) { - sourceNode := n.peerToNode[sender] +func (n *NetworkFacade) Disconnect(sender network.DisconnectablePeer) { + sourceNode := n.peerToNode[sender.(*facadePeer)] n.fuzzer.Disconnect(n.nodeID, sourceNode) } diff --git a/agreement/gossip/network_test.go b/agreement/gossip/network_test.go index 8584dc8d26..3607afb168 100644 --- a/agreement/gossip/network_test.go +++ b/agreement/gossip/network_test.go @@ -136,7 +136,7 @@ func (w *whiteholeNetwork) Relay(ctx context.Context, tag protocol.Tag, data []b func (w *whiteholeNetwork) BroadcastSimple(tag protocol.Tag, data []byte) error { return w.Broadcast(context.Background(), tag, data, true, nil) } -func (w *whiteholeNetwork) Disconnect(badnode network.Peer) { +func (w *whiteholeNetwork) Disconnect(badnode network.DisconnectablePeer) { return } func (w *whiteholeNetwork) DisconnectPeers() { diff --git a/catchup/fetcher_test.go b/catchup/fetcher_test.go index 983de01475..02224d7bdd 100644 --- a/catchup/fetcher_test.go +++ b/catchup/fetcher_test.go @@ -21,7 +21,6 @@ import ( "net" "net/http" "net/url" - "strings" "testing" "github.com/gorilla/mux" @@ -174,8 +173,8 @@ func (b *basicRPCNode) GetPeers(options ...network.PeerOption) []network.Peer { return b.peers } -func (b *basicRPCNode) SubstituteGenesisID(rawURL string) string { - return strings.Replace(rawURL, "{genesisID}", "test genesisID", -1) +func (b *basicRPCNode) GetGenesisID() string { + return "test genesisID" } type httpTestPeerSource struct { @@ -192,8 +191,8 @@ func (s *httpTestPeerSource) RegisterHandlers(dispatch []network.TaggedMessageHa s.dispatchHandlers = append(s.dispatchHandlers, dispatch...) } -func (s *httpTestPeerSource) SubstituteGenesisID(rawURL string) string { - return strings.Replace(rawURL, "{genesisID}", "test genesisID", -1) +func (s *httpTestPeerSource) GetGenesisID() string { + return "test genesisID" } // implement network.HTTPPeer @@ -239,6 +238,8 @@ func (p *testUnicastPeer) GetAddress() string { return "test" } +func (p *testUnicastPeer) GetNetwork() network.GossipNode { return p.gn } + func (p *testUnicastPeer) Request(ctx context.Context, tag protocol.Tag, topics network.Topics) (resp *network.Response, e error) { responseChannel := make(chan *network.Response, 1) diff --git a/catchup/ledgerFetcher.go b/catchup/ledgerFetcher.go index 43a039a09e..971aeb5eb5 100644 --- a/catchup/ledgerFetcher.go +++ b/catchup/ledgerFetcher.go @@ -79,7 +79,7 @@ func (lf *ledgerFetcher) requestLedger(ctx context.Context, peer network.HTTPPee return nil, err } - parsedURL.Path = lf.net.SubstituteGenesisID(path.Join(parsedURL.Path, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36))) + parsedURL.Path = network.SubstituteGenesisID(lf.net, path.Join(parsedURL.Path, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36))) ledgerURL := parsedURL.String() lf.log.Debugf("ledger %s %#v peer %#v %T", method, ledgerURL, peer, peer) request, err := http.NewRequestWithContext(ctx, method, ledgerURL, nil) diff --git a/components/mocks/mockNetwork.go b/components/mocks/mockNetwork.go index 8c8eb113f1..e1ceb63142 100644 --- a/components/mocks/mockNetwork.go +++ b/components/mocks/mockNetwork.go @@ -28,6 +28,7 @@ import ( // MockNetwork is a dummy network that doesn't do anything type MockNetwork struct { network.GossipNode + GenesisID string } // Broadcast - unused function @@ -58,7 +59,7 @@ func (network *MockNetwork) RequestConnectOutgoing(replace bool, quit <-chan str } // Disconnect - unused function -func (network *MockNetwork) Disconnect(badpeer network.Peer) { +func (network *MockNetwork) Disconnect(badpeer network.DisconnectablePeer) { } // DisconnectPeers - unused function @@ -75,7 +76,7 @@ func (network *MockNetwork) GetPeers(options ...network.PeerOption) []network.Pe } // GetRoundTripper -- returns the network round tripper -func (network *MockNetwork) GetRoundTripper() http.RoundTripper { +func (network *MockNetwork) GetRoundTripper(peer network.Peer) http.RoundTripper { return http.DefaultTransport } @@ -106,7 +107,10 @@ func (network *MockNetwork) GetHTTPRequestConnection(request *http.Request) (con return nil } -// SubstituteGenesisID - empty implementation -func (network *MockNetwork) SubstituteGenesisID(rawURL string) string { - return rawURL +// GetGenesisID - empty implementation +func (network *MockNetwork) GetGenesisID() string { + if network.GenesisID == "" { + return "mocknet" + } + return network.GenesisID } diff --git a/config/localTemplate.go b/config/localTemplate.go index 9c547720e0..694a926015 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -585,6 +585,12 @@ type Local struct { // EnableP2P turns on the peer to peer network EnableP2P bool `version[31]:"false"` + // EnableP2PHybridMode turns on both websockets and P2P networking. + EnableP2PHybridMode bool `version[31]:"false"` + + // P2PListenAddress sets the listen address used for P2P networking, if hybrid mode is set. + P2PListenAddress string `version[31]:""` + // P2PPersistPeerID will write the private key used for the node's PeerID to the P2PPrivateKeyLocation. // This is only used when P2PEnable is true. If P2PPrivateKey is not specified, it uses the default location. P2PPersistPeerID bool `version[29]:"false"` diff --git a/config/local_defaults.go b/config/local_defaults.go index 2aa1ba3be9..b1c1f23784 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -76,6 +76,7 @@ var defaultLocal = Local{ EnableMetricReporting: false, EnableOutgoingNetworkMessageFiltering: true, EnableP2P: false, + EnableP2PHybridMode: false, EnablePingHandler: true, EnableProcessBlockStats: false, EnableProfiler: false, @@ -117,6 +118,7 @@ var defaultLocal = Local{ OptimizeAccountsDatabaseOnStartup: false, OutgoingMessageFilterBucketCount: 3, OutgoingMessageFilterBucketSize: 128, + P2PListenAddress: "", P2PPersistPeerID: false, P2PPrivateKeyLocation: "", ParticipationKeysRefreshInterval: 60000000000, diff --git a/data/txHandler_test.go b/data/txHandler_test.go index 486bd7c0f8..ea622ca68b 100644 --- a/data/txHandler_test.go +++ b/data/txHandler_test.go @@ -60,7 +60,8 @@ var txBacklogSize = config.GetDefaultLocal().TxBacklogSize // mock sender is used to implement OnClose, since TXHandlers expect to use Senders and ERL Clients type mockSender struct{} -func (m mockSender) OnClose(func()) {} +func (m mockSender) OnClose(func()) {} +func (m mockSender) GetNetwork() network.GossipNode { panic("not implemented") } // txHandlerConfig is a subset of tx handler related options from config.Local type txHandlerConfig struct { diff --git a/installer/config.json.example b/installer/config.json.example index b4de6dd5df..1179a5b629 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -55,6 +55,7 @@ "EnableMetricReporting": false, "EnableOutgoingNetworkMessageFiltering": true, "EnableP2P": false, + "EnableP2PHybridMode": false, "EnablePingHandler": true, "EnableProcessBlockStats": false, "EnableProfiler": false, @@ -96,6 +97,7 @@ "OptimizeAccountsDatabaseOnStartup": false, "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, + "P2PListenAddress": "", "P2PPersistPeerID": false, "P2PPrivateKeyLocation": "", "ParticipationKeysRefreshInterval": 60000000000, diff --git a/network/connPerfMon_test.go b/network/connPerfMon_test.go index a8398b0f36..ae9038c5ce 100644 --- a/network/connPerfMon_test.go +++ b/network/connPerfMon_test.go @@ -48,7 +48,7 @@ func makeMsgPool(N int, peers []Peer) (out []IncomingMessage) { addMsg := func(msgCount int) { for i := 0; i < msgCount; i++ { - msg.Sender = peers[(int(msgIndex)+i)%len(peers)] + msg.Sender = peers[(int(msgIndex)+i)%len(peers)].(DisconnectablePeer) timer += int64(7 * time.Nanosecond) msg.Received = timer out = append(out, msg) diff --git a/network/gossipNode.go b/network/gossipNode.go index 7ae667170a..8d1b877449 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -20,6 +20,7 @@ import ( "context" "net" "net/http" + "strings" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/protocol" @@ -28,6 +29,11 @@ import ( // Peer opaque interface for referring to a neighbor in the network type Peer interface{} +// DisconnectablePeer is a Peer with a long-living connection to a network that can be disconnected +type DisconnectablePeer interface { + GetNetwork() GossipNode +} + // PeerOption allows users to specify a subset of peers to query // //msgp:ignore PeerOption @@ -51,7 +57,7 @@ type GossipNode interface { Address() (string, bool) Broadcast(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error Relay(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error - Disconnect(badnode Peer) + Disconnect(badnode DisconnectablePeer) DisconnectPeers() // only used by testing // RegisterHTTPHandler path accepts gorilla/mux path annotations @@ -78,7 +84,7 @@ type GossipNode interface { ClearHandlers() // GetRoundTripper returns a Transport that would limit the number of outgoing connections. - GetRoundTripper() http.RoundTripper + GetRoundTripper(peer Peer) http.RoundTripper // OnNetworkAdvance notifies the network library that the agreement protocol was able to make a notable progress. // this is the only indication that we have that we haven't formed a clique, where all incoming messages @@ -90,8 +96,8 @@ type GossipNode interface { // request that was provided to the http handler ( or provide a fallback Context() to that ) GetHTTPRequestConnection(request *http.Request) (conn net.Conn) - // SubstituteGenesisID substitutes the "{genesisID}" with their network-specific genesisID. - SubstituteGenesisID(rawURL string) string + // GetGenesisID returns the network-specific genesisID. + GetGenesisID() string // called from wsPeer to report that it has closed peerRemoteClose(peer *wsPeer, reason disconnectReason) @@ -109,7 +115,7 @@ var outgoingMessagesBufferSize = int( // IncomingMessage represents a message arriving from some peer in our p2p network type IncomingMessage struct { - Sender Peer + Sender DisconnectablePeer Tag Tag Data []byte Err error @@ -207,3 +213,8 @@ func max(numbers ...uint64) (maxNum uint64) { } return } + +// SubstituteGenesisID substitutes the "{genesisID}" with their network-specific genesisID. +func SubstituteGenesisID(net GossipNode, rawURL string) string { + return strings.Replace(rawURL, "{genesisID}", net.GetGenesisID(), -1) +} diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go new file mode 100644 index 0000000000..c09c5d4a1a --- /dev/null +++ b/network/hybridNetwork.go @@ -0,0 +1,221 @@ +// Copyright (C) 2019-2023 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package network + +import ( + "context" + "fmt" + "net" + "net/http" + "sync" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" +) + +// HybridP2PNetwork runs both P2PNetwork and WebsocketNetwork to implement the GossipNode interface +type HybridP2PNetwork struct { + p2pNetwork *P2PNetwork + wsNetwork *WebsocketNetwork + genesisID string + + useP2PAddress bool +} + +// NewHybridP2PNetwork constructs a GossipNode that combines P2PNetwork and WebsocketNetwork +func NewHybridP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID, nodeInfo NodeInfo) (*HybridP2PNetwork, error) { + // supply alternate NetAddress for P2P network + p2pcfg := cfg + p2pcfg.NetAddress = cfg.P2PListenAddress + p2pnet, err := NewP2PNetwork(log, p2pcfg, datadir, phonebookAddresses, genesisID, networkID) + if err != nil { + return nil, err + } + wsnet, err := NewWebsocketNetwork(log, cfg, phonebookAddresses, genesisID, networkID, nodeInfo, p2pnet.PeerID(), p2pnet.PeerIDSigner()) + if err != nil { + return nil, err + } + return &HybridP2PNetwork{ + p2pNetwork: p2pnet, + wsNetwork: wsnet, + genesisID: genesisID, + }, nil +} + +// Address implements GossipNode +func (n *HybridP2PNetwork) Address() (string, bool) { + // TODO map from configuration? used for REST API, goal status, algod.net, etc + if n.useP2PAddress { + return n.p2pNetwork.Address() + } + return n.wsNetwork.Address() +} + +type hybridNetworkError struct{ p2pErr, wsErr error } + +func (e *hybridNetworkError) Error() string { + return fmt.Sprintf("p2pErr: %s, wsErr: %s", e.p2pErr, e.wsErr) +} +func (e *hybridNetworkError) Unwrap() []error { return []error{e.p2pErr, e.wsErr} } + +func (n *HybridP2PNetwork) runParallel(fn func(net GossipNode) error) error { + var wg sync.WaitGroup + var p2pErr, wsErr error + + wg.Add(2) + go func() { + defer wg.Done() + p2pErr = fn(n.p2pNetwork) + }() + go func() { + defer wg.Done() + wsErr = fn(n.wsNetwork) + }() + wg.Wait() + + if p2pErr != nil && wsErr != nil { + return &hybridNetworkError{p2pErr, wsErr} + } + if p2pErr != nil { + return p2pErr + } + if wsErr != nil { + return wsErr + } + return nil +} + +// Broadcast implements GossipNode +func (n *HybridP2PNetwork) Broadcast(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error { + return n.runParallel(func(net GossipNode) error { + return net.Broadcast(ctx, tag, data, wait, except) + }) +} + +// Relay implements GossipNode +func (n *HybridP2PNetwork) Relay(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error { + return n.runParallel(func(net GossipNode) error { + return net.Relay(ctx, tag, data, wait, except) + }) +} + +// Disconnect implements GossipNode +func (n *HybridP2PNetwork) Disconnect(badnode DisconnectablePeer) { + net := badnode.GetNetwork() + if net == n.p2pNetwork { + n.p2pNetwork.Disconnect(badnode) + } else if net == n.wsNetwork { + n.wsNetwork.Disconnect(badnode) + } else { + panic("badnode.GetNetwork() returned a network that is not part of this HybridP2PNetwork") + } +} + +// DisconnectPeers implements GossipNode +func (n *HybridP2PNetwork) DisconnectPeers() { + _ = n.runParallel(func(net GossipNode) error { + net.DisconnectPeers() + return nil + }) +} + +// RegisterHTTPHandler implements GossipNode +func (n *HybridP2PNetwork) RegisterHTTPHandler(path string, handler http.Handler) { + n.p2pNetwork.RegisterHTTPHandler(path, handler) + n.wsNetwork.RegisterHTTPHandler(path, handler) +} + +// RequestConnectOutgoing implements GossipNode +func (n *HybridP2PNetwork) RequestConnectOutgoing(replace bool, quit <-chan struct{}) {} + +// GetPeers implements GossipNode +func (n *HybridP2PNetwork) GetPeers(options ...PeerOption) []Peer { + // TODO better way of combining data from peerstore and returning in GetPeers + var peers []Peer + peers = append(peers, n.p2pNetwork.GetPeers(options...)...) + peers = append(peers, n.wsNetwork.GetPeers(options...)...) + return peers +} + +// Start implements GossipNode +func (n *HybridP2PNetwork) Start() { + _ = n.runParallel(func(net GossipNode) error { + net.Start() + return nil + }) +} + +// Stop implements GossipNode +func (n *HybridP2PNetwork) Stop() { + _ = n.runParallel(func(net GossipNode) error { + net.Start() + return nil + }) +} + +// RegisterHandlers adds to the set of given message handlers. +func (n *HybridP2PNetwork) RegisterHandlers(dispatch []TaggedMessageHandler) { + n.p2pNetwork.RegisterHandlers(dispatch) + n.wsNetwork.RegisterHandlers(dispatch) +} + +// ClearHandlers deregisters all the existing message handlers. +func (n *HybridP2PNetwork) ClearHandlers() { + n.p2pNetwork.ClearHandlers() + n.wsNetwork.ClearHandlers() +} + +// GetRoundTripper returns a Transport that would limit the number of outgoing connections. +func (n *HybridP2PNetwork) GetRoundTripper(peer Peer) http.RoundTripper { + // TODO today this is used by HTTPTxSync.Sync after calling GetPeers(network.PeersPhonebookRelays) + switch p := peer.(type) { + case *wsPeer: + return p.net.GetRoundTripper(peer) + case gossipSubPeer: + return p.net.GetRoundTripper(peer) + default: + panic("unrecognized peer type") + } +} + +// OnNetworkAdvance notifies the network library that the agreement protocol was able to make a notable progress. +// this is the only indication that we have that we haven't formed a clique, where all incoming messages +// arrive very quickly, but might be missing some votes. The usage of this call is expected to have similar +// characteristics as with a watchdog timer. +func (n *HybridP2PNetwork) OnNetworkAdvance() { + _ = n.runParallel(func(net GossipNode) error { + net.OnNetworkAdvance() + return nil + }) +} + +// GetHTTPRequestConnection returns the underlying connection for the given request. Note that the request must be the same +// request that was provided to the http handler ( or provide a fallback Context() to that ) +func (n *HybridP2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn net.Conn) { + return nil +} + +// GetGenesisID returns the network-specific genesisID. +func (n *HybridP2PNetwork) GetGenesisID() string { + return n.genesisID +} + +// called from wsPeer to report that it has closed +func (n *HybridP2PNetwork) peerRemoteClose(peer *wsPeer, reason disconnectReason) { + panic("wsPeer should only call WebsocketNetwork.peerRemoteClose or P2PNetwork.peerRemoteClose") +} diff --git a/network/netidentity.go b/network/netidentity.go index 940ea0a633..e7bee84835 100644 --- a/network/netidentity.go +++ b/network/netidentity.go @@ -94,12 +94,39 @@ type identityChallengeScheme interface { VerifyResponse(h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) } +type identityChallengeSigner interface { + Sign(message crypto.Hashable) crypto.Signature + SignBytes(message []byte) crypto.Signature + Verify(message crypto.Hashable, sig crypto.Signature) bool + PublicKey() crypto.PublicKey +} + +type identityChallengeLegacySigner struct { + keys *crypto.SignatureSecrets +} + +func (s *identityChallengeLegacySigner) Sign(message crypto.Hashable) crypto.Signature { + return s.keys.Sign(message) +} + +func (s *identityChallengeLegacySigner) SignBytes(message []byte) crypto.Signature { + return s.keys.SignBytes(message) +} + +func (s *identityChallengeLegacySigner) Verify(message crypto.Hashable, sig crypto.Signature) bool { + return s.keys.SignatureVerifier.Verify(message, sig) +} + +func (s *identityChallengeLegacySigner) PublicKey() crypto.PublicKey { + return s.keys.SignatureVerifier +} + // identityChallengePublicKeyScheme implements IdentityChallengeScheme by // exchanging and verifying public key challenges and attaching them to headers, // or returning the message payload to be sent type identityChallengePublicKeyScheme struct { dedupName string - identityKeys *crypto.SignatureSecrets + identityKeys identityChallengeSigner } // NewIdentityChallengeScheme will create a default Identification Scheme @@ -108,15 +135,21 @@ func NewIdentityChallengeScheme(dn string) *identityChallengePublicKeyScheme { if dn == "" { return &identityChallengePublicKeyScheme{} } + var seed crypto.Seed crypto.RandBytes(seed[:]) return &identityChallengePublicKeyScheme{ dedupName: dn, - identityKeys: crypto.GenerateSignatureSecrets(seed), + identityKeys: &identityChallengeLegacySigner{keys: crypto.GenerateSignatureSecrets(seed)}, } } +// NewIdentityChallengeSchemeWithSigner will create an identification Scheme with a given signer +func NewIdentityChallengeSchemeWithSigner(dn string, signer identityChallengeSigner) *identityChallengePublicKeyScheme { + return &identityChallengePublicKeyScheme{dedupName: dn, identityKeys: signer} +} + // AttachChallenge will generate a new identity challenge and will encode and attach the challenge // as a header. It returns the identityChallengeValue used for this challenge, so the network can // confirm it later (by passing it to VerifyResponse), or returns an empty challenge if dedupName is @@ -126,7 +159,7 @@ func (i identityChallengePublicKeyScheme) AttachChallenge(attachTo http.Header, return identityChallengeValue{} } c := identityChallenge{ - Key: i.identityKeys.SignatureVerifier, + Key: i.identityKeys.PublicKey(), Challenge: newIdentityChallengeValue(), PublicAddress: []byte(addr), } @@ -173,7 +206,7 @@ func (i identityChallengePublicKeyScheme) VerifyRequestAndAttachResponse(attachT } // make the response object, encode it and attach it to the header r := identityChallengeResponse{ - Key: i.identityKeys.SignatureVerifier, + Key: i.identityKeys.PublicKey(), Challenge: idChal.Msg.Challenge, ResponseChallenge: newIdentityChallengeValue(), } @@ -271,12 +304,12 @@ type identityVerificationMessageSigned struct { Signature crypto.Signature `codec:"sig"` } -func (i identityChallenge) signAndEncodeB64(s *crypto.SignatureSecrets) string { +func (i identityChallenge) signAndEncodeB64(s identityChallengeSigner) string { signedChal := i.Sign(s) return base64.StdEncoding.EncodeToString(protocol.Encode(&signedChal)) } -func (i identityChallenge) Sign(secrets *crypto.SignatureSecrets) identityChallengeSigned { +func (i identityChallenge) Sign(secrets identityChallengeSigner) identityChallengeSigned { return identityChallengeSigned{Msg: i, Signature: secrets.Sign(i)} } @@ -289,12 +322,12 @@ func (i identityChallengeSigned) Verify() bool { return i.Msg.Key.Verify(i.Msg, i.Signature) } -func (i identityChallengeResponse) signAndEncodeB64(s *crypto.SignatureSecrets) string { +func (i identityChallengeResponse) signAndEncodeB64(s identityChallengeSigner) string { signedChalResp := i.Sign(s) return base64.StdEncoding.EncodeToString(protocol.Encode(&signedChalResp)) } -func (i identityChallengeResponse) Sign(secrets *crypto.SignatureSecrets) identityChallengeResponseSigned { +func (i identityChallengeResponse) Sign(secrets identityChallengeSigner) identityChallengeResponseSigned { return identityChallengeResponseSigned{Msg: i, Signature: secrets.Sign(i)} } @@ -307,7 +340,7 @@ func (i identityChallengeResponseSigned) Verify() bool { return i.Msg.Key.Verify(i.Msg, i.Signature) } -func (i identityVerificationMessage) Sign(secrets *crypto.SignatureSecrets) identityVerificationMessageSigned { +func (i identityVerificationMessage) Sign(secrets identityChallengeSigner) identityVerificationMessageSigned { return identityVerificationMessageSigned{Msg: i, Signature: secrets.Sign(i)} } diff --git a/network/netidentity_test.go b/network/netidentity_test.go index 9222da4600..8093d01b22 100644 --- a/network/netidentity_test.go +++ b/network/netidentity_test.go @@ -180,7 +180,7 @@ func TestIdentityChallengeSchemeBadSignature(t *testing.T) { // Copy the logic of attaching the header and signing so we can sign it wrong c := identityChallengeSigned{ Msg: identityChallenge{ - Key: i.identityKeys.SignatureVerifier, + Key: i.identityKeys.PublicKey(), Challenge: newIdentityChallengeValue(), PublicAddress: []byte("i1"), }} @@ -232,7 +232,7 @@ func TestIdentityChallengeSchemeBadResponseSignature(t *testing.T) { r := http.Header{} resp := identityChallengeResponseSigned{ Msg: identityChallengeResponse{ - Key: i.identityKeys.SignatureVerifier, + Key: i.identityKeys.PublicKey(), Challenge: origChal, ResponseChallenge: newIdentityChallengeValue(), }} diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 42db4694c4..d4694a874f 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -29,18 +29,21 @@ import ( "github.com/libp2p/go-libp2p" pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/crypto" "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/p2p/muxer/yamux" + "github.com/libp2p/go-libp2p/p2p/security/noise" "github.com/libp2p/go-libp2p/p2p/transport/tcp" ) // Service defines the interface used by the network integrating with underlying p2p implementation type Service interface { Close() error - ID() peer.ID // return peer.ID for self + ID() peer.ID // return peer.ID for self + IDSigner() *PeerIDChallengeSigner AddrInfo() peer.AddrInfo // return addrInfo for self DialNode(context.Context, *peer.AddrInfo) error @@ -60,6 +63,7 @@ type serviceImpl struct { streams *streamManager pubsub *pubsub.PubSub pubsubCtx context.Context + privKey crypto.PrivKey topics map[string]*pubsub.Topic topicsMu deadlock.RWMutex @@ -99,6 +103,7 @@ func makeHost(cfg config.Local, datadir string, pstore peerstore.Peerstore) (hos libp2p.Muxer("/yamux/1.0.0", &ymx), libp2p.Peerstore(pstore), libp2p.ListenAddrStrings(listenAddr), + libp2p.Security(noise.ID, noise.New), ) } @@ -125,6 +130,7 @@ func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, data streams: sm, pubsub: ps, pubsubCtx: ctx, + privKey: privKey, topics: make(map[string]*pubsub.Topic), }, nil } @@ -139,6 +145,11 @@ func (s *serviceImpl) ID() peer.ID { return s.host.ID() } +// IDSigner returns a PeerIDChallengeSigner that implements the network identityChallengeSigner interface +func (s *serviceImpl) IDSigner() *PeerIDChallengeSigner { + return &PeerIDChallengeSigner{key: s.privKey} +} + // DialPeersUntilTargetCount attempts to establish connections to the provided phonebook addresses func (s *serviceImpl) DialPeersUntilTargetCount(targetConnCount int) { peerIDs := s.host.Peerstore().Peers() diff --git a/network/p2p/peerID.go b/network/p2p/peerID.go index 4d808b05e9..9050bc021d 100644 --- a/network/p2p/peerID.go +++ b/network/p2p/peerID.go @@ -25,6 +25,7 @@ import ( "path" "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" @@ -104,3 +105,49 @@ func generatePrivKey() (crypto.PrivKey, error) { priv, _, err := crypto.GenerateEd25519Key(rand.Reader) return priv, err } + +// PeerIDChallengeSigner implements the identityChallengeSigner interface in the network package. +type PeerIDChallengeSigner struct { + key crypto.PrivKey +} + +// Sign implements the identityChallengeSigner interface. +func (p *PeerIDChallengeSigner) Sign(message algocrypto.Hashable) algocrypto.Signature { + return p.SignBytes(algocrypto.HashRep(message)) +} + +// SignBytes implements the identityChallengeSigner interface. +func (p *PeerIDChallengeSigner) SignBytes(message []byte) algocrypto.Signature { + // libp2p Ed25519PrivateKey.Sign() returns a signature with a length of 64 bytes and no error + sig, err := p.key.Sign(message) + if len(sig) != len(algocrypto.Signature{}) { + panic(fmt.Sprintf("invalid signature length: %d", len(sig))) + } + if err != nil { + panic(err) + } + return algocrypto.Signature(sig) +} + +// Verify implements the identityChallengeSigner interface. +func (p *PeerIDChallengeSigner) Verify(message algocrypto.Hashable, sig algocrypto.Signature) bool { + // libp2p Ed25519PublicKey.Verify() returns a bool and no error + ret, err := p.key.GetPublic().Verify(algocrypto.HashRep(message), sig[:]) + if err != nil { + panic(err) + } + return ret +} + +// PublicKey implements the identityChallengeSigner interface. +func (p *PeerIDChallengeSigner) PublicKey() algocrypto.PublicKey { + // libp2p Ed25519PublicKey.Raw() returns a 32-byte public key and no error + pub, err := p.key.GetPublic().Raw() + if len(pub) != len(algocrypto.PublicKey{}) { + panic(fmt.Sprintf("invalid public key length: %d", len(pub))) + } + if err != nil { + panic(err) + } + return algocrypto.PublicKey(pub) +} diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 71188b2566..b5122516ff 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -59,6 +59,7 @@ type P2PNetwork struct { handler msgHandler broadcaster msgBroadcaster wsPeers map[peer.ID]*wsPeer + wsPeersToIDs map[*wsPeer]peer.ID wsPeersLock deadlock.RWMutex wsPeersChangeCounter atomic.Int32 wsPeersConnectivityCheckTicker *time.Ticker @@ -68,6 +69,13 @@ type p2pPeerStats struct { txReceived atomic.Uint64 } +type gossipSubPeer struct { + peerID peer.ID + net GossipNode +} + +func (p gossipSubPeer) GetNetwork() GossipNode { return p.net } + // NewP2PNetwork returns an instance of GossipNode that uses the p2p.Service func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID) (*P2PNetwork, error) { const readBufferLen = 2048 @@ -83,13 +91,14 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo } net := &P2PNetwork{ - log: log, - config: cfg, - genesisID: genesisID, - networkID: networkID, - topicTags: map[protocol.Tag]string{"TX": p2p.TXTopicName}, - wsPeers: make(map[peer.ID]*wsPeer), - peerStats: make(map[peer.ID]*p2pPeerStats), + log: log, + config: cfg, + genesisID: genesisID, + networkID: networkID, + topicTags: map[protocol.Tag]string{"TX": p2p.TXTopicName}, + wsPeers: make(map[peer.ID]*wsPeer), + wsPeersToIDs: make(map[*wsPeer]peer.ID), + peerStats: make(map[peer.ID]*p2pPeerStats), } net.ctx, net.ctxCancel = context.WithCancel(context.Background()) net.handler = msgHandler{ @@ -126,6 +135,16 @@ func (n *P2PNetwork) setup() error { return nil } +// PeerID returns this node's peer ID. +func (n *P2PNetwork) PeerID() p2p.PeerID { + return p2p.PeerID(n.service.ID()) +} + +// PeerIDSigner returns an identityChallengeSigner that uses the libp2p peer ID's private key. +func (n *P2PNetwork) PeerIDSigner() identityChallengeSigner { + return n.service.IDSigner() +} + // Start threads, listen on sockets. func (n *P2PNetwork) Start() { n.wg.Add(1) @@ -176,6 +195,7 @@ func (n *P2PNetwork) innerStop() { n.log.Warnf("Error closing peer %s: %v", peerID, err) } delete(n.wsPeers, peerID) + delete(n.wsPeersToIDs, peer) } n.wsPeersLock.Unlock() closeGroup.Wait() @@ -244,27 +264,35 @@ func (n *P2PNetwork) Relay(ctx context.Context, tag protocol.Tag, data []byte, w } // Disconnect from a peer, probably due to protocol errors. -func (n *P2PNetwork) Disconnect(badnode Peer) { - node, ok := badnode.(peer.ID) - if !ok { - n.log.Warnf("Unknown peer type %T", badnode) - return - } +func (n *P2PNetwork) Disconnect(badpeer DisconnectablePeer) { + var peerID peer.ID + var wsp *wsPeer + n.wsPeersLock.Lock() defer n.wsPeersLock.Unlock() - if wsPeer, ok := n.wsPeers[node]; ok { - wsPeer.CloseAndWait(time.Now().Add(peerDisconnectionAckDuration)) - delete(n.wsPeers, node) + switch p := badpeer.(type) { + case gossipSubPeer: // Disconnect came from a message received via GossipSub + peerID, wsp = p.peerID, n.wsPeers[p.peerID] + case *wsPeer: // Disconnect came from a message received via wsPeer + peerID, wsp = n.wsPeersToIDs[p], p + default: + n.log.Warnf("Unknown peer type %T", badpeer) + return + } + if wsp != nil { + wsp.CloseAndWait(time.Now().Add(peerDisconnectionAckDuration)) + delete(n.wsPeers, peerID) + delete(n.wsPeersToIDs, wsp) } else { - n.log.Warnf("Could not find wsPeer reference for peer %s", node) + n.log.Warnf("Could not find wsPeer reference for peer %s", peerID) } - err := n.service.ClosePeer(node) + err := n.service.ClosePeer(peerID) if err != nil { - n.log.Warnf("Error disconnecting from peer %s: %v", node, err) + n.log.Warnf("Error disconnecting from peer %s: %v", peerID, err) } } -func (n *P2PNetwork) disconnectThread(badnode Peer, reason disconnectReason) { +func (n *P2PNetwork) disconnectThread(badnode DisconnectablePeer, reason disconnectReason) { defer n.wg.Done() n.Disconnect(badnode) // ignores reason } @@ -309,7 +337,7 @@ func (n *P2PNetwork) ClearHandlers() { } // GetRoundTripper returns a Transport that would limit the number of outgoing connections. -func (n *P2PNetwork) GetRoundTripper() http.RoundTripper { +func (n *P2PNetwork) GetRoundTripper(peer Peer) http.RoundTripper { return http.DefaultTransport } @@ -323,11 +351,6 @@ func (n *P2PNetwork) OnNetworkAdvance() {} // request that was provided to the http handler ( or provide a fallback Context() to that ) func (n *P2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn net.Conn) { return nil } -// SubstituteGenesisID substitutes the "{genesisID}" with their network-specific genesisID. -func (n *P2PNetwork) SubstituteGenesisID(rawURL string) string { - return strings.Replace(rawURL, "{genesisID}", n.genesisID, -1) -} - // wsStreamHandler is a callback that the p2p package calls when a new peer connects and establishes a // stream for the websocket protocol. func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream network.Stream, incoming bool) { @@ -358,13 +381,14 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream n } // create a wsPeer for this stream and added it to the peers map. wsp := &wsPeer{ - wsPeerCore: makePeerCore(ctx, n, n.log, n.handler.readBuffer, addr, n.GetRoundTripper(), addr), + wsPeerCore: makePeerCore(ctx, n, n.log, n.handler.readBuffer, addr, n.GetRoundTripper(nil), addr), conn: &wsPeerConnP2PImpl{stream: stream}, outgoing: !incoming, } wsp.init(n.config, outgoingMessagesBufferSize) n.wsPeersLock.Lock() n.wsPeers[peer] = wsp + n.wsPeersToIDs[wsp] = peer n.wsPeersLock.Unlock() n.wsPeersChangeCounter.Add(1) } @@ -374,6 +398,7 @@ func (n *P2PNetwork) peerRemoteClose(peer *wsPeer, reason disconnectReason) { remotePeerID := peer.conn.(*wsPeerConnP2PImpl).stream.Conn().RemotePeer() n.wsPeersLock.Lock() delete(n.wsPeers, remotePeerID) + delete(n.wsPeersToIDs, peer) n.wsPeersLock.Unlock() n.wsPeersChangeCounter.Add(1) } @@ -438,7 +463,7 @@ func (n *P2PNetwork) txTopicHandleLoop() { // txTopicValidator calls txHandler to validate and process incoming transactions. func (n *P2PNetwork) txTopicValidator(ctx context.Context, peerID peer.ID, msg *pubsub.Message) pubsub.ValidationResult { inmsg := IncomingMessage{ - Sender: msg.ReceivedFrom, + Sender: gossipSubPeer{peerID: msg.ReceivedFrom, net: n}, Tag: protocol.TxnTag, Data: msg.Data, Net: n, @@ -446,7 +471,7 @@ func (n *P2PNetwork) txTopicValidator(ctx context.Context, peerID peer.ID, msg * } // if we sent the message, don't validate it - if inmsg.Sender == n.service.ID() { + if msg.ReceivedFrom == n.service.ID() { return pubsub.ValidationAccept } diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 3b6d127596..75fc66b0ad 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -197,6 +197,10 @@ func (s *mockService) ID() peer.ID { return s.id } +func (s *mockService) IDSigner() *p2p.PeerIDChallengeSigner { + panic("not implemented") +} + func (s *mockService) AddrInfo() peer.AddrInfo { return peer.AddrInfo{ ID: s.id, diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 05e7ba44ca..f6978dc3ac 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -44,6 +44,7 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/network/limitlistener" + "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/protocol" tools_network "github.com/algorand/go-algorand/tools/network" "github.com/algorand/go-algorand/tools/network/dnssec" @@ -195,6 +196,9 @@ type WebsocketNetwork struct { NetworkID protocol.NetworkID RandomID string + peerID p2p.PeerID + peerIDSigner identityChallengeSigner + ready atomic.Int32 readyChan chan struct{} @@ -340,7 +344,7 @@ type networkPeerManager interface { // used by msgHandler Broadcast(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error - disconnectThread(badnode Peer, reason disconnectReason) + disconnectThread(badnode DisconnectablePeer, reason disconnectReason) checkPeersConnectivity() } @@ -455,13 +459,13 @@ func (wn *WebsocketNetwork) RelayArray(ctx context.Context, tags []protocol.Tag, return nil } -func (wn *WebsocketNetwork) disconnectThread(badnode Peer, reason disconnectReason) { +func (wn *WebsocketNetwork) disconnectThread(badnode DisconnectablePeer, reason disconnectReason) { defer wn.wg.Done() wn.disconnect(badnode, reason) } // Disconnect from a peer, probably due to protocol errors. -func (wn *WebsocketNetwork) Disconnect(node Peer) { +func (wn *WebsocketNetwork) Disconnect(node DisconnectablePeer) { wn.disconnect(node, disconnectBadData) } @@ -543,14 +547,14 @@ func (wn *WebsocketNetwork) GetPeers(options ...PeerOption) []Peer { var addrs []string addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryRelayRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(), "" /*origin address*/) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersPhonebookArchivalNodes: var addrs []string addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryRelayRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(), "" /*origin address*/) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersPhonebookArchivers: @@ -558,7 +562,7 @@ func (wn *WebsocketNetwork) GetPeers(options ...PeerOption) []Peer { var addrs []string addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryArchiverRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(), "" /*origin address*/) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersConnectedIn: @@ -718,12 +722,17 @@ func (wn *WebsocketNetwork) Start() { } } } - // if the network has a public address, use that as the name for connection deduplication - if wn.config.PublicAddress != "" { + // if the network has a public address or a libp2p peer ID, use that as the name for connection deduplication + if wn.config.PublicAddress != "" || (wn.peerID != "" && wn.peerIDSigner != nil) { wn.RegisterHandlers(identityHandlers) } - if wn.identityScheme == nil && wn.config.PublicAddress != "" { - wn.identityScheme = NewIdentityChallengeScheme(wn.config.PublicAddress) + if wn.identityScheme == nil { + if wn.peerID != "" && wn.peerIDSigner != nil { + wn.identityScheme = NewIdentityChallengeSchemeWithSigner(string(wn.peerID), wn.peerIDSigner) + } + if wn.config.PublicAddress != "" { + wn.identityScheme = NewIdentityChallengeScheme(wn.config.PublicAddress) + } } wn.meshUpdateRequests <- meshRequest{false, nil} @@ -1074,7 +1083,7 @@ func (wn *WebsocketNetwork) ServeHTTP(response http.ResponseWriter, request *htt } peer := &wsPeer{ - wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, trackedRequest.otherPublicAddr, wn.GetRoundTripper(), trackedRequest.remoteHost), + wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, trackedRequest.otherPublicAddr, wn.GetRoundTripper(nil), trackedRequest.remoteHost), conn: wsPeerWebsocketConnImpl{conn}, outgoing: false, InstanceName: trackedRequest.otherInstanceName, @@ -2009,7 +2018,7 @@ func (wn *WebsocketNetwork) numOutgoingPending() int { // GetRoundTripper returns an http.Transport that limits the number of connection // to comply with connectionsRateLimitingCount. -func (wn *WebsocketNetwork) GetRoundTripper() http.RoundTripper { +func (wn *WebsocketNetwork) GetRoundTripper(peer Peer) http.RoundTripper { return &wn.transport } @@ -2148,7 +2157,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { } peer := &wsPeer{ - wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(), "" /* origin */), + wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(nil), "" /* origin */), conn: wsPeerWebsocketConnImpl{conn}, outgoing: true, incomingMsgFilter: wn.incomingMsgFilter, @@ -2237,7 +2246,7 @@ func (wn *WebsocketNetwork) SetPeerData(peer Peer, key string, value interface{} } // NewWebsocketNetwork constructor for websockets based gossip network -func NewWebsocketNetwork(log logging.Logger, config config.Local, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID, nodeInfo NodeInfo) (wn *WebsocketNetwork, err error) { +func NewWebsocketNetwork(log logging.Logger, config config.Local, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID, nodeInfo NodeInfo, peerID p2p.PeerID, idSigner identityChallengeSigner) (wn *WebsocketNetwork, err error) { phonebook := MakePhonebook(config.ConnectionsRateLimitingCount, time.Duration(config.ConnectionsRateLimitingWindowSeconds)*time.Second) phonebook.AddPersistentPeers(phonebookAddresses, string(networkID), PhoneBookEntryRelayRole) @@ -2248,6 +2257,8 @@ func NewWebsocketNetwork(log logging.Logger, config config.Local, phonebookAddre GenesisID: genesisID, NetworkID: networkID, nodeInfo: nodeInfo, + peerID: peerID, + peerIDSigner: idSigner, resolveSRVRecords: tools_network.ReadFromSRV, } @@ -2257,7 +2268,7 @@ func NewWebsocketNetwork(log logging.Logger, config config.Local, phonebookAddre // NewWebsocketGossipNode constructs a websocket network node and returns it as a GossipNode interface implementation func NewWebsocketGossipNode(log logging.Logger, config config.Local, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID) (gn GossipNode, err error) { - return NewWebsocketNetwork(log, config, phonebookAddresses, genesisID, networkID, nil) + return NewWebsocketNetwork(log, config, phonebookAddresses, genesisID, networkID, nil, "", nil) } // SetPrioScheme specifies the network priority scheme for a network node @@ -2478,7 +2489,5 @@ func (wn *WebsocketNetwork) postMessagesOfInterestThread() { } } -// SubstituteGenesisID substitutes the "{genesisID}" with their network-specific genesisID. -func (wn *WebsocketNetwork) SubstituteGenesisID(rawURL string) string { - return strings.Replace(rawURL, "{genesisID}", wn.GenesisID, -1) -} +// GetGenesisID returns the network-specific genesisID. +func (wn *WebsocketNetwork) GetGenesisID() string { return wn.GenesisID } diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index 445ede3dc3..710ac3f532 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -1715,7 +1715,7 @@ func TestPeeringWithBadIdentityChallenge(t *testing.T) { attachChallenge: func(attach http.Header, addr string) identityChallengeValue { s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys c := identityChallenge{ - Key: s.identityKeys.SignatureVerifier, + Key: s.identityKeys.PublicKey(), Challenge: newIdentityChallengeValue(), PublicAddress: []byte("incorrect address!"), } @@ -1733,7 +1733,7 @@ func TestPeeringWithBadIdentityChallenge(t *testing.T) { attachChallenge: func(attach http.Header, addr string) identityChallengeValue { s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys c := identityChallenge{ - Key: s.identityKeys.SignatureVerifier, + Key: s.identityKeys.PublicKey(), Challenge: newIdentityChallengeValue(), PublicAddress: []byte("incorrect address!"), }.Sign(s.identityKeys) @@ -1853,7 +1853,7 @@ func TestPeeringWithBadIdentityChallengeResponse(t *testing.T) { protocol.Decode(msg, &idChal) // make the response object, with an incorrect challenge encode it and attach it to the header r := identityChallengeResponse{ - Key: s.identityKeys.SignatureVerifier, + Key: s.identityKeys.PublicKey(), Challenge: newIdentityChallengeValue(), ResponseChallenge: newIdentityChallengeValue(), } @@ -1876,7 +1876,7 @@ func TestPeeringWithBadIdentityChallengeResponse(t *testing.T) { protocol.Decode(msg, &idChal) // make the response object, then change the signature and encode and attach r := identityChallengeResponse{ - Key: s.identityKeys.SignatureVerifier, + Key: s.identityKeys.PublicKey(), Challenge: newIdentityChallengeValue(), ResponseChallenge: newIdentityChallengeValue(), }.Sign(s.identityKeys) diff --git a/network/wsPeer.go b/network/wsPeer.go index 9daf7b0ece..5cd0cc2b1c 100644 --- a/network/wsPeer.go +++ b/network/wsPeer.go @@ -364,6 +364,10 @@ func (wp *wsPeerCore) GetHTTPClient() *http.Client { return &wp.client } +func (wp *wsPeerCore) GetNetwork() GossipNode { + return wp.net +} + // Version returns the matching version from network.SupportedProtocolVersions func (wp *wsPeer) Version() string { return wp.version diff --git a/node/follower_node.go b/node/follower_node.go index 66790b1291..5d0887b61f 100644 --- a/node/follower_node.go +++ b/node/follower_node.go @@ -94,7 +94,7 @@ func MakeFollower(log logging.Logger, rootDir string, cfg config.Local, phoneboo node.config = cfg // tie network, block fetcher, and agreement services together - p2pNode, err := network.NewWebsocketNetwork(node.log, node.config, phonebookAddresses, genesis.ID(), genesis.Network, nil) + p2pNode, err := network.NewWebsocketNetwork(node.log, node.config, phonebookAddresses, genesis.ID(), genesis.Network, nil, "", nil) if err != nil { log.Errorf("could not create websocket node: %v", err) return nil, err diff --git a/node/node.go b/node/node.go index b637eda32e..0241fcda5a 100644 --- a/node/node.go +++ b/node/node.go @@ -211,7 +211,13 @@ func MakeFull(log logging.Logger, rootDir string, cfg config.Local, phonebookAdd // tie network, block fetcher, and agreement services together var p2pNode network.GossipNode - if cfg.EnableP2P { + if cfg.EnableP2PHybridMode { + p2pNode, err = network.NewHybridP2PNetwork(node.log, node.config, node.genesisDirs.RootGenesisDir, phonebookAddresses, genesis.ID(), genesis.Network, node) + if err != nil { + log.Errorf("could not create hybrid p2p node: %v", err) + return nil, err + } + } else if cfg.EnableP2P { // TODO: pass more appropriate genesisDir (hot/cold). Presently this is just used to store a peerID key. p2pNode, err = network.NewP2PNetwork(node.log, node.config, node.genesisDirs.RootGenesisDir, phonebookAddresses, genesis.ID(), genesis.Network) if err != nil { @@ -220,7 +226,7 @@ func MakeFull(log logging.Logger, rootDir string, cfg config.Local, phonebookAdd } } else { var wsNode *network.WebsocketNetwork - wsNode, err = network.NewWebsocketNetwork(node.log, node.config, phonebookAddresses, genesis.ID(), genesis.Network, node) + wsNode, err = network.NewWebsocketNetwork(node.log, node.config, phonebookAddresses, genesis.ID(), genesis.Network, node, "", nil) if err != nil { log.Errorf("could not create websocket node: %v", err) return nil, err diff --git a/rpcs/blockService.go b/rpcs/blockService.go index a3bf886f2b..91bdceea09 100644 --- a/rpcs/blockService.go +++ b/rpcs/blockService.go @@ -497,7 +497,7 @@ func RawBlockBytes(l LedgerForBlockService, round basics.Round) ([]byte, error) // FormatBlockQuery formats a block request query for the given network and round number func FormatBlockQuery(round uint64, parsedURL string, net network.GossipNode) string { - return net.SubstituteGenesisID(path.Join(parsedURL, "/v1/{genesisID}/block/"+strconv.FormatUint(uint64(round), 36))) + return network.SubstituteGenesisID(net, path.Join(parsedURL, "/v1/{genesisID}/block/"+strconv.FormatUint(uint64(round), 36))) } func makeFallbackEndpoints(log logging.Logger, customFallbackEndpoints string) (fe fallbackEndpoints) { diff --git a/rpcs/blockService_test.go b/rpcs/blockService_test.go index 832b59c55b..5c339ec895 100644 --- a/rpcs/blockService_test.go +++ b/rpcs/blockService_test.go @@ -70,6 +70,10 @@ func (mup *mockUnicastPeer) Respond(ctx context.Context, reqMsg network.Incoming return nil } +func (mup *mockUnicastPeer) GetNetwork() network.GossipNode { + panic("not implemented") +} + // TestHandleCatchupReqNegative covers the error reporting in handleCatchupReq func TestHandleCatchupReqNegative(t *testing.T) { partitiontest.PartitionTest(t) @@ -141,6 +145,8 @@ func TestRedirectFallbackArchiver(t *testing.T) { net1 := &httpTestPeerSource{} net2 := &httpTestPeerSource{} + net1.GenesisID = "test-genesis-ID" + net2.GenesisID = "test-genesis-ID" config := config.GetDefaultLocal() // Need to enable block service fallbacks @@ -249,6 +255,8 @@ func TestRedirectFallbackEndpoints(t *testing.T) { net1 := &httpTestPeerSource{} net2 := &httpTestPeerSource{} + net1.GenesisID = "test-genesis-ID" + net2.GenesisID = "test-genesis-ID" nodeA := &basicRPCNode{} nodeB := &basicRPCNode{} @@ -261,8 +269,8 @@ func TestRedirectFallbackEndpoints(t *testing.T) { // Set the first to a bad address, the second to self, and the third to the one that has the block. // If RR is right, should succeed. config.BlockServiceCustomFallbackEndpoints = fmt.Sprintf("://badaddress,%s,%s", nodeA.rootURL(), nodeB.rootURL()) - bs1 := MakeBlockService(log, config, ledger1, net1, "{genesisID}") - bs2 := MakeBlockService(log, config, ledger2, net2, "{genesisID}") + bs1 := MakeBlockService(log, config, ledger1, net1, "test-genesis-ID") + bs2 := MakeBlockService(log, config, ledger2, net2, "test-genesis-ID") nodeA.RegisterHTTPHandler(BlockServiceBlockPath, bs1) nodeB.RegisterHTTPHandler(BlockServiceBlockPath, bs2) @@ -312,6 +320,8 @@ func TestRedirectOnFullCapacity(t *testing.T) { net1 := &httpTestPeerSource{} net2 := &httpTestPeerSource{} + net1.GenesisID = "test-genesis-ID" + net2.GenesisID = "test-genesis-ID" config := config.GetDefaultLocal() // Need to enable block service fallbacks @@ -490,12 +500,13 @@ func TestRedirectExceptions(t *testing.T) { addBlock(t, ledger1) net1 := &httpTestPeerSource{} + net1.GenesisID = "test-genesis-ID" config := config.GetDefaultLocal() // Need to enable block service fallbacks config.EnableBlockServiceFallbackToArchiver = true - bs1 := MakeBlockService(log, config, ledger1, net1, "{genesisID}") + bs1 := MakeBlockService(log, config, ledger1, net1, "test-genesis-ID") nodeA := &basicRPCNode{} diff --git a/rpcs/httpTxSync.go b/rpcs/httpTxSync.go index 6f42eeaacf..13d476ad77 100644 --- a/rpcs/httpTxSync.go +++ b/rpcs/httpTxSync.go @@ -107,14 +107,14 @@ func (hts *HTTPTxSync) Sync(ctx context.Context, bloom *bloom.Filter) (txgroups client := hpeer.GetHTTPClient() if client == nil { client = &http.Client{} - client.Transport = hts.peers.GetRoundTripper() + client.Transport = hts.peers.GetRoundTripper(peer) } parsedURL, err := network.ParseHostOrURL(hts.rootURL) if err != nil { hts.log.Warnf("txSync bad url %v: %s", hts.rootURL, err) return nil, err } - parsedURL.Path = hts.peers.SubstituteGenesisID(path.Join(parsedURL.Path, TxServiceHTTPPath)) + parsedURL.Path = network.SubstituteGenesisID(hts.peers, path.Join(parsedURL.Path, TxServiceHTTPPath)) syncURL := parsedURL.String() hts.log.Infof("http sync from %s", syncURL) params := url.Values{} diff --git a/rpcs/txService_test.go b/rpcs/txService_test.go index 8ef49e45a6..1c28ea80ee 100644 --- a/rpcs/txService_test.go +++ b/rpcs/txService_test.go @@ -22,7 +22,6 @@ import ( "net/http" "net/url" "os" - "strings" "sync" "testing" "time" @@ -116,9 +115,7 @@ func (b *basicRPCNode) GetPeers(options ...network.PeerOption) []network.Peer { return b.peers } -func (b *basicRPCNode) SubstituteGenesisID(rawURL string) string { - return strings.Replace(rawURL, "{genesisID}", "test genesisID", -1) -} +func (b *basicRPCNode) GetGenesisID() string { return "test genesisID" } func nodePair() (*basicRPCNode, *basicRPCNode) { nodeA := &basicRPCNode{} diff --git a/rpcs/txSyncer_test.go b/rpcs/txSyncer_test.go index 43e85f4523..6d8928c3fe 100644 --- a/rpcs/txSyncer_test.go +++ b/rpcs/txSyncer_test.go @@ -22,7 +22,6 @@ import ( "math/rand" "net/http" "net/rpc" - "strings" "sync/atomic" "testing" "time" @@ -170,9 +169,6 @@ type mockClientAggregator struct { func (mca *mockClientAggregator) GetPeers(options ...network.PeerOption) []network.Peer { return mca.peers } -func (mca *mockClientAggregator) SubstituteGenesisID(rawURL string) string { - return strings.Replace(rawURL, "{genesisID}", "test genesisID", -1) -} const numberOfPeers = 10 @@ -283,7 +279,7 @@ func TestSync(t *testing.T) { runner := mockRunner{failWithNil: false, failWithError: false, txgroups: pool.PendingTxGroups()[len(pool.PendingTxGroups())-1:], done: make(chan *rpc.Call)} client := mockRPCClient{client: &runner, rootURL: nodeAURL, log: logging.TestingLog(t)} - clientAgg := mockClientAggregator{peers: []network.Peer{&client}} + clientAgg := mockClientAggregator{peers: []network.Peer{&client}, MockNetwork: mocks.MockNetwork{GenesisID: "test genesisID"}} handler := mockHandler{} syncerPool := makeMockPendingTxAggregate(3) syncer := MakeTxSyncer(syncerPool, &clientAgg, &handler, testSyncInterval, testSyncTimeout, config.GetDefaultLocal().TxSyncServeResponseSize) @@ -322,7 +318,7 @@ func TestStartAndStop(t *testing.T) { runner := mockRunner{failWithNil: false, failWithError: false, txgroups: pool.PendingTxGroups()[len(pool.PendingTxGroups())-1:], done: make(chan *rpc.Call)} client := mockRPCClient{client: &runner, rootURL: nodeAURL, log: logging.TestingLog(t)} - clientAgg := mockClientAggregator{peers: []network.Peer{&client}} + clientAgg := mockClientAggregator{peers: []network.Peer{&client}, MockNetwork: mocks.MockNetwork{GenesisID: "test genesisID"}} handler := mockHandler{} syncerPool := makeMockPendingTxAggregate(0) diff --git a/test/testdata/configs/config-v31.json b/test/testdata/configs/config-v31.json index b4de6dd5df..1179a5b629 100644 --- a/test/testdata/configs/config-v31.json +++ b/test/testdata/configs/config-v31.json @@ -55,6 +55,7 @@ "EnableMetricReporting": false, "EnableOutgoingNetworkMessageFiltering": true, "EnableP2P": false, + "EnableP2PHybridMode": false, "EnablePingHandler": true, "EnableProcessBlockStats": false, "EnableProfiler": false, @@ -96,6 +97,7 @@ "OptimizeAccountsDatabaseOnStartup": false, "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, + "P2PListenAddress": "", "P2PPersistPeerID": false, "P2PPrivateKeyLocation": "", "ParticipationKeysRefreshInterval": 60000000000, From 51a1b9d2779bdffd5b3e1157fa331d5383b829c9 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:04:40 -0500 Subject: [PATCH 05/38] p2p: fix dht-hybrid merge (#5833) --- network/p2p/p2p.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index d4694a874f..52f23c0802 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -130,7 +130,7 @@ func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, data streams: sm, pubsub: ps, pubsubCtx: ctx, - privKey: privKey, + privKey: pstore.PrivKey(h.ID()), topics: make(map[string]*pubsub.Topic), }, nil } From 5fef1a9500a6846b729e776340828e899a177adf Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Fri, 17 Nov 2023 14:28:36 -0500 Subject: [PATCH 06/38] move new config opts to v33 --- config/localTemplate.go | 6 +++--- test/testdata/configs/config-v30.json | 1 - test/testdata/configs/config-v31.json | 3 --- test/testdata/configs/config-v33.json | 3 +++ 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/config/localTemplate.go b/config/localTemplate.go index 15af0be015..869fd653c3 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -604,10 +604,10 @@ type Local struct { EnableP2P bool `version[31]:"false"` // EnableP2PHybridMode turns on both websockets and P2P networking. - EnableP2PHybridMode bool `version[31]:"false"` + EnableP2PHybridMode bool `version[33]:"false"` // P2PListenAddress sets the listen address used for P2P networking, if hybrid mode is set. - P2PListenAddress string `version[31]:""` + P2PListenAddress string `version[33]:""` // P2PPersistPeerID will write the private key used for the node's PeerID to the P2PPrivateKeyLocation. // This is only used when P2PEnable is true. If P2PPrivateKey is not specified, it uses the default location. @@ -622,7 +622,7 @@ type Local struct { DisableAPIAuth bool `version[30]:"false"` // EnableDHT will turn on the hash table for use with capabilities advertisement - EnableDHTProviders bool `version[30]:"false"` + EnableDHTProviders bool `version[33]:"false"` } // DNSBootstrapArray returns an array of one or more DNS Bootstrap identifiers diff --git a/test/testdata/configs/config-v30.json b/test/testdata/configs/config-v30.json index 1c71a55563..5021c8fd40 100644 --- a/test/testdata/configs/config-v30.json +++ b/test/testdata/configs/config-v30.json @@ -41,7 +41,6 @@ "EnableBlockService": false, "EnableBlockServiceFallbackToArchiver": true, "EnableCatchupFromArchiveServers": false, - "EnableDHTProviders": false, "EnableDeveloperAPI": false, "EnableExperimentalAPI": false, "EnableFollowMode": false, diff --git a/test/testdata/configs/config-v31.json b/test/testdata/configs/config-v31.json index 1179a5b629..fccf558c44 100644 --- a/test/testdata/configs/config-v31.json +++ b/test/testdata/configs/config-v31.json @@ -45,7 +45,6 @@ "EnableBlockService": false, "EnableBlockServiceFallbackToArchiver": false, "EnableCatchupFromArchiveServers": false, - "EnableDHTProviders": false, "EnableDeveloperAPI": false, "EnableExperimentalAPI": false, "EnableFollowMode": false, @@ -55,7 +54,6 @@ "EnableMetricReporting": false, "EnableOutgoingNetworkMessageFiltering": true, "EnableP2P": false, - "EnableP2PHybridMode": false, "EnablePingHandler": true, "EnableProcessBlockStats": false, "EnableProfiler": false, @@ -97,7 +95,6 @@ "OptimizeAccountsDatabaseOnStartup": false, "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, - "P2PListenAddress": "", "P2PPersistPeerID": false, "P2PPrivateKeyLocation": "", "ParticipationKeysRefreshInterval": 60000000000, diff --git a/test/testdata/configs/config-v33.json b/test/testdata/configs/config-v33.json index aa1cb71712..f2fdcce0e3 100644 --- a/test/testdata/configs/config-v33.json +++ b/test/testdata/configs/config-v33.json @@ -45,6 +45,7 @@ "EnableBlockService": false, "EnableBlockServiceFallbackToArchiver": false, "EnableCatchupFromArchiveServers": false, + "EnableDHTProviders": false, "EnableDeveloperAPI": false, "EnableExperimentalAPI": false, "EnableFollowMode": false, @@ -55,6 +56,7 @@ "EnableMetricReporting": false, "EnableOutgoingNetworkMessageFiltering": true, "EnableP2P": false, + "EnableP2PHybridMode": false, "EnablePingHandler": true, "EnableProcessBlockStats": false, "EnableProfiler": false, @@ -97,6 +99,7 @@ "OptimizeAccountsDatabaseOnStartup": false, "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, + "P2PListenAddress": "", "P2PPersistPeerID": false, "P2PPrivateKeyLocation": "", "ParticipationKeysRefreshInterval": 60000000000, From 1de159521540f88e3f643109d318ed031271d31c Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:15:53 -0500 Subject: [PATCH 07/38] p2p: start p2p networking and DHT ops when starting services, not when instantiating them (#5867) --- agreement/fuzzer/networkFacade_test.go | 2 +- agreement/gossip/network_test.go | 4 +- components/mocks/mockNetwork.go | 3 +- daemon/algod/server.go | 11 +- network/gossipNode.go | 2 +- network/hybridNetwork.go | 12 +- network/p2p/capabilities.go | 28 +-- network/p2p/capabilities_test.go | 22 ++- network/p2p/dht/dht.go | 46 +---- network/p2p/dht/dht_test.go | 46 +---- network/p2p/p2p.go | 65 ++++--- network/p2pNetwork.go | 107 +++++++++++- network/p2pNetwork_test.go | 226 +++++++++++++++++++++++-- network/wsNetwork.go | 12 +- network/wsNetwork_test.go | 4 + node/follower_node.go | 20 ++- node/node.go | 41 ++--- tools/debug/algodump/main.go | 6 +- tools/debug/transplanter/main.go | 6 +- 19 files changed, 467 insertions(+), 196 deletions(-) diff --git a/agreement/fuzzer/networkFacade_test.go b/agreement/fuzzer/networkFacade_test.go index 35eba4a273..77f58de3bf 100644 --- a/agreement/fuzzer/networkFacade_test.go +++ b/agreement/fuzzer/networkFacade_test.go @@ -247,7 +247,7 @@ func (n *NetworkFacade) PushDownstreamMessage(newMsg context.CancelFunc) bool { func (n *NetworkFacade) Address() (string, bool) { return "mock network", true } // Start - unused function -func (n *NetworkFacade) Start() {} +func (n *NetworkFacade) Start() error { return nil } // Stop - unused function func (n *NetworkFacade) Stop() {} diff --git a/agreement/gossip/network_test.go b/agreement/gossip/network_test.go index 3607afb168..fc6d4b032b 100644 --- a/agreement/gossip/network_test.go +++ b/agreement/gossip/network_test.go @@ -160,7 +160,7 @@ func (w *whiteholeNetwork) GetHTTPRequestConnection(request *http.Request) (conn return nil } -func (w *whiteholeNetwork) Start() { +func (w *whiteholeNetwork) Start() error { w.quit = make(chan struct{}) go func(w *whiteholeNetwork) { w.domain.messagesMu.Lock() @@ -216,7 +216,7 @@ func (w *whiteholeNetwork) Start() { atomic.AddUint32(&w.lastMsgRead, 1) } }(w) - return + return nil } func (w *whiteholeNetwork) getMux() *network.Multiplexer { return w.mux diff --git a/components/mocks/mockNetwork.go b/components/mocks/mockNetwork.go index e1ceb63142..39b39054fe 100644 --- a/components/mocks/mockNetwork.go +++ b/components/mocks/mockNetwork.go @@ -47,7 +47,8 @@ func (network *MockNetwork) Address() (string, bool) { } // Start - unused function -func (network *MockNetwork) Start() { +func (network *MockNetwork) Start() error { + return nil } // Stop - unused function diff --git a/daemon/algod/server.go b/daemon/algod/server.go index 1b40e98bfb..0dc5fcafab 100644 --- a/daemon/algod/server.go +++ b/daemon/algod/server.go @@ -56,7 +56,7 @@ const maxHeaderBytes = 4096 type ServerNode interface { apiServer.APINodeInterface ListeningAddress() (string, bool) - Start() + Start() error Stop() } @@ -272,7 +272,13 @@ func makeListener(addr string) (net.Listener, error) { func (s *Server) Start() { s.log.Info("Trying to start an Algorand node") fmt.Print("Initializing the Algorand node... ") - s.node.Start() + err := s.node.Start() + if err != nil { + msg := fmt.Sprintf("Failed to start alg Algorand node: %v", err) + s.log.Error(msg) + fmt.Println(msg) + os.Exit(1) + } s.log.Info("Successfully started an Algorand node.") fmt.Println("Success!") @@ -291,7 +297,6 @@ func (s *Server) Start() { } var apiToken string - var err error fmt.Printf("API authentication disabled: %v\n", cfg.DisableAPIAuth) if !cfg.DisableAPIAuth { apiToken, err = tokens.GetAndValidateAPIToken(s.RootPath, tokens.AlgodTokenFilename) diff --git a/network/gossipNode.go b/network/gossipNode.go index 8d1b877449..56f285cb06 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -72,7 +72,7 @@ type GossipNode interface { GetPeers(options ...PeerOption) []Peer // Start threads, listen on sockets. - Start() + Start() error // Close sockets. Stop threads. Stop() diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go index c09c5d4a1a..b43082eabb 100644 --- a/network/hybridNetwork.go +++ b/network/hybridNetwork.go @@ -42,7 +42,7 @@ func NewHybridP2PNetwork(log logging.Logger, cfg config.Local, datadir string, p // supply alternate NetAddress for P2P network p2pcfg := cfg p2pcfg.NetAddress = cfg.P2PListenAddress - p2pnet, err := NewP2PNetwork(log, p2pcfg, datadir, phonebookAddresses, genesisID, networkID) + p2pnet, err := NewP2PNetwork(log, p2pcfg, datadir, phonebookAddresses, genesisID, networkID, nodeInfo) if err != nil { return nil, err } @@ -153,17 +153,17 @@ func (n *HybridP2PNetwork) GetPeers(options ...PeerOption) []Peer { } // Start implements GossipNode -func (n *HybridP2PNetwork) Start() { - _ = n.runParallel(func(net GossipNode) error { - net.Start() - return nil +func (n *HybridP2PNetwork) Start() error { + err := n.runParallel(func(net GossipNode) error { + return net.Start() }) + return err } // Stop implements GossipNode func (n *HybridP2PNetwork) Stop() { _ = n.runParallel(func(net GossipNode) error { - net.Start() + net.Stop() return nil }) } diff --git a/network/p2p/capabilities.go b/network/p2p/capabilities.go index 149d643da1..502383b4ee 100644 --- a/network/p2p/capabilities.go +++ b/network/p2p/capabilities.go @@ -30,7 +30,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" algoDht "github.com/algorand/go-algorand/network/p2p/dht" - "github.com/algorand/go-algorand/network/p2p/peerstore" + "github.com/algorand/go-algorand/protocol" ) // Capability represents functions that some nodes may provide and other nodes would want to know about @@ -88,7 +88,9 @@ func (c *CapabilitiesDiscovery) PeersForCapability(capability Capability, n int) ctx, cancel := context.WithTimeout(context.Background(), operationTimeout) defer cancel() var peers []peer.AddrInfo - peersChan, err := c.FindPeers(ctx, string(capability), discovery.Limit(n)) + // +1 because it can include self but we exclude self from the returned list + // that might confuse the caller (and tests assertions) + peersChan, err := c.FindPeers(ctx, string(capability), discovery.Limit(n+1)) if err != nil { return nil, err } @@ -146,17 +148,19 @@ func (c *CapabilitiesDiscovery) AdvertiseCapabilities(capabilities ...Capability }() } +// Sizer exposes the Size method +type Sizer interface { + Size() int +} + +// RoutingTable exposes some knowledge about the DHT routing table +func (c *CapabilitiesDiscovery) RoutingTable() Sizer { + return c.dht.RoutingTable() +} + // MakeCapabilitiesDiscovery creates a new CapabilitiesDiscovery object which exposes peer discovery and capabilities advertisement -func MakeCapabilitiesDiscovery(ctx context.Context, cfg config.Local, datadir string, network string, log logging.Logger, bootstrapPeers []*peer.AddrInfo) (*CapabilitiesDiscovery, error) { - pstore, err := peerstore.NewPeerStore(bootstrapPeers) - if err != nil { - return nil, err - } - h, err := makeHost(cfg, datadir, pstore) - if err != nil { - return nil, err - } - discDht, err := algoDht.MakeDHT(ctx, h, network, cfg, bootstrapPeers) +func MakeCapabilitiesDiscovery(ctx context.Context, cfg config.Local, h host.Host, networkID protocol.NetworkID, log logging.Logger, bootstrapFunc func() []peer.AddrInfo) (*CapabilitiesDiscovery, error) { + discDht, err := algoDht.MakeDHT(ctx, h, networkID, cfg, bootstrapFunc) if err != nil { return nil, err } diff --git a/network/p2p/capabilities_test.go b/network/p2p/capabilities_test.go index 82fee84d03..ca3de978b7 100644 --- a/network/p2p/capabilities_test.go +++ b/network/p2p/capabilities_test.go @@ -47,7 +47,11 @@ func TestCapabilities_Discovery(t *testing.T) { testSize := 3 for i := 0; i < testSize; i++ { tempdir := t.TempDir() - capD, err := MakeCapabilitiesDiscovery(context.Background(), config.GetDefaultLocal(), tempdir, "devtestnet", logging.Base(), []*peer.AddrInfo{}) + ps, err := peerstore.NewPeerStore(nil) + require.NoError(t, err) + h, _, err := MakeHost(config.GetDefaultLocal(), tempdir, ps) + require.NoError(t, err) + capD, err := MakeCapabilitiesDiscovery(context.Background(), config.GetDefaultLocal(), h, "devtestnet", logging.Base(), func() []peer.AddrInfo { return nil }) require.NoError(t, err) caps = append(caps, capD) addrs = append(addrs, peer.AddrInfo{ @@ -72,7 +76,7 @@ func TestCapabilities_Discovery(t *testing.T) { func setupDHTHosts(t *testing.T, numHosts int) []*dht.IpfsDHT { var hosts []host.Host - var bootstrapPeers []*peer.AddrInfo + var bootstrapPeers []peer.AddrInfo var dhts []*dht.IpfsDHT cfg := config.GetDefaultLocal() for i := 0; i < numHosts; i++ { @@ -87,10 +91,10 @@ func setupDHTHosts(t *testing.T, numHosts int) []*dht.IpfsDHT { libp2p.Peerstore(ps)) require.NoError(t, err) hosts = append(hosts, h) - bootstrapPeers = append(bootstrapPeers, &peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}) + bootstrapPeers = append(bootstrapPeers, peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}) } for _, h := range hosts { - ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, bootstrapPeers) + ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, func() []peer.AddrInfo { return bootstrapPeers }) require.NoError(t, err) err = ht.Bootstrap(context.Background()) require.NoError(t, err) @@ -117,7 +121,7 @@ func waitForRouting(t *testing.T, disc *CapabilitiesDiscovery) { func setupCapDiscovery(t *testing.T, numHosts int, numBootstrapPeers int) []*CapabilitiesDiscovery { var hosts []host.Host - var bootstrapPeers []*peer.AddrInfo + var bootstrapPeers []peer.AddrInfo var capsDisc []*CapabilitiesDiscovery cfg := config.GetDefaultLocal() for i := 0; i < numHosts; i++ { @@ -132,17 +136,19 @@ func setupCapDiscovery(t *testing.T, numHosts int, numBootstrapPeers int) []*Cap libp2p.Peerstore(ps)) require.NoError(t, err) hosts = append(hosts, h) - bootstrapPeers = append(bootstrapPeers, &peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}) + bootstrapPeers = append(bootstrapPeers, peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}) } for _, h := range hosts { bp := bootstrapPeers if numBootstrapPeers != 0 && numBootstrapPeers != numHosts { + bp = make([]peer.AddrInfo, len(bootstrapPeers)) + copy(bp, bootstrapPeers) rand.Shuffle(len(bootstrapPeers), func(i, j int) { bp[i], bp[j] = bp[j], bp[i] }) bp = bp[:numBootstrapPeers] } - ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, bp) + ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, func() []peer.AddrInfo { return bp }) require.NoError(t, err) disc, err := algodht.MakeDiscovery(ht) require.NoError(t, err) @@ -304,7 +310,7 @@ func TestCapabilities_ExcludesSelf(t *testing.T) { disc := setupCapDiscovery(t, 2, 2) testPeersFound := func(disc *CapabilitiesDiscovery, n int, cap Capability) bool { - peers, err := disc.PeersForCapability(cap, n+1) + peers, err := disc.PeersForCapability(cap, n) if err == nil && len(peers) == n { return true } diff --git a/network/p2p/dht/dht.go b/network/p2p/dht/dht.go index 9266605af6..4b55e7e4f7 100644 --- a/network/p2p/dht/dht.go +++ b/network/p2p/dht/dht.go @@ -32,7 +32,6 @@ import ( "github.com/libp2p/go-libp2p/p2p/discovery/routing" "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/network/p2p/dnsaddr" algoproto "github.com/algorand/go-algorand/protocol" ) @@ -40,54 +39,19 @@ const minBackoff = time.Second * 5 const maxBackoff = time.Second * 20 const baseBackoff = float64(1.1) -// getBootstrapPeersFunc looks up a list of Multiaddrs strings from the dnsaddr records at the primary -// SRV record domain. -func getBootstrapPeersFunc(cfg config.Local, network string) func() []peer.AddrInfo { - return func() []peer.AddrInfo { - var addrs []peer.AddrInfo - bootstraps := cfg.DNSBootstrapArray(algoproto.NetworkID(network)) - for _, dnsBootstrap := range bootstraps { - controller := dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecuritySRVEnforced(), "") - resolvedAddrs, err := dnsaddr.MultiaddrsFromResolver(dnsBootstrap.PrimarySRVBootstrap, controller) - if err != nil { - continue - } - for _, resolvedAddr := range resolvedAddrs { - info, err0 := peer.AddrInfoFromP2pAddr(resolvedAddr) - if err0 != nil { - continue - } - addrs = append(addrs, *info) - } - } - return addrs - } -} - -func dhtProtocolPrefix(network string) protocol.ID { - return protocol.ID(fmt.Sprintf("/algorand/kad/%s", network)) +func dhtProtocolPrefix(networkID algoproto.NetworkID) protocol.ID { + return protocol.ID(fmt.Sprintf("/algorand/kad/%s", networkID)) } // MakeDHT creates the dht.IpfsDHT object -func MakeDHT(ctx context.Context, h host.Host, network string, cfg config.Local, bootstrapPeers []*peer.AddrInfo) (*dht.IpfsDHT, error) { +func MakeDHT(ctx context.Context, h host.Host, networkID algoproto.NetworkID, cfg config.Local, bootstrapFunc func() []peer.AddrInfo) (*dht.IpfsDHT, error) { dhtCfg := []dht.Option{ // Automatically determine server or client mode dht.Mode(dht.ModeAutoServer), // We don't need the value store right now dht.DisableValues(), - dht.ProtocolPrefix(dhtProtocolPrefix(network)), - } - if len(bootstrapPeers) > 0 { - var peers []peer.AddrInfo - for _, bPeer := range bootstrapPeers { - if bPeer != nil { - peers = append(peers, *bPeer) - } - } - dhtCfg = append(dhtCfg, dht.BootstrapPeers(peers...)) - - } else { - dhtCfg = append(dhtCfg, dht.BootstrapPeersFunc(getBootstrapPeersFunc(cfg, network))) + dht.ProtocolPrefix(dhtProtocolPrefix(networkID)), + dht.BootstrapPeersFunc(bootstrapFunc), } return dht.New(ctx, h, dhtCfg...) } diff --git a/network/p2p/dht/dht_test.go b/network/p2p/dht/dht_test.go index 3d4eabb3bc..93b8b379e5 100644 --- a/network/p2p/dht/dht_test.go +++ b/network/p2p/dht/dht_test.go @@ -39,7 +39,7 @@ func TestDHTBasic(t *testing.T) { h, "devtestnet", config.GetDefaultLocal(), - []*peer.AddrInfo{{}}) + func() []peer.AddrInfo { return nil }) require.NoError(t, err) _, err = MakeDiscovery(dht) require.NoError(t, err) @@ -55,52 +55,10 @@ func TestDHTBasicAlgodev(t *testing.T) { require.NoError(t, err) cfg := config.GetDefaultLocal() cfg.DNSBootstrapID = ".algodev.network" - dht, err := MakeDHT(context.Background(), h, "betanet", cfg, []*peer.AddrInfo{}) + dht, err := MakeDHT(context.Background(), h, "betanet", cfg, func() []peer.AddrInfo { return nil }) require.NoError(t, err) _, err = MakeDiscovery(dht) require.NoError(t, err) err = dht.Bootstrap(context.Background()) require.NoError(t, err) } - -func TestGetBootstrapPeers(t *testing.T) { - t.Parallel() - partitiontest.PartitionTest(t) - - cfg := config.GetDefaultLocal() - cfg.DNSBootstrapID = ".algodev.network" - cfg.DNSSecurityFlags = 0 - - addrs := getBootstrapPeersFunc(cfg, "test")() - - require.GreaterOrEqual(t, len(addrs), 1) - addr := addrs[0] - require.Equal(t, len(addr.Addrs), 1) - require.GreaterOrEqual(t, len(addr.Addrs), 1) -} - -func TestGetBootstrapPeersFailure(t *testing.T) { - t.Parallel() - partitiontest.PartitionTest(t) - - cfg := config.GetDefaultLocal() - cfg.DNSSecurityFlags = 0 - cfg.DNSBootstrapID = "non-existent.algodev.network" - - addrs := getBootstrapPeersFunc(cfg, "test")() - - require.Equal(t, 0, len(addrs)) -} - -func TestGetBootstrapPeersInvalidAddr(t *testing.T) { - t.Parallel() - partitiontest.PartitionTest(t) - - cfg := config.GetDefaultLocal() - cfg.DNSSecurityFlags = 0 - cfg.DNSBootstrapID = ".algodev.network" - - addrs := getBootstrapPeersFunc(cfg, "testInvalidAddr")() - - require.Equal(t, 0, len(addrs)) -} diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 52f23c0802..801a007e51 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -26,6 +26,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-deadlock" + "github.com/multiformats/go-multiaddr" "github.com/libp2p/go-libp2p" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -41,6 +42,7 @@ import ( // Service defines the interface used by the network integrating with underlying p2p implementation type Service interface { + Start() error Close() error ID() peer.ID // return peer.ID for self IDSigner() *PeerIDChallengeSigner @@ -58,12 +60,13 @@ type Service interface { // serviceImpl manages integration with libp2p and implements the Service interface type serviceImpl struct { - log logging.Logger - host host.Host - streams *streamManager - pubsub *pubsub.PubSub - pubsubCtx context.Context - privKey crypto.PrivKey + log logging.Logger + listenAddr string + host host.Host + streams *streamManager + pubsub *pubsub.PubSub + pubsubCtx context.Context + privKey crypto.PrivKey topics map[string]*pubsub.Topic topicsMu deadlock.RWMutex @@ -74,11 +77,13 @@ const AlgorandWsProtocol = "/algorand-ws/1.0.0" const dialTimeout = 30 * time.Second -func makeHost(cfg config.Local, datadir string, pstore peerstore.Peerstore) (host.Host, error) { +// MakeHost creates a libp2p host but does not start listening. +// Use host.Network().Listen() on the returned address to start listening. +func MakeHost(cfg config.Local, datadir string, pstore peerstore.Peerstore) (host.Host, string, error) { // load stored peer ID, or make ephemeral peer ID privKey, err := GetPrivKey(cfg, datadir) if err != nil { - return nil, err + return nil, "", err } // muxer supports tweaking fields from yamux.Config @@ -96,24 +101,26 @@ func makeHost(cfg config.Local, datadir string, pstore peerstore.Peerstore) (hos listenAddr = "/ip4/0.0.0.0/tcp/0" } - return libp2p.New( + // the libp2p.NoListenAddrs builtin disables relays but this one does not + var noListenAddrs = func(cfg *libp2p.Config) error { + cfg.ListenAddrs = []multiaddr.Multiaddr{} + return nil + } + + host, err := libp2p.New( libp2p.Identity(privKey), libp2p.UserAgent(ua), libp2p.Transport(tcp.NewTCPTransport), libp2p.Muxer("/yamux/1.0.0", &ymx), libp2p.Peerstore(pstore), - libp2p.ListenAddrStrings(listenAddr), + noListenAddrs, libp2p.Security(noise.ID, noise.New), ) + return host, listenAddr, err } // MakeService creates a P2P service instance -func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, datadir string, pstore peerstore.Peerstore, wsStreamHandler StreamHandler) (*serviceImpl, error) { - h, err := makeHost(cfg, datadir, pstore) - if err != nil { - return nil, err - } - log.Infof("P2P service started: peer ID %s addrs %s", h.ID(), h.Addrs()) +func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, h host.Host, listenAddr string, wsStreamHandler StreamHandler, bootstrapPeers []*peer.AddrInfo) (*serviceImpl, error) { sm := makeStreamManager(ctx, log, h, wsStreamHandler) h.Network().Notify(sm) @@ -125,16 +132,28 @@ func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, data } return &serviceImpl{ - log: log, - host: h, - streams: sm, - pubsub: ps, - pubsubCtx: ctx, - privKey: pstore.PrivKey(h.ID()), - topics: make(map[string]*pubsub.Topic), + log: log, + listenAddr: listenAddr, + host: h, + streams: sm, + pubsub: ps, + pubsubCtx: ctx, + privKey: h.Peerstore().PrivKey(h.ID()), + topics: make(map[string]*pubsub.Topic), }, nil } +// Close shuts down the P2P service +func (s *serviceImpl) Start() error { + listenAddr, err := multiaddr.NewMultiaddr(s.listenAddr) + if err != nil { + s.log.Errorf("failed to create multiaddress: %s", err) + return err + } + + return s.host.Network().Listen(listenAddr) +} + // Close shuts down the P2P service func (s *serviceImpl) Close() error { return s.host.Close() diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index b5122516ff..4cd293c3f2 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -28,6 +28,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network/p2p" + "github.com/algorand/go-algorand/network/p2p/dnsaddr" "github.com/algorand/go-algorand/network/p2p/peerstore" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-deadlock" @@ -63,6 +64,68 @@ type P2PNetwork struct { wsPeersLock deadlock.RWMutex wsPeersChangeCounter atomic.Int32 wsPeersConnectivityCheckTicker *time.Ticker + + capabilitiesDiscovery *p2p.CapabilitiesDiscovery + + bootstrapper bootstrapper + nodeInfo NodeInfo +} + +type bootstrapper struct { + cfg config.Local + networkID protocol.NetworkID + phonebookPeers []*peer.AddrInfo + started bool +} + +func (b *bootstrapper) start() { + b.started = true +} + +func (b *bootstrapper) stop() { + b.started = false +} + +func (b *bootstrapper) BootstrapFunc() []peer.AddrInfo { + // not started yet, do not give it any peers + if !b.started { + return nil + } + + // have a list of peers, use them + if len(b.phonebookPeers) > 0 { + var addrs []peer.AddrInfo + for _, bPeer := range b.phonebookPeers { + if bPeer != nil { + addrs = append(addrs, *bPeer) + } + } + return addrs + } + + return getBootstrapPeers(b.cfg, b.networkID) +} + +// getBootstrapPeers looks up a list of Multiaddrs strings from the dnsaddr records at the primary +// SRV record domain. +func getBootstrapPeers(cfg config.Local, network protocol.NetworkID) []peer.AddrInfo { + var addrs []peer.AddrInfo + bootstraps := cfg.DNSBootstrapArray(network) + for _, dnsBootstrap := range bootstraps { + controller := dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecuritySRVEnforced(), "") + resolvedAddrs, err := dnsaddr.MultiaddrsFromResolver(dnsBootstrap.PrimarySRVBootstrap, controller) + if err != nil { + continue + } + for _, resolvedAddr := range resolvedAddrs { + info, err0 := peer.AddrInfoFromP2pAddr(resolvedAddr) + if err0 != nil { + continue + } + addrs = append(addrs, *info) + } + } + return addrs } type p2pPeerStats struct { @@ -77,7 +140,7 @@ type gossipSubPeer struct { func (p gossipSubPeer) GetNetwork() GossipNode { return p.net } // NewP2PNetwork returns an instance of GossipNode that uses the p2p.Service -func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID) (*P2PNetwork, error) { +func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID, node NodeInfo) (*P2PNetwork, error) { const readBufferLen = 2048 // create Peerstore and add phonebook addresses @@ -99,6 +162,7 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo wsPeers: make(map[peer.ID]*wsPeer), wsPeersToIDs: make(map[*wsPeer]peer.ID), peerStats: make(map[peer.ID]*p2pPeerStats), + nodeInfo: node, } net.ctx, net.ctxCancel = context.WithCancel(context.Background()) net.handler = msgHandler{ @@ -115,11 +179,32 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo broadcastQueueBulk: make(chan broadcastRequest, 100), } - net.service, err = p2p.MakeService(net.ctx, log, cfg, datadir, pstore, net.wsStreamHandler) + h, la, err := p2p.MakeHost(cfg, datadir, pstore) + if err != nil { + return nil, err + } + log.Infof("P2P host created: peer ID %s addrs %s", h.ID(), h.Addrs()) + + net.service, err = p2p.MakeService(net.ctx, log, cfg, h, la, net.wsStreamHandler, addrInfo) if err != nil { return nil, err } + bootstrapper := &bootstrapper{ + cfg: cfg, + networkID: networkID, + phonebookPeers: addrInfo, + } + + if cfg.EnableDHTProviders { + disc, err0 := p2p.MakeCapabilitiesDiscovery(net.ctx, cfg, h, networkID, net.log, bootstrapper.BootstrapFunc) + if err0 != nil { + log.Errorf("Failed to create dht node capabilities discovery: %v", err) + return nil, err + } + net.capabilitiesDiscovery = disc + } + err = net.setup() if err != nil { return nil, err @@ -146,8 +231,13 @@ func (n *P2PNetwork) PeerIDSigner() identityChallengeSigner { } // Start threads, listen on sockets. -func (n *P2PNetwork) Start() { +func (n *P2PNetwork) Start() error { n.wg.Add(1) + n.bootstrapper.start() + err := n.service.Start() + if err != nil { + return err + } go n.txTopicHandleLoop() if n.wsPeersConnectivityCheckTicker != nil { @@ -166,10 +256,20 @@ func (n *P2PNetwork) Start() { n.wg.Add(1) go n.meshThread() + + if n.capabilitiesDiscovery != nil { + n.capabilitiesDiscovery.AdvertiseCapabilities(n.nodeInfo.Capabilities()...) + } + + return nil } // Stop closes sockets and stop threads. func (n *P2PNetwork) Stop() { + if n.capabilitiesDiscovery != nil { + n.capabilitiesDiscovery.Close() + } + n.handler.ClearHandlers([]Tag{}) if n.wsPeersConnectivityCheckTicker != nil { n.wsPeersConnectivityCheckTicker.Stop() @@ -178,6 +278,7 @@ func (n *P2PNetwork) Stop() { n.innerStop() n.ctxCancel() n.service.Close() + n.bootstrapper.stop() n.wg.Wait() } diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 75fc66b0ad..5ddc9af20e 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -19,6 +19,7 @@ package network import ( "context" "fmt" + "sync" "sync/atomic" "testing" "time" @@ -42,24 +43,24 @@ func TestP2PSubmitTX(t *testing.T) { cfg := config.GetDefaultLocal() log := logging.TestingLog(t) - netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet) + netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, &nopeNodeInfo{}) require.NoError(t, err) - peerInfoA := netA.service.AddrInfo() + netA.Start() + defer netA.Stop() + peerInfoA := netA.service.AddrInfo() addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) require.NoError(t, err) require.NotZero(t, addrsA[0]) - netA.Start() - defer netA.Stop() multiAddrStr := addrsA[0].String() phoneBookAddresses := []string{multiAddrStr} - netB, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet) + netB, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) require.NoError(t, err) netB.Start() defer netB.Stop() - netC, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet) + netC, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) require.NoError(t, err) netC.Start() @@ -116,27 +117,30 @@ func TestP2PSubmitWS(t *testing.T) { cfg := config.GetDefaultLocal() log := logging.TestingLog(t) - netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet) + netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + + err = netA.Start() require.NoError(t, err) + defer netA.Stop() peerInfoA := netA.service.AddrInfo() addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) require.NoError(t, err) require.NotZero(t, addrsA[0]) - netA.Start() - defer netA.Stop() multiAddrStr := addrsA[0].String() phoneBookAddresses := []string{multiAddrStr} - netB, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet) + netB, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + err = netB.Start() require.NoError(t, err) - netB.Start() defer netB.Stop() - netC, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet) - + netC, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + err = netC.Start() require.NoError(t, err) - netC.Start() defer netC.Stop() require.Eventually( @@ -189,6 +193,10 @@ type mockService struct { peers map[peer.ID]peer.AddrInfo } +func (s *mockService) Start() error { + return nil +} + func (s *mockService) Close() error { return nil } @@ -254,7 +262,7 @@ func TestP2PNetworkAddress(t *testing.T) { cfg := config.GetDefaultLocal() log := logging.TestingLog(t) - netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet) + netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, &nopeNodeInfo{}) defer netA.Stop() require.NoError(t, err) addrInfo := netA.service.AddrInfo() @@ -308,3 +316,191 @@ func TestP2PNetworkAddress(t *testing.T) { require.False(t, ok) require.Empty(t, retAddr) } + +func TestBootstrapFunc(t *testing.T) { + t.Parallel() + partitiontest.PartitionTest(t) + + b := bootstrapper{} + require.Nil(t, b.BootstrapFunc()) + + b.started = true + p := peer.AddrInfo{ID: "test"} + b.phonebookPeers = []*peer.AddrInfo{&p} + require.Equal(t, []peer.AddrInfo{p}, b.BootstrapFunc()) + + b.phonebookPeers = nil + + b.cfg = config.GetDefaultLocal() + b.cfg.DNSBootstrapID = ".algodev.network" + b.cfg.DNSSecurityFlags = 0 + b.networkID = "test" + + addrs := b.BootstrapFunc() + + require.GreaterOrEqual(t, len(addrs), 1) + addr := addrs[0] + require.Equal(t, len(addr.Addrs), 1) + require.GreaterOrEqual(t, len(addr.Addrs), 1) +} + +func TestGetBootstrapPeersFailure(t *testing.T) { + t.Parallel() + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.DNSSecurityFlags = 0 + cfg.DNSBootstrapID = "non-existent.algodev.network" + + addrs := getBootstrapPeers(cfg, "test") + + require.Equal(t, 0, len(addrs)) +} + +func TestGetBootstrapPeersInvalidAddr(t *testing.T) { + t.Parallel() + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.DNSSecurityFlags = 0 + cfg.DNSBootstrapID = ".algodev.network" + + addrs := getBootstrapPeers(cfg, "testInvalidAddr") + + require.Equal(t, 0, len(addrs)) +} + +type capNodeInfo struct { + nopeNodeInfo + cap p2p.Capability +} + +func (ni *capNodeInfo) Capabilities() []p2p.Capability { + return []p2p.Capability{ni.cap} +} + +func waitForRouting(t *testing.T, disc *p2p.CapabilitiesDiscovery) { + refreshCtx, refCancel := context.WithTimeout(context.Background(), time.Second*5) + for { + select { + case <-refreshCtx.Done(): + refCancel() + require.Fail(t, "failed to populate routing table before timeout") + default: + if disc.RoutingTable().Size() > 0 { + refCancel() + return + } + } + time.Sleep(50 * time.Millisecond) + } +} + +// TestP2PNetworkDHTCapabilities runs nodes with capabilites and ensures that connected nodes +// can discover themself. The other nodes receive the first node in bootstrap list before starting. +// There is two variations of the test: only netA advertises capabilities, and all nodes advertise. +func TestP2PNetworkDHTCapabilities(t *testing.T) { + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.EnableDHTProviders = true + log := logging.TestingLog(t) + + cap := p2p.Archival + tests := []struct { + name string + nis []NodeInfo + numCapPeers int + }{ + {"cap=all", []NodeInfo{&capNodeInfo{cap: cap}, &capNodeInfo{cap: cap}, &capNodeInfo{cap: cap}}, 2}, // each has 2 peers with capabilities + {"cap=netA", []NodeInfo{&capNodeInfo{cap: cap}, &nopeNodeInfo{}, &nopeNodeInfo{}}, 1}, // each has 1 peer with capabilities + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, test.nis[0]) + require.NoError(t, err) + + err = netA.Start() + require.NoError(t, err) + defer netA.Stop() + + peerInfoA := netA.service.AddrInfo() + addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) + require.NoError(t, err) + require.NotZero(t, addrsA[0]) + + multiAddrStr := addrsA[0].String() + phoneBookAddresses := []string{multiAddrStr} + netB, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, test.nis[1]) + require.NoError(t, err) + err = netB.Start() + require.NoError(t, err) + defer netB.Stop() + + netC, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, test.nis[2]) + require.NoError(t, err) + err = netC.Start() + require.NoError(t, err) + defer netC.Stop() + + require.Eventually( + t, + func() bool { + return len(netA.service.ListPeersForTopic(p2p.TXTopicName)) > 0 && + len(netB.service.ListPeersForTopic(p2p.TXTopicName)) > 0 && + len(netC.service.ListPeersForTopic(p2p.TXTopicName)) > 0 + }, + 2*time.Second, + 50*time.Millisecond, + ) + t.Logf("peers connected") + + discs := []*p2p.CapabilitiesDiscovery{netA.capabilitiesDiscovery, netB.capabilitiesDiscovery, netC.capabilitiesDiscovery} + + var wg sync.WaitGroup + wg.Add(len(discs)) + for _, disc := range discs { + if disc == nil { + wg.Done() + continue + } + go func(disc *p2p.CapabilitiesDiscovery) { + defer wg.Done() + waitForRouting(t, disc) + }(disc) + } + wg.Wait() + + t.Logf("DHT is ready") + + // ensure all peers are connected + for _, disc := range discs { + require.Equal(t, 2, len(disc.Host().Network().Peers())) + } + + wg.Add(len(discs)) + for _, disc := range discs { + go func(disc *p2p.CapabilitiesDiscovery) { + defer wg.Done() + if disc == netA.capabilitiesDiscovery { + return + } + require.Eventuallyf(t, + func() bool { + peers, err := disc.PeersForCapability(cap, test.numCapPeers) + if err == nil && len(peers) == test.numCapPeers { + return true + } + return false + }, + time.Minute, + time.Second, + fmt.Sprintf("Not all expected %s cap peers were found", cap), + ) + }(disc) + } + wg.Wait() + }) + } +} diff --git a/network/wsNetwork.go b/network/wsNetwork.go index a7fab2d174..b5858cb22c 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -156,6 +156,8 @@ const GossipNetworkPath = "/v1/{genesisID}/gossip" type NodeInfo interface { // IsParticipating returns true if this node has stake and may vote on blocks or propose blocks. IsParticipating() bool + // Capabilities returns a list of capabilities this node has. + Capabilities() []p2p.Capability } type nopeNodeInfo struct { @@ -165,6 +167,10 @@ func (nnni *nopeNodeInfo) IsParticipating() bool { return false } +func (nnni *nopeNodeInfo) Capabilities() []p2p.Capability { + return nil +} + // WebsocketNetwork implements GossipNode type WebsocketNetwork struct { listener net.Listener @@ -680,7 +686,7 @@ func (wn *WebsocketNetwork) setup() { } // Start makes network connections and threads -func (wn *WebsocketNetwork) Start() { +func (wn *WebsocketNetwork) Start() error { wn.messagesOfInterestMu.Lock() defer wn.messagesOfInterestMu.Unlock() wn.messagesOfInterestEncoded = true @@ -692,7 +698,7 @@ func (wn *WebsocketNetwork) Start() { listener, err := net.Listen("tcp", wn.config.NetAddress) if err != nil { wn.log.Errorf("network could not listen %v: %s", wn.config.NetAddress, err) - return + return err } // wrap the original listener with a limited connection listener listener = limitlistener.RejectingLimitListener( @@ -768,6 +774,8 @@ func (wn *WebsocketNetwork) Start() { go wn.postMessagesOfInterestThread() wn.log.Infof("serving genesisID=%s on %#v with RandomID=%s", wn.GenesisID, wn.PublicAddress(), wn.RandomID) + + return nil } func (wn *WebsocketNetwork) httpdThread() { diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index 5a07c38de7..368d098961 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -40,6 +40,7 @@ import ( "time" "github.com/algorand/go-algorand/internal/rapidgen" + "github.com/algorand/go-algorand/network/p2p" "pgregory.net/rapid" "github.com/stretchr/testify/assert" @@ -3289,6 +3290,9 @@ type participatingNodeInfo struct { func (nnni *participatingNodeInfo) IsParticipating() bool { return true } +func (nnni *participatingNodeInfo) Capabilities() []p2p.Capability { + return nil +} func TestWebsocketNetworkTXMessageOfInterestPN(t *testing.T) { // Tests that A->B follows MOI diff --git a/node/follower_node.go b/node/follower_node.go index b60f185992..7c8ab482ea 100644 --- a/node/follower_node.go +++ b/node/follower_node.go @@ -162,7 +162,7 @@ func (node *AlgorandFollowerNode) Config() config.Local { } // Start the node: connect to peers while obtaining a lock. Doesn't wait for initial sync. -func (node *AlgorandFollowerNode) Start() { +func (node *AlgorandFollowerNode) Start() error { node.mu.Lock() defer node.mu.Unlock() @@ -172,22 +172,30 @@ func (node *AlgorandFollowerNode) Start() { // The start network is being called only after the various services start up. // We want to do so in order to let the services register their callbacks with the // network package before any connections are being made. - startNetwork := func() { + startNetwork := func() error { if !node.config.DisableNetworking { // start accepting connections - node.net.Start() + err := node.net.Start() + if err != nil { + return err + } node.config.NetAddress, _ = node.net.Address() } + return nil } + var err error if node.catchpointCatchupService != nil { - startNetwork() - _ = node.catchpointCatchupService.Start(node.ctx) + err = startNetwork() + if err == nil { + err = node.catchpointCatchupService.Start(node.ctx) + } } else { node.catchupService.Start() node.blockService.Start() - startNetwork() + err = startNetwork() } + return err } // ListeningAddress retrieves the node's current listening address, if any. diff --git a/node/node.go b/node/node.go index 165712148a..07e634c407 100644 --- a/node/node.go +++ b/node/node.go @@ -28,8 +28,6 @@ import ( "sync" "time" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/algorand/go-deadlock" "github.com/algorand/go-algorand/agreement" @@ -156,8 +154,6 @@ type AlgorandFullNode struct { tracer messagetracer.MessageTracer stateProofWorker *stateproof.Worker - - capabilitiesDiscovery *p2p.CapabilitiesDiscovery } // TxnWithStatus represents information about a single transaction, @@ -200,15 +196,6 @@ func MakeFull(log logging.Logger, rootDir string, cfg config.Local, phonebookAdd return nil, err } - if cfg.EnableDHTProviders { - caps, err0 := p2p.MakeCapabilitiesDiscovery(node.ctx, node.config, node.genesisDirs.RootGenesisDir, string(genesis.Network), node.log, []*peer.AddrInfo{}) - if err0 != nil { - log.Errorf("Failed to create dht node capabilities discovery: %v", err) - return nil, err - } - node.capabilitiesDiscovery = caps - } - // tie network, block fetcher, and agreement services together var p2pNode network.GossipNode if cfg.EnableP2PHybridMode { @@ -219,7 +206,7 @@ func MakeFull(log logging.Logger, rootDir string, cfg config.Local, phonebookAdd } } else if cfg.EnableP2P { // TODO: pass more appropriate genesisDir (hot/cold). Presently this is just used to store a peerID key. - p2pNode, err = network.NewP2PNetwork(node.log, node.config, node.genesisDirs.RootGenesisDir, phonebookAddresses, genesis.ID(), genesis.Network) + p2pNode, err = network.NewP2PNetwork(node.log, node.config, node.genesisDirs.RootGenesisDir, phonebookAddresses, genesis.ID(), genesis.Network, node) if err != nil { log.Errorf("could not create p2p node: %v", err) return nil, err @@ -360,7 +347,7 @@ func (node *AlgorandFullNode) Config() config.Local { } // Start the node: connect to peers and run the agreement service while obtaining a lock. Doesn't wait for initial sync. -func (node *AlgorandFullNode) Start() { +func (node *AlgorandFullNode) Start() error { node.mu.Lock() defer node.mu.Unlock() @@ -370,12 +357,16 @@ func (node *AlgorandFullNode) Start() { // The start network is being called only after the various services start up. // We want to do so in order to let the services register their callbacks with the // network package before any connections are being made. - startNetwork := func() { + startNetwork := func() error { if !node.config.DisableNetworking { // start accepting connections - node.net.Start() + err := node.net.Start() + if err != nil { + return err + } node.config.NetAddress, _ = node.net.Address() } + return nil } if node.catchpointCatchupService != nil { @@ -389,17 +380,18 @@ func (node *AlgorandFullNode) Start() { node.ledgerService.Start() node.txHandler.Start() node.stateProofWorker.Start() - startNetwork() + err := startNetwork() + if err != nil { + return err + } node.startMonitoringRoutines() } - if node.capabilitiesDiscovery != nil { - node.capabilitiesDiscovery.AdvertiseCapabilities(node.capabilities()...) - } - + return nil } -func (node *AlgorandFullNode) capabilities() []p2p.Capability { +// Capabilities returns the node's capabilities for advertising to other nodes. +func (node *AlgorandFullNode) Capabilities() []p2p.Capability { var caps []p2p.Capability if node.IsArchival() { caps = append(caps, p2p.Archival) @@ -465,9 +457,6 @@ func (node *AlgorandFullNode) Stop() { node.lowPriorityCryptoVerificationPool.Shutdown() node.cryptoPool.Shutdown() node.cancelCtx() - if node.capabilitiesDiscovery != nil { - node.capabilitiesDiscovery.Close() - } } // note: unlike the other two functions, this accepts a whole filename diff --git a/tools/debug/algodump/main.go b/tools/debug/algodump/main.go index 429cec9f9d..4d8863b354 100644 --- a/tools/debug/algodump/main.go +++ b/tools/debug/algodump/main.go @@ -179,7 +179,11 @@ func main() { *genesisID, protocol.NetworkID(*networkID)) setDumpHandlers(n) - n.Start() + err := n.Start() + if err != nil { + log.Errorf("Failed to start network: %v", err) + return + } for { time.Sleep(time.Second) diff --git a/tools/debug/transplanter/main.go b/tools/debug/transplanter/main.go index 1ee2a9c84e..e892d1fcb7 100644 --- a/tools/debug/transplanter/main.go +++ b/tools/debug/transplanter/main.go @@ -393,7 +393,11 @@ func main() { os.Exit(1) } - followerNode.Start() + err = followerNode.Start() + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot start follower node: %v", err) + os.Exit(1) + } for followerNode.Ledger().Latest() < basics.Round(*roundStart) { fmt.Printf("At round %d, waiting for %d\n", followerNode.Ledger().Latest(), *roundStart) From 30eeafabd5724b68c52dfb94661b7e417c9eba2d Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Tue, 9 Jan 2024 17:38:55 -0500 Subject: [PATCH 08/38] p2p: merge master, update license year --- network/hybridNetwork.go | 2 +- network/p2p/capabilities.go | 2 +- network/p2p/capabilities_test.go | 2 +- network/p2p/dht/dht.go | 2 +- network/p2p/dht/dht_test.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go index b43082eabb..b6168dc71e 100644 --- a/network/hybridNetwork.go +++ b/network/hybridNetwork.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// Copyright (C) 2019-2024 Algorand, Inc. // This file is part of go-algorand // // go-algorand is free software: you can redistribute it and/or modify diff --git a/network/p2p/capabilities.go b/network/p2p/capabilities.go index 502383b4ee..1ead897e6b 100644 --- a/network/p2p/capabilities.go +++ b/network/p2p/capabilities.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// Copyright (C) 2019-2024 Algorand, Inc. // This file is part of go-algorand // // go-algorand is free software: you can redistribute it and/or modify diff --git a/network/p2p/capabilities_test.go b/network/p2p/capabilities_test.go index ca3de978b7..65db0433c9 100644 --- a/network/p2p/capabilities_test.go +++ b/network/p2p/capabilities_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// Copyright (C) 2019-2024 Algorand, Inc. // This file is part of go-algorand // // go-algorand is free software: you can redistribute it and/or modify diff --git a/network/p2p/dht/dht.go b/network/p2p/dht/dht.go index 4b55e7e4f7..1d4fa8426a 100644 --- a/network/p2p/dht/dht.go +++ b/network/p2p/dht/dht.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// Copyright (C) 2019-2024 Algorand, Inc. // This file is part of go-algorand // // go-algorand is free software: you can redistribute it and/or modify diff --git a/network/p2p/dht/dht_test.go b/network/p2p/dht/dht_test.go index 93b8b379e5..51cb8978f7 100644 --- a/network/p2p/dht/dht_test.go +++ b/network/p2p/dht/dht_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2023 Algorand, Inc. +// Copyright (C) 2019-2024 Algorand, Inc. // This file is part of go-algorand // // go-algorand is free software: you can redistribute it and/or modify From d69938a8a786f4e1f1bf75ad8a0f147974d588c0 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Mon, 29 Jan 2024 13:25:19 -0500 Subject: [PATCH 09/38] p2p: HTTP catchup over p2p network (#5898) --- catchup/universalFetcher.go | 16 +- data/transactions/logic/opcodes.go | 9 +- go.mod | 50 ++-- go.sum | 103 ++++---- network/addr.go | 9 + network/addr_test.go | 2 + network/p2p/capabilities_test.go | 12 + network/p2p/dnsaddr/resolve.go | 4 +- network/p2p/dnsaddr/resolveController.go | 20 +- network/p2p/http.go | 46 ++++ network/p2p/p2p.go | 3 + network/p2p/peerID.go | 4 + network/p2p/streams.go | 48 +++- network/p2pNetwork.go | 189 ++++++++++++-- network/p2pNetwork_test.go | 150 +++++++++-- network/wsPeer.go | 7 +- node/node_test.go | 308 +++++++++++++++++++---- tools/block-generator/go.mod | 50 ++-- tools/block-generator/go.sum | 103 ++++---- 19 files changed, 872 insertions(+), 261 deletions(-) create mode 100644 network/p2p/http.go diff --git a/catchup/universalFetcher.go b/catchup/universalFetcher.go index 926c85bb48..4e4d920c23 100644 --- a/catchup/universalFetcher.go +++ b/catchup/universalFetcher.go @@ -219,13 +219,19 @@ type HTTPFetcher struct { // getBlockBytes gets a block. // Core piece of FetcherClient interface func (hf *HTTPFetcher) getBlockBytes(ctx context.Context, r basics.Round) (data []byte, err error) { - parsedURL, err := network.ParseHostOrURL(hf.rootURL) - if err != nil { - return nil, err + var blockURL string + + if network.IsMultiaddr(hf.rootURL) { + blockURL = rpcs.FormatBlockQuery(uint64(r), "", hf.net) + } else { + if parsedURL, err0 := network.ParseHostOrURL(hf.rootURL); err0 == nil { + parsedURL.Path = rpcs.FormatBlockQuery(uint64(r), parsedURL.Path, hf.net) + blockURL = parsedURL.String() + } else { + return nil, err0 + } } - parsedURL.Path = rpcs.FormatBlockQuery(uint64(r), parsedURL.Path, hf.net) - blockURL := parsedURL.String() hf.log.Debugf("block GET %#v peer %#v %T", blockURL, hf.peer, hf.peer) request, err := http.NewRequest("GET", blockURL, nil) if err != nil { diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index b8ecd76cac..2e3786588e 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -834,8 +834,13 @@ func OpcodesByVersion(version uint64) []OpSpec { } } result := maps.Values(subv) - slices.SortFunc(result, func(a, b OpSpec) bool { - return a.Opcode < b.Opcode + slices.SortFunc(result, func(a, b OpSpec) int { + if a.Opcode == b.Opcode { + return 0 + } else if a.Opcode > b.Opcode { + return 1 + } + return -1 }) return result } diff --git a/go.mod b/go.mod index 9ebaa462bd..d106515fe1 100644 --- a/go.mod +++ b/go.mod @@ -30,21 +30,21 @@ require ( github.com/jmoiron/sqlx v1.2.0 github.com/karalabe/usb v0.0.2 github.com/labstack/echo/v4 v4.9.1 - github.com/libp2p/go-libp2p v0.29.1 + github.com/libp2p/go-libp2p v0.32.2 github.com/libp2p/go-libp2p-kad-dht v0.24.3 - github.com/libp2p/go-libp2p-pubsub v0.9.3 + github.com/libp2p/go-libp2p-pubsub v0.10.0 github.com/libp2p/go-yamux/v4 v4.0.1 github.com/mattn/go-sqlite3 v1.14.16 - github.com/miekg/dns v1.1.55 - github.com/multiformats/go-multiaddr v0.10.1 + github.com/miekg/dns v1.1.56 + github.com/multiformats/go-multiaddr v0.12.0 github.com/multiformats/go-multiaddr-dns v0.3.1 github.com/olivere/elastic v6.2.14+incompatible github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.14.0 - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 - golang.org/x/sync v0.3.0 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d + golang.org/x/sync v0.4.0 golang.org/x/sys v0.13.0 golang.org/x/text v0.13.0 gopkg.in/sohlich/elogrus.v3 v3.0.0-20180410122755-1fa29e2f2009 @@ -81,17 +81,16 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect + github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect - github.com/huin/goupnp v1.2.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect + github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/invopop/yaml v0.1.0 // indirect github.com/ipfs/boxo v0.10.0 // indirect @@ -104,7 +103,7 @@ require ( github.com/jbenet/goprocess v0.1.4 // indirect github.com/jmespath/go-jmespath v0.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.17.2 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/koron/go-ssdp v0.0.4 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -119,11 +118,11 @@ require ( github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-nat v0.2.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect - github.com/libp2p/go-reuseport v0.3.0 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect @@ -137,10 +136,10 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multicodec v0.9.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.4.1 // indirect + github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect - github.com/onsi/ginkgo/v2 v2.11.0 // indirect - github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/onsi/ginkgo/v2 v2.13.0 // indirect + github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect @@ -152,10 +151,9 @@ require ( github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-19 v0.3.3 // indirect - github.com/quic-go/qtls-go1-20 v0.2.3 // indirect - github.com/quic-go/quic-go v0.36.3 // indirect - github.com/quic-go/webtransport-go v0.5.3 // indirect + github.com/quic-go/qtls-go1-20 v0.3.4 // indirect + github.com/quic-go/quic-go v0.39.4 // indirect + github.com/quic-go/webtransport-go v0.6.0 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -169,16 +167,16 @@ require ( go.opentelemetry.io/otel v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/otel/trace v1.16.0 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/dig v1.17.0 // indirect - go.uber.org/fx v1.20.0 // indirect + go.uber.org/dig v1.17.1 // indirect + go.uber.org/fx v1.20.1 // indirect + go.uber.org/mock v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/mod v0.12.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect - golang.org/x/tools v0.11.0 // indirect + golang.org/x/tools v0.14.0 // indirect gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 15a6c0d6b8..45f0add778 100644 --- a/go.sum +++ b/go.sum @@ -191,8 +191,6 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -229,8 +227,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= -github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b h1:RMpPgZTSApbPf7xaVel+QkoGPRLFLrwFO89uDUHEGf0= +github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -255,12 +253,12 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= -github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= -github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -321,8 +319,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -352,16 +350,16 @@ github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38y github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= -github.com/libp2p/go-libp2p v0.29.1 h1:yNeg6XgP8gbdc4YSrwiIt5T1TGOrVjH8dzl8h0GIOfQ= -github.com/libp2p/go-libp2p v0.29.1/go.mod h1:20El+LLy3/YhdUYIvGbLnvVJN32nMdqY6KXBENRAfLY= +github.com/libp2p/go-libp2p v0.32.2 h1:s8GYN4YJzgUoyeYNPdW7JZeZ5Ee31iNaIBfGYMAY4FQ= +github.com/libp2p/go-libp2p v0.32.2/go.mod h1:E0LKe+diV/ZVJVnOJby8VC5xzHF0660osg71skcxJvk= github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= github.com/libp2p/go-libp2p-kad-dht v0.24.3 h1:VjxtDVWaaf4UFjGBf+yl2JCiGaHx7+ctAUa9oJCR3QE= github.com/libp2p/go-libp2p-kad-dht v0.24.3/go.mod h1:BShPzRbK6+fN3hk8a0WGAYKpb8m4k+DtchkqouGTrSg= github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0= github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0= -github.com/libp2p/go-libp2p-pubsub v0.9.3 h1:ihcz9oIBMaCK9kcx+yHWm3mLAFBMAUsM4ux42aikDxo= -github.com/libp2p/go-libp2p-pubsub v0.9.3/go.mod h1:RYA7aM9jIic5VV47WXu4GkcRxRhrdElWf8xtyli+Dzc= +github.com/libp2p/go-libp2p-pubsub v0.10.0 h1:wS0S5FlISavMaAbxyQn3dxMOe2eegMfswM471RuHJwA= +github.com/libp2p/go-libp2p-pubsub v0.10.0/go.mod h1:1OxbaT/pFRO5h+Dpze8hdHQ63R0ke55XTs6b6NwLLkw= github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= @@ -371,8 +369,8 @@ github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= -github.com/libp2p/go-reuseport v0.3.0 h1:iiZslO5byUYZEg9iCwJGf5h+sf1Agmqx2V2FDjPyvUw= -github.com/libp2p/go-reuseport v0.3.0/go.mod h1:laea40AimhtfEqysZ71UpYj4S+R9VpH8PgqLo7L+SwI= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -394,8 +392,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -408,8 +406,8 @@ github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= -github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -439,8 +437,8 @@ github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= -github.com/multiformats/go-multiaddr v0.10.1 h1:HghtFrWyZEPrpTvgAMFJi6gFdgHfs2cb0pyfDsk+lqU= -github.com/multiformats/go-multiaddr v0.10.1/go.mod h1:jLEZsA61rwWNZQTHHnqq2HNa+4os/Hz54eqiRnsRqYQ= +github.com/multiformats/go-multiaddr v0.12.0 h1:1QlibTFkoXJuDjjYsMHhE73TnzJQl8FSWatk/0gxGzE= +github.com/multiformats/go-multiaddr v0.12.0/go.mod h1:WmZXgObOQOYp9r3cslLlppkrz1FYSHmE834dfz/lWu8= github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= @@ -452,8 +450,8 @@ github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI1 github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= -github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= +github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= +github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= @@ -468,13 +466,14 @@ github.com/olivere/elastic v6.2.14+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGe github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= -github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= @@ -508,14 +507,12 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-19 v0.3.3 h1:wznEHvJwd+2X3PqftRha0SUKmGsnb6dfArMhy9PeJVE= -github.com/quic-go/qtls-go1-19 v0.3.3/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.2.3 h1:m575dovXn1y2ATOb1XrRFcrv0F+EQmlowTkoraNkDPI= -github.com/quic-go/qtls-go1-20 v0.2.3/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/quic-go/quic-go v0.36.3 h1:f+yOqeGhMoRX7/M3wmEw/djhzKWr15FtQysox85/834= -github.com/quic-go/quic-go v0.36.3/go.mod h1:qxQumdeKw5GmWs1OsTZZnOxzSI+RJWuhf1O8FN35L2o= -github.com/quic-go/webtransport-go v0.5.3 h1:5XMlzemqB4qmOlgIus5zB45AcZ2kCgCy2EptUrfOPWU= -github.com/quic-go/webtransport-go v0.5.3/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= +github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/quic-go v0.39.4 h1:PelfiuG7wXEffUT2yceiqz5V6Pc0TA5ruOd1LcmFc1s= +github.com/quic-go/quic-go v0.39.4/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= +github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY= +github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -634,13 +631,14 @@ go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLk go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= -go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= -go.uber.org/fx v1.20.0 h1:ZMC/pnRvhsthOZh9MZjMq5U8Or3mA9zBSPaLnzs3ihQ= -go.uber.org/fx v1.20.0/go.mod h1:qCUj0btiR3/JnanEr1TYEePfSw6o/4qYJscgvzQ5Ub0= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -648,8 +646,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -667,8 +665,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -681,8 +679,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -722,8 +720,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -793,11 +791,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/network/addr.go b/network/addr.go index 1e2b04a447..c12a09a4af 100644 --- a/network/addr.go +++ b/network/addr.go @@ -64,6 +64,15 @@ func ParseHostOrURL(addr string) (*url.URL, error) { return parsed, err /* return original err, not our prefix altered try */ } +// IsMultiaddr returns true if the provided string is a valid multiaddr. +func IsMultiaddr(addr string) bool { + if strings.HasPrefix(addr, "/") && !strings.HasPrefix(addr, "//") { // multiaddr starts with '/' but not '//' which is possible for scheme relative URLS + _, err := multiaddr.NewMultiaddr(addr) + return err == nil + } + return false +} + // ParseHostOrURLOrMultiaddr returns an error if it could not parse the provided // string as a valid "host:port", full URL, or multiaddr. If no error, it returns // a host:port address, or a multiaddr. diff --git a/network/addr_test.go b/network/addr_test.go index 377fe72a91..eec2eccc36 100644 --- a/network/addr_test.go +++ b/network/addr_test.go @@ -124,6 +124,7 @@ func TestParseHostURLOrMultiaddr(t *testing.T) { v, err := ParseHostOrURLOrMultiaddr(addr) require.NoError(t, err) require.Equal(t, addr, v) + require.True(t, IsMultiaddr(addr)) }) } @@ -131,6 +132,7 @@ func TestParseHostURLOrMultiaddr(t *testing.T) { t.Run(addr, func(t *testing.T) { _, err := ParseHostOrURLOrMultiaddr(addr) require.Error(t, err) + require.False(t, IsMultiaddr(addr)) }) } diff --git a/network/p2p/capabilities_test.go b/network/p2p/capabilities_test.go index 65db0433c9..d6694e5fb2 100644 --- a/network/p2p/capabilities_test.go +++ b/network/p2p/capabilities_test.go @@ -96,6 +96,12 @@ func setupDHTHosts(t *testing.T, numHosts int) []*dht.IpfsDHT { for _, h := range hosts { ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, func() []peer.AddrInfo { return bootstrapPeers }) require.NoError(t, err) + // this is a workaround for the following issue + // "failed to negotiate security protocol: error reading handshake message: noise: message is too short" + // it appears simultenous connectino attempts (dht.New() attempts to connect) causes this handshake error. + // https://github.com/libp2p/go-libp2p-noise/issues/70 + time.Sleep(200 * time.Millisecond) + err = ht.Bootstrap(context.Background()) require.NoError(t, err) dhts = append(dhts, ht) @@ -150,6 +156,12 @@ func setupCapDiscovery(t *testing.T, numHosts int, numBootstrapPeers int) []*Cap } ht, err := algodht.MakeDHT(context.Background(), h, "devtestnet", cfg, func() []peer.AddrInfo { return bp }) require.NoError(t, err) + // this is a workaround for the following issue + // "failed to negotiate security protocol: error reading handshake message: noise: message is too short" + // it appears simultenous connectino attempts (dht.New() attempts to connect) causes this handshake error. + // https://github.com/libp2p/go-libp2p-noise/issues/70 + time.Sleep(200 * time.Millisecond) + disc, err := algodht.MakeDiscovery(ht) require.NoError(t, err) cd := &CapabilitiesDiscovery{ diff --git a/network/p2p/dnsaddr/resolve.go b/network/p2p/dnsaddr/resolve.go index 176d62f946..56caa84f4b 100644 --- a/network/p2p/dnsaddr/resolve.go +++ b/network/p2p/dnsaddr/resolve.go @@ -30,7 +30,7 @@ func isDnsaddr(maddr multiaddr.Multiaddr) bool { } // Iterate runs through the resolvable dnsaddrs in the tree using the resolveController and invokes f for each dnsaddr node lookup -func Iterate(initial multiaddr.Multiaddr, controller *MultiaddrDNSResolveController, f func(dnsaddr multiaddr.Multiaddr, entries []multiaddr.Multiaddr) error) error { +func Iterate(initial multiaddr.Multiaddr, controller ResolveController, f func(dnsaddr multiaddr.Multiaddr, entries []multiaddr.Multiaddr) error) error { resolver := controller.Resolver() if resolver == nil { return errors.New("passed controller has no resolvers Iterate") @@ -64,7 +64,7 @@ func Iterate(initial multiaddr.Multiaddr, controller *MultiaddrDNSResolveControl // Any further dnsaddrs will be looked up until all TXT records have been fetched, // and the full list of resulting Multiaddrs is returned. // It uses the MultiaddrDNSResolveController to cycle through DNS resolvers on failure. -func MultiaddrsFromResolver(domain string, controller *MultiaddrDNSResolveController) ([]multiaddr.Multiaddr, error) { +func MultiaddrsFromResolver(domain string, controller ResolveController) ([]multiaddr.Multiaddr, error) { dnsaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/dnsaddr/%s", domain)) if err != nil { return nil, fmt.Errorf("unable to construct multiaddr for %s : %v", domain, err) diff --git a/network/p2p/dnsaddr/resolveController.go b/network/p2p/dnsaddr/resolveController.go index 73a46243d0..ff606b39a1 100644 --- a/network/p2p/dnsaddr/resolveController.go +++ b/network/p2p/dnsaddr/resolveController.go @@ -17,15 +17,29 @@ package dnsaddr import ( + "context" + + "github.com/multiformats/go-multiaddr" madns "github.com/multiformats/go-multiaddr-dns" log "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/tools/network" ) +// Resolver is an interface for resolving dnsaddrs +type Resolver interface { + Resolve(ctx context.Context, maddr multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) +} + +// ResolveController is an interface for cycling through resolvers +type ResolveController interface { + Resolver() Resolver + NextResolver() Resolver +} + // MultiaddrDNSResolveController returns a madns.Resolver, cycling through underlying net.Resolvers type MultiaddrDNSResolveController struct { - resolver *madns.Resolver + resolver Resolver nextResolvers []func() *madns.Resolver controller network.ResolveController } @@ -45,7 +59,7 @@ func NewMultiaddrDNSResolveController(secure bool, fallbackDNSResolverAddress st } // NextResolver applies the nextResolvers functions in order and returns the most recent result -func (c *MultiaddrDNSResolveController) NextResolver() *madns.Resolver { +func (c *MultiaddrDNSResolveController) NextResolver() Resolver { if len(c.nextResolvers) == 0 { c.resolver = nil } else { @@ -56,7 +70,7 @@ func (c *MultiaddrDNSResolveController) NextResolver() *madns.Resolver { } // Resolver returns the current resolver, invokes NextResolver if the resolver is nil -func (c *MultiaddrDNSResolveController) Resolver() *madns.Resolver { +func (c *MultiaddrDNSResolveController) Resolver() Resolver { if c.resolver == nil { c.resolver = c.NextResolver() } diff --git a/network/p2p/http.go b/network/p2p/http.go new file mode 100644 index 0000000000..1f39fa56c2 --- /dev/null +++ b/network/p2p/http.go @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2024 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 ( + "net/http" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/peer" + libp2phttp "github.com/libp2p/go-libp2p/p2p/http" +) + +// MakeHTTPClient creates a http.Client that uses libp2p transport for a goven protocol and peer address. +func MakeHTTPClient(protocolID string, addrInfo peer.AddrInfo) (http.Client, error) { + clientStreamHost, err := libp2p.New(libp2p.NoListenAddrs) + if err != nil { + return http.Client{}, err + } + + client := libp2phttp.Host{StreamHost: clientStreamHost} + + // Do not use client.NamespacedClient to prevent it making connection to a well-known handler + // to make a NamespaceRoundTripper that limits to specific URL paths. + // First, we do not want make requests when listing peers (the main MakeHTTPClient invoker). + // Secondly, this makes unit testing easier - no need to register fake handlers. + rt, err := client.NewConstrainedRoundTripper(addrInfo) + if err != nil { + return http.Client{}, err + } + + return http.Client{Transport: rt}, nil +} diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 7450f34794..99f537ed36 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -75,6 +75,9 @@ type serviceImpl struct { // AlgorandWsProtocol defines a libp2p protocol name for algorand's websockets messages const AlgorandWsProtocol = "/algorand-ws/1.0.0" +// AlgorandP2pHTTPProtocol defines a libp2p protocol name for algorand's http over p2p messages +const AlgorandP2pHTTPProtocol = "/algorand-http/1.0.0" + const dialTimeout = 30 * time.Second // MakeHost creates a libp2p host but does not start listening. diff --git a/network/p2p/peerID.go b/network/p2p/peerID.go index 382258ac22..8ca584cbe8 100644 --- a/network/p2p/peerID.go +++ b/network/p2p/peerID.go @@ -39,6 +39,10 @@ const DefaultPrivKeyPath = "peerIDPrivKey.pem" // PeerID is a string representation of a peer's public key, primarily used to avoid importing libp2p into packages that shouldn't need it type PeerID string +func (id PeerID) String() string { + return peer.ID(id).String() +} + // GetPrivKey manages loading and creation of private keys for network PeerIDs // It prioritizes, in this order: // 1. user supplied path to privKey diff --git a/network/p2p/streams.go b/network/p2p/streams.go index 4a7a2d8e01..0e5c59d50c 100644 --- a/network/p2p/streams.go +++ b/network/p2p/streams.go @@ -73,7 +73,17 @@ func (n *streamManager) streamHandler(stream network.Stream) { n.log.Infof("Failed to check old stream with %s: %v", remotePeer, err) } n.streams[stream.Conn().RemotePeer()] = stream - n.handler(n.ctx, remotePeer, stream, true) + + // streamHandler is supposed to be called for accepted streams, so we expect incoming here + incoming := stream.Stat().Direction == network.DirInbound + if !incoming { + if stream.Stat().Direction == network.DirUnknown { + n.log.Warnf("Unknown direction for a steam %s to/from %s", stream.ID(), remotePeer) + } else { + n.log.Warnf("Unexpected outgoing sream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) + } + } + n.handler(n.ctx, remotePeer, stream, incoming) return } // otherwise, the old stream is still open, so we can close the new one @@ -82,7 +92,16 @@ func (n *streamManager) streamHandler(stream network.Stream) { } // no old stream n.streams[stream.Conn().RemotePeer()] = stream - n.handler(n.ctx, remotePeer, stream, true) + // streamHandler is supposed to be called for accepted streams, so we expect incoming here + incoming := stream.Stat().Direction == network.DirInbound + if !incoming { + if stream.Stat().Direction == network.DirUnknown { + n.log.Warnf("streamHandler: unknown direction for a steam %s to/from %s", stream.ID(), remotePeer) + } else { + n.log.Warnf("Unexpected outgoing sream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) + } + } + n.handler(n.ctx, remotePeer, stream, incoming) } // Connected is called when a connection is opened @@ -95,8 +114,13 @@ func (n *streamManager) Connected(net network.Network, conn network.Conn) { return } + needUnlock := true n.streamsLock.Lock() - defer n.streamsLock.Unlock() + defer func() { + if needUnlock { + n.streamsLock.Unlock() + } + }() _, ok := n.streams[remotePeer] if ok { return // there's already an active stream with this peer for our protocol @@ -107,9 +131,23 @@ func (n *streamManager) Connected(net network.Network, conn network.Conn) { n.log.Infof("Failed to open stream to %s: %v", remotePeer, err) return } - n.streams[remotePeer] = stream - n.handler(n.ctx, remotePeer, stream, false) + + // release the lock to let handler do its thing + // otherwise reading/writing to the stream will deadlock + needUnlock = false + n.streamsLock.Unlock() + + // a new stream created above, expected direction is outbound + incoming := stream.Stat().Direction == network.DirInbound + if incoming { + n.log.Warnf("Unexpected incoming sream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) + } else { + if stream.Stat().Direction == network.DirUnknown { + n.log.Warnf("Connected: unknown direction for a steam %s to/from %s", stream.ID(), remotePeer) + } + } + n.handler(n.ctx, remotePeer, stream, incoming) } // Disconnected is called when a connection is closed diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index f54c5de89c..4cfec68bad 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -32,10 +32,12 @@ import ( "github.com/algorand/go-algorand/network/p2p/peerstore" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-deadlock" + "github.com/gorilla/mux" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" + libp2phttp "github.com/libp2p/go-libp2p/p2p/http" manet "github.com/multiformats/go-multiaddr/net" ) @@ -69,13 +71,19 @@ type P2PNetwork struct { bootstrapper bootstrapper nodeInfo NodeInfo + pstore *peerstore.PeerStore + httpServer libp2phttp.Host + + p2phttpMux *mux.Router + p2phttpMuxRegistarOnce sync.Once } type bootstrapper struct { - cfg config.Local - networkID protocol.NetworkID - phonebookPeers []*peer.AddrInfo - started bool + cfg config.Local + networkID protocol.NetworkID + phonebookPeers []*peer.AddrInfo + resolveControler dnsaddr.ResolveController + started bool } func (b *bootstrapper) start() { @@ -103,16 +111,15 @@ func (b *bootstrapper) BootstrapFunc() []peer.AddrInfo { return addrs } - return getBootstrapPeers(b.cfg, b.networkID) + return getBootstrapPeers(b.cfg, b.networkID, b.resolveControler) } // getBootstrapPeers looks up a list of Multiaddrs strings from the dnsaddr records at the primary // SRV record domain. -func getBootstrapPeers(cfg config.Local, network protocol.NetworkID) []peer.AddrInfo { +func getBootstrapPeers(cfg config.Local, network protocol.NetworkID, controller dnsaddr.ResolveController) []peer.AddrInfo { var addrs []peer.AddrInfo bootstraps := cfg.DNSBootstrapArray(network) for _, dnsBootstrap := range bootstraps { - controller := dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecuritySRVEnforced(), "") resolvedAddrs, err := dnsaddr.MultiaddrsFromResolver(dnsBootstrap.PrimarySRVBootstrap, controller) if err != nil { continue @@ -163,6 +170,8 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo wsPeersToIDs: make(map[*wsPeer]peer.ID), peerStats: make(map[peer.ID]*p2pPeerStats), nodeInfo: node, + pstore: pstore, + p2phttpMux: mux.NewRouter(), } net.ctx, net.ctxCancel = context.WithCancel(context.Background()) net.handler = msgHandler{ @@ -191,9 +200,10 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo } bootstrapper := &bootstrapper{ - cfg: cfg, - networkID: networkID, - phonebookPeers: addrInfo, + cfg: cfg, + networkID: networkID, + phonebookPeers: addrInfo, + resolveControler: dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecuritySRVEnforced(), ""), } if cfg.EnableDHTProviders { @@ -205,6 +215,10 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo net.capabilitiesDiscovery = disc } + net.httpServer = libp2phttp.Host{ + StreamHost: h, + } + err = net.setup() if err != nil { return nil, err @@ -250,6 +264,9 @@ func (n *P2PNetwork) Start() error { go n.handler.messageHandlerThread(&n.wg, n.wsPeersConnectivityCheckTicker.C, n) } + n.wg.Add(1) + go n.httpdThread() + n.wg.Add(1) go n.broadcaster.broadcastThread(&n.wg, n) n.service.DialPeersUntilTargetCount(n.config.GossipFanout) @@ -279,6 +296,7 @@ func (n *P2PNetwork) Stop() { n.ctxCancel() n.service.Close() n.bootstrapper.stop() + n.httpServer.Close() n.wg.Wait() } @@ -316,6 +334,15 @@ func (n *P2PNetwork) meshThread() { } } +func (n *P2PNetwork) httpdThread() { + defer n.wg.Done() + err := n.httpServer.Serve() + if err != nil { + n.log.Errorf("Error serving libp2phttp: %v", err) + return + } +} + // GetGenesisID implements GossipNode func (n *P2PNetwork) GetGenesisID() string { return n.genesisID @@ -407,6 +434,10 @@ func (n *P2PNetwork) DisconnectPeers() { // RegisterHTTPHandler path accepts gorilla/mux path annotations func (n *P2PNetwork) RegisterHTTPHandler(path string, handler http.Handler) { + n.p2phttpMux.Handle(path, handler) + n.p2phttpMuxRegistarOnce.Do(func() { + n.httpServer.SetHTTPHandlerAtPath(p2p.AlgorandP2pHTTPProtocol, "/", n.p2phttpMux) + }) } // RequestConnectOutgoing asks the system to actually connect to peers. @@ -417,13 +448,97 @@ func (n *P2PNetwork) RequestConnectOutgoing(replace bool, quit <-chan struct{}) // GetPeers returns a list of Peers we could potentially send a direct message to. func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { - // currently returns same list of peers for all PeerOption filters. peers := make([]Peer, 0) - n.wsPeersLock.RLock() - for _, peer := range n.wsPeers { - peers = append(peers, Peer(peer)) + for _, option := range options { + switch option { + case PeersConnectedOut: + n.wsPeersLock.RLock() + for _, peer := range n.wsPeers { + if peer.outgoing { + peers = append(peers, Peer(peer)) + } + } + n.wsPeersLock.RUnlock() + case PeersPhonebookRelays: + // TODO: query peerstore for PhoneBookEntryRelayRole + // TODO: currently peerstore is not populated in a way to store roles + // return all nodes at the moment + + // // return copy of phonebook, which probably also contains peers we're connected to, but if it doesn't maybe we shouldn't be making new connections to those peers (because they disappeared from the directory) + // addrs := n.pstore.GetAddresses(1000, PhoneBookEntryRelayRole) + // for _, addr := range addrs { + // peerCore := makePeerCore(n.ctx, n, n.log, n.handler.readBuffer, addr, n.GetRoundTripper(nil), "" /*origin address*/) + // peers = append(peers, &peerCore) + // } + + // temporary return all nodes + n.wsPeersLock.RLock() + for _, peer := range n.wsPeers { + peers = append(peers, Peer(peer)) + } + n.wsPeersLock.RUnlock() + + case PeersPhonebookArchivalNodes: + // query known archvial nodes from DHT if enabled + if n.config.EnableDHTProviders { + const nodesToFind = 5 + info, err := n.capabilitiesDiscovery.PeersForCapability(p2p.Archival, nodesToFind) + if err != nil { + n.log.Warnf("Error getting archival nodes from capabilities discovery: %v", err) + return peers + } + n.log.Debugf("Got %d archival node(s) from DHT", len(info)) + for _, addrInfo := range info { + info := addrInfo + mas, err := peer.AddrInfoToP2pAddrs(&info) + if err != nil { + n.log.Warnf("Archival AddrInfo conversion error: %v", err) + continue + } + if len(mas) == 0 { + n.log.Warnf("Archival AddrInfo: empty multiaddr for : %v", addrInfo) + continue + } + addr := mas[0].String() + client, err := p2p.MakeHTTPClient(p2p.AlgorandP2pHTTPProtocol, addrInfo) + if err != nil { + n.log.Warnf("MakeHTTPClient failed: %v", err) + continue + } + + peerCore := makePeerCoreWithClient( + n.ctx, n, n.log, n.handler.readBuffer, + addr /*rootURL*/, client, "", /*origin address*/ + ) + peers = append(peers, &peerCore) + } + if n.log.GetLevel() >= logging.Debug && len(peers) > 0 { + addrs := make([]string, 0, len(peers)) + for _, peer := range peers { + addrs = append(addrs, peer.(*wsPeerCore).rootURL) + } + n.log.Debugf("Archival node(s) from DHT: %v", addrs) + } + } + case PeersPhonebookArchivers: + // TODO: remove after merging with master + // temporary return all nodes + n.wsPeersLock.RLock() + for _, peer := range n.wsPeers { + peers = append(peers, Peer(peer)) + } + n.wsPeersLock.RUnlock() + + case PeersConnectedIn: + n.wsPeersLock.RLock() + for _, peer := range n.wsPeers { + if !peer.outgoing { + peers = append(peers, Peer(peer)) + } + } + n.wsPeersLock.RUnlock() + } } - n.wsPeersLock.RUnlock() return peers } @@ -468,6 +583,23 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream n return } } else { + n.wsPeersLock.Lock() + numOutgoingPeers := 0 + for _, peer := range n.wsPeers { + if peer.outgoing { + n.log.Debugf("outgoing peer orig=%s addr=%s", peer.OriginAddress(), peer.GetAddress()) + numOutgoingPeers++ + } + } + n.wsPeersLock.Unlock() + if numOutgoingPeers >= n.config.GossipFanout { + // this appears to be some auxiliary connection made by libp2p itself like DHT connection. + // skip this connection since there are already enough peers + n.log.Debugf("skipping outgoing connection to peer %s: num outgoing %d > fanout %d ", peer, numOutgoingPeers, n.config.GossipFanout) + stream.Close() + return + } + _, err := stream.Write([]byte("1")) if err != nil { n.log.Warnf("wsStreamHandler: error sending initial message: %s", err) @@ -476,7 +608,8 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream n } // get address for peer ID - addr := stream.Conn().RemoteMultiaddr().String() + ma := stream.Conn().RemoteMultiaddr() + addr := ma.String() if addr == "" { n.log.Warnf("Could not get address for peer %s", peer) } @@ -492,6 +625,30 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream n n.wsPeersToIDs[wsp] = peer n.wsPeersLock.Unlock() n.wsPeersChangeCounter.Add(1) + + event := "ConnectedOut" + msg := "Made outgoing connection to peer %s" + if incoming { + event = "ConnectedIn" + msg = "Accepted incoming connection from peer %s" + } + localAddr, _ := n.Address() + n.log.With("event", event).With("remote", addr).With("local", localAddr).Infof(msg, peer.String()) + + if n.log.GetLevel() >= logging.Debug { + n.log.Debugf("streams for %s conn %s ", stream.Conn().Stat().Direction.String(), stream.Conn().ID()) + for _, s := range stream.Conn().GetStreams() { + n.log.Debugf("%s stream %s protocol %s", s.Stat().Direction.String(), s.ID(), s.Protocol()) + } + } + // TODO: add telemetry + // n.log.EventWithDetails(telemetryspec.Network, telemetryspec.ConnectPeerEvent, + // telemetryspec.PeerEventDetails{ + // Address: addr, + // TelemetryGUID: trackedRequest.otherTelemetryGUID, + // Incoming: true, + // InstanceName: trackedRequest.otherInstanceName, + // }) } // peerRemoteClose called from wsPeer to report that it has closed diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index e4e811f695..5cabaf7a57 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -19,6 +19,8 @@ package network import ( "context" "fmt" + "io" + "net/http" "sync" "sync/atomic" "testing" @@ -27,6 +29,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network/p2p" + "github.com/algorand/go-algorand/network/p2p/dnsaddr" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" @@ -34,6 +37,7 @@ import ( "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" peerstore "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" ) @@ -49,7 +53,9 @@ func TestP2PSubmitTX(t *testing.T) { defer netA.Stop() peerInfoA := netA.service.AddrInfo() + fmt.Print("peerInfoA is ", peerInfoA) addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) + fmt.Printf("addrsA is %v\n", addrsA) require.NoError(t, err) require.NotZero(t, addrsA[0]) @@ -225,9 +231,7 @@ func (s *mockService) DialPeersUntilTargetCount(targetConnCount int) { } func (s *mockService) ClosePeer(peer peer.ID) error { - if _, ok := s.peers[peer]; ok { - delete(s.peers, peer) - } + delete(s.peers, peer) return nil } @@ -246,10 +250,6 @@ func (s *mockService) Publish(ctx context.Context, topic string, data []byte) er return nil } -func (s *mockService) setAddrs(addrs []ma.Multiaddr) { - s.addrs = addrs -} - func makeMockService(id peer.ID, addrs []ma.Multiaddr) *mockService { return &mockService{ id: id, @@ -317,6 +317,31 @@ func TestP2PNetworkAddress(t *testing.T) { require.Empty(t, retAddr) } +type nilResolveController struct{} + +func (c *nilResolveController) Resolver() dnsaddr.Resolver { + return nil +} + +func (c *nilResolveController) NextResolver() dnsaddr.Resolver { + return nil +} + +type mockResolveController struct { + nilResolveController +} + +func (c *mockResolveController) Resolver() dnsaddr.Resolver { + return &mockResolver{} +} + +type mockResolver struct{} + +func (r *mockResolver) Resolve(ctx context.Context, maddr multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { + ma, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC") + return []multiaddr.Multiaddr{ma}, err +} + func TestBootstrapFunc(t *testing.T) { t.Parallel() partitiontest.PartitionTest(t) @@ -334,7 +359,8 @@ func TestBootstrapFunc(t *testing.T) { b.cfg = config.GetDefaultLocal() b.cfg.DNSBootstrapID = ".algodev.network" b.cfg.DNSSecurityFlags = 0 - b.networkID = "test" + b.networkID = "devnet" + b.resolveControler = &mockResolveController{} addrs := b.BootstrapFunc() @@ -352,7 +378,8 @@ func TestGetBootstrapPeersFailure(t *testing.T) { cfg.DNSSecurityFlags = 0 cfg.DNSBootstrapID = "non-existent.algodev.network" - addrs := getBootstrapPeers(cfg, "test") + controller := nilResolveController{} + addrs := getBootstrapPeers(cfg, "test", &controller) require.Equal(t, 0, len(addrs)) } @@ -365,7 +392,8 @@ func TestGetBootstrapPeersInvalidAddr(t *testing.T) { cfg.DNSSecurityFlags = 0 cfg.DNSBootstrapID = ".algodev.network" - addrs := getBootstrapPeers(cfg, "testInvalidAddr") + controller := nilResolveController{} + addrs := getBootstrapPeers(cfg, "testInvalidAddr", &controller) require.Equal(t, 0, len(addrs)) } @@ -456,6 +484,7 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { ) t.Logf("peers connected") + nets := []*P2PNetwork{netA, netB, netC} discs := []*p2p.CapabilitiesDiscovery{netA.capabilitiesDiscovery, netB.capabilitiesDiscovery, netC.capabilitiesDiscovery} var wg sync.WaitGroup @@ -480,10 +509,12 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { } wg.Add(len(discs)) - for _, disc := range discs { - go func(disc *p2p.CapabilitiesDiscovery) { + for i := range discs { + go func(idx int) { + disc := discs[idx] defer wg.Done() - if disc == netA.capabilitiesDiscovery { + // skip netA since it is special for the test cap=netA + if test.name == "cap=netA" && disc == netA.capabilitiesDiscovery { return } require.Eventuallyf(t, @@ -498,9 +529,100 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { time.Second, fmt.Sprintf("Not all expected %s cap peers were found", cap), ) - }(disc) + // ensure GetPeers gets PeersPhonebookArchivalNodes peers + // it appears there are artifical peers because of listening on localhost and on a real network interface + // so filter out and save only unique peers by their IDs + net := nets[idx] + peers := net.GetPeers(PeersPhonebookArchivalNodes) + uniquePeerIDs := make(map[peer.ID]struct{}) + for _, peer := range peers { + wsPeer := peer.(*wsPeerCore) + pi, err := peerstore.AddrInfoFromString(wsPeer.rootURL) + require.NoError(t, err) + uniquePeerIDs[pi.ID] = struct{}{} + } + require.Equal(t, test.numCapPeers, len(uniquePeerIDs)) + }(i) } wg.Wait() }) } } + +// TestMultiaddrConversionToFrom ensures Multiaddr can be serialized back to an address without losing information +func TestMultiaddrConversionToFrom(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + a := "/ip4/192.168.1.1/tcp/8180/p2p/Qmewz5ZHN1AAGTarRbMupNPbZRfg3p5jUGoJ3JYEatJVVk" + ma, err := multiaddr.NewMultiaddr(a) + require.NoError(t, err) + require.Equal(t, a, ma.String()) + + // this conversion drops the p2p proto part + pi, err := peer.AddrInfoFromP2pAddr(ma) + require.NoError(t, err) + require.NotEqual(t, a, pi.Addrs[0].String()) + require.Len(t, pi.Addrs, 1) + + mas, err := peer.AddrInfoToP2pAddrs(pi) + require.NoError(t, err) + require.Len(t, mas, 1) + require.Equal(t, a, mas[0].String()) +} + +type p2phttpHandler struct { + retData string +} + +func (h *p2phttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(h.retData)) +} + +func TestP2PHTTPHandler(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + cfg := config.GetDefaultLocal() + cfg.EnableDHTProviders = true + cfg.GossipFanout = 1 + log := logging.TestingLog(t) + + netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + + h := &p2phttpHandler{"hello"} + netA.RegisterHTTPHandler("/test", h) + + h2 := &p2phttpHandler{"world"} + netA.RegisterHTTPHandler("/bar", h2) + + netA.Start() + defer netA.Stop() + + peerInfoA := netA.service.AddrInfo() + addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) + require.NoError(t, err) + require.NotZero(t, addrsA[0]) + + httpClient, err := p2p.MakeHTTPClient(p2p.AlgorandP2pHTTPProtocol, netA.service.AddrInfo()) + require.NoError(t, err) + resp, err := httpClient.Get("/test") + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "hello", string(body)) + + httpClient, err = p2p.MakeHTTPClient(p2p.AlgorandP2pHTTPProtocol, netA.service.AddrInfo()) + require.NoError(t, err) + resp, err = httpClient.Get("/bar") + require.NoError(t, err) + defer resp.Body.Close() + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "world", string(body)) + +} diff --git a/network/wsPeer.go b/network/wsPeer.go index 6477285ade..06af7c6bad 100644 --- a/network/wsPeer.go +++ b/network/wsPeer.go @@ -348,6 +348,11 @@ type TCPInfoUnicastPeer interface { // Create a wsPeerCore object func makePeerCore(ctx context.Context, net GossipNode, log logging.Logger, readBuffer chan<- IncomingMessage, rootURL string, roundTripper http.RoundTripper, originAddress string) wsPeerCore { + return makePeerCoreWithClient(ctx, net, log, readBuffer, rootURL, http.Client{Transport: roundTripper}, originAddress) +} + +// Create a wsPeerCore object +func makePeerCoreWithClient(ctx context.Context, net GossipNode, log logging.Logger, readBuffer chan<- IncomingMessage, rootURL string, client http.Client, originAddress string) wsPeerCore { return wsPeerCore{ net: net, netCtx: ctx, @@ -355,7 +360,7 @@ func makePeerCore(ctx context.Context, net GossipNode, log logging.Logger, readB readBuffer: readBuffer, rootURL: rootURL, originAddress: originAddress, - client: http.Client{Transport: roundTripper}, + client: client, } } diff --git a/node/node_test.go b/node/node_test.go index dabb7958a5..f82df98b84 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -33,13 +33,13 @@ import ( "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/data" "github.com/algorand/go-algorand/data/account" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" + "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/stateproof" "github.com/algorand/go-algorand/test/partitiontest" @@ -61,31 +61,78 @@ var defaultConfig = config.Local{ IncomingConnectionsLimit: -1, } +type nodeInfo struct { + idx int + host string + wsPort int + p2pPort int + p2pID p2p.PeerID + rootDir string + genesis bookkeeping.Genesis +} + +func (ni nodeInfo) wsNetAddr() string { + return fmt.Sprintf("%s:%d", ni.host, ni.wsPort) +} + +func (ni nodeInfo) p2pNetAddr() string { + return fmt.Sprintf("%s:%d", ni.host, ni.p2pPort) +} + +func (ni nodeInfo) p2pMultiAddr() string { + return fmt.Sprintf("/ip4/%s/tcp/%d/p2p/%s", ni.host, ni.p2pPort, ni.p2pID.String()) +} + +type configHook func(ni nodeInfo, cfg config.Local) (nodeInfo, config.Local) +type phonebookHook func([]nodeInfo, int) []string + func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationPool execpool.BacklogPool, customConsensus config.ConsensusProtocols) ([]*AlgorandFullNode, []string) { + minMoneyAtStart := 10000 + maxMoneyAtStart := 100000 + gen := rand.New(rand.NewSource(2)) + + const numAccounts = 10 + acctStake := make([]basics.MicroAlgos, numAccounts) + for i := range acctStake { + acctStake[i] = basics.MicroAlgos{Raw: uint64(minMoneyAtStart + (gen.Int() % (maxMoneyAtStart - minMoneyAtStart)))} + } + + configHook := func(ni nodeInfo, cfg config.Local) (nodeInfo, config.Local) { + cfg.NetAddress = ni.wsNetAddr() + return ni, cfg + } + + phonebookHook := func(nodes []nodeInfo, nodeIdx int) []string { + phonebook := make([]string, 0, len(nodes)-1) + for i := range nodes { + if i != nodeIdx { + phonebook = append(phonebook, nodes[i].wsNetAddr()) + } + } + return phonebook + } + nodes, wallets := setupFullNodesEx(t, proto, verificationPool, customConsensus, acctStake, configHook, phonebookHook) + require.Len(t, nodes, numAccounts) + require.Len(t, wallets, numAccounts) + return nodes, wallets +} + +func setupFullNodesEx( + t *testing.T, proto protocol.ConsensusVersion, verificationPool execpool.BacklogPool, customConsensus config.ConsensusProtocols, + acctStake []basics.MicroAlgos, configHook configHook, phonebookHook phonebookHook, +) ([]*AlgorandFullNode, []string) { + util.SetFdSoftLimit(1000) + f, _ := os.Create(t.Name() + ".log") logging.Base().SetJSONFormatter() logging.Base().SetOutput(f) logging.Base().SetLevel(logging.Debug) - - numAccounts := 10 - minMoneyAtStart := 10000 - maxMoneyAtStart := 100000 + t.Logf("Logging to %s\n", t.Name()+".log") firstRound := basics.Round(0) lastRound := basics.Round(200) - genesis := make(map[basics.Address]basics.AccountData) - gen := rand.New(rand.NewSource(2)) - neighbors := make([]string, numAccounts) - for i := range neighbors { - neighbors[i] = "127.0.0.1:" + strconv.Itoa(10000+i) - } - - wallets := make([]string, numAccounts) - nodes := make([]*AlgorandFullNode, numAccounts) - rootDirs := make([]string, 0) - // The genesis configuration is missing allocations, but that's OK // because we explicitly generated the sqlite database above (in // installFullNode). @@ -97,16 +144,31 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP RewardsPool: poolAddr.String(), } + genesis := make(map[basics.Address]basics.AccountData) + numAccounts := len(acctStake) + wallets := make([]string, numAccounts) + nodeInfos := make([]nodeInfo, numAccounts) + for i := range wallets { rootDirectory := t.TempDir() - rootDirs = append(rootDirs, rootDirectory) + nodeInfos[i] = nodeInfo{ + idx: i, + host: "127.0.0.1", + wsPort: 10000 + 100*i, + p2pPort: 10000 + 100*i + 1, + rootDir: rootDirectory, + genesis: g, + } - defaultConfig.NetAddress = neighbors[i] - defaultConfig.SaveToDisk(rootDirectory) + ni, cfg := configHook(nodeInfos[i], defaultConfig) + nodeInfos[i] = ni + cfg.SaveToDisk(rootDirectory) - // Save empty phonebook - we'll add peers after they've been assigned listening ports - err := config.SavePhonebookToDisk(make([]string, 0), rootDirectory) - require.NoError(t, err) + t.Logf("Root directory of node %d (%s): %s\n", i, ni.wsNetAddr(), rootDirectory) + + // // Save empty phonebook - we'll add peers after they've been assigned listening ports + // err := config.SavePhonebookToDisk(make([]string, 0), rootDirectory) + // require.NoError(t, err) genesisDir := filepath.Join(rootDirectory, g.ID()) os.Mkdir(genesisDir, 0700) @@ -140,7 +202,7 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP data := basics.AccountData{ Status: basics.Online, - MicroAlgos: basics.MicroAlgos{Raw: uint64(minMoneyAtStart + (gen.Int() % (maxMoneyAtStart - minMoneyAtStart)))}, + MicroAlgos: acctStake[i], SelectionID: part.VRFSecrets().PK, VoteID: part.VotingSecrets().OneTimeSignatureVerifier, } @@ -152,34 +214,37 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP MicroAlgos: basics.MicroAlgos{Raw: uint64(100000)}, } - bootstrap := bookkeeping.MakeGenesisBalances(genesis, sinkAddr, poolAddr) + for addr, data := range genesis { + g.Allocation = append(g.Allocation, bookkeeping.GenesisAllocation{ + Address: addr.String(), + State: bookkeeping.GenesisAccountData{ + Status: data.Status, + MicroAlgos: data.MicroAlgos, + VoteID: data.VoteID, + StateProofID: data.StateProofID, + SelectionID: data.SelectionID, + VoteFirstValid: data.VoteFirstValid, + VoteLastValid: data.VoteLastValid, + VoteKeyDilution: data.VoteKeyDilution, + }, + }) + } - for i, rootDirectory := range rootDirs { + nodes := make([]*AlgorandFullNode, numAccounts) + for i := range nodes { + rootDirectory := nodeInfos[i].rootDir genesisDir := filepath.Join(rootDirectory, g.ID()) - ledgerFilenamePrefix := filepath.Join(genesisDir, config.LedgerFilenamePrefix) if customConsensus != nil { - err := config.SaveConfigurableConsensus(genesisDir, customConsensus) - require.Nil(t, err) + err0 := config.SaveConfigurableConsensus(genesisDir, customConsensus) + require.Nil(t, err0) + err0 = config.LoadConfigurableConsensusProtocols(genesisDir) + require.Nil(t, err0) } - err1 := config.LoadConfigurableConsensusProtocols(genesisDir) - require.Nil(t, err1) - nodeID := fmt.Sprintf("Node%d", i) - const inMem = false - cfg, err := config.LoadConfigFromDisk(rootDirectory) - require.NoError(t, err) - cfg.Archival = true - _, err = data.LoadLedger(logging.Base().With("name", nodeID), ledgerFilenamePrefix, inMem, g.Proto, bootstrap, g.ID(), g.Hash(), nil, cfg) - require.NoError(t, err) - } - for i := range nodes { - var nodeNeighbors []string - nodeNeighbors = append(nodeNeighbors, neighbors[:i]...) - nodeNeighbors = append(nodeNeighbors, neighbors[i+1:]...) - rootDirectory := rootDirs[i] cfg, err := config.LoadConfigFromDisk(rootDirectory) + phonebook := phonebookHook(nodeInfos, i) require.NoError(t, err) - node, err := MakeFull(logging.Base().With("source", t.Name()+strconv.Itoa(i)), rootDirectory, cfg, nodeNeighbors, g) + node, err := MakeFull(logging.Base(), rootDirectory, cfg, phonebook, g) nodes[i] = node require.NoError(t, err) } @@ -190,7 +255,14 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP func TestSyncingFullNode(t *testing.T) { partitiontest.PartitionTest(t) - t.Skip("Flaky in nightly test environment") + if testing.Short() { + t.Skip("Test takes ~50 seconds.") + } + + if (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" && runtime.GOOS != "darwin") && + strings.ToUpper(os.Getenv("CIRCLECI")) == "TRUE" { + t.Skip("Test is too heavy for amd64 builder running in parallel with other packages") + } backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) defer backlogPool.Shutdown() @@ -203,7 +275,7 @@ func TestSyncingFullNode(t *testing.T) { initialRound := nodes[0].ledger.NextRound() - startAndConnectNodes(nodes, true) + startAndConnectNodes(nodes, defaultFirstNodeStartDelay) counter := 0 for tests := uint64(0); tests < 16; tests++ { @@ -252,7 +324,7 @@ func TestInitialSync(t *testing.T) { t.Skip("Test takes ~25 seconds.") } - if (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") && + if (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" && runtime.GOOS != "darwin") && strings.ToUpper(os.Getenv("CIRCLECI")) == "TRUE" { t.Skip("Test is too heavy for amd64 builder running in parallel with other packages") } @@ -267,7 +339,7 @@ func TestInitialSync(t *testing.T) { } initialRound := nodes[0].ledger.NextRound() - startAndConnectNodes(nodes, true) + startAndConnectNodes(nodes, defaultFirstNodeStartDelay) select { case <-nodes[0].ledger.Wait(initialRound): @@ -289,7 +361,14 @@ func TestInitialSync(t *testing.T) { func TestSimpleUpgrade(t *testing.T) { partitiontest.PartitionTest(t) - t.Skip("Flaky in nightly test environment.") + if testing.Short() { + t.Skip("Test takes ~50 seconds.") + } + + if (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" && runtime.GOOS != "darwin") && + strings.ToUpper(os.Getenv("CIRCLECI")) == "TRUE" { + t.Skip("Test is too heavy for amd64 builder running in parallel with other packages") + } backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) defer backlogPool.Shutdown() @@ -338,13 +417,13 @@ func TestSimpleUpgrade(t *testing.T) { initialRound := nodes[0].ledger.NextRound() - startAndConnectNodes(nodes, false) + startAndConnectNodes(nodes, nodelayFirstNodeStartDelay) maxRounds := basics.Round(16) roundsCheckedForUpgrade := 0 for tests := basics.Round(0); tests < maxRounds; tests++ { - blocks := make([]bookkeeping.Block, len(wallets), len(wallets)) + blocks := make([]bookkeeping.Block, len(wallets)) for i := range wallets { select { case <-nodes[i].ledger.Wait(initialRound + tests): @@ -387,10 +466,13 @@ func TestSimpleUpgrade(t *testing.T) { require.Equal(t, 2, roundsCheckedForUpgrade) } -func startAndConnectNodes(nodes []*AlgorandFullNode, delayStartFirstNode bool) { +const defaultFirstNodeStartDelay = 20 * time.Second +const nodelayFirstNodeStartDelay = 0 + +func startAndConnectNodes(nodes []*AlgorandFullNode, delayStartFirstNode time.Duration) { var wg sync.WaitGroup for i := range nodes { - if delayStartFirstNode && i == 0 { + if delayStartFirstNode != nodelayFirstNodeStartDelay && i == 0 { continue } wg.Add(1) @@ -401,9 +483,9 @@ func startAndConnectNodes(nodes []*AlgorandFullNode, delayStartFirstNode bool) { } wg.Wait() - if delayStartFirstNode { + if delayStartFirstNode != nodelayFirstNodeStartDelay { connectPeers(nodes[1:]) - delayStartNode(nodes[0], nodes[1:], 20*time.Second) + delayStartNode(nodes[0], nodes[1:], delayStartFirstNode) } else { connectPeers(nodes) } @@ -754,3 +836,119 @@ func TestMaxSizesCorrect(t *testing.T) { tsSize := uint64(network.MaxMessageLength) require.Equal(t, tsSize, protocol.TopicMsgRespTag.MaxMessageSize()) } + +// TestNodeHybridTopology set ups 3 nodes network with the following topology: +// N -- R -- A and ensures N can discover A and download blocks from it. +// +// N is a non-part node that joins the network later +// R is a non-arhival relay node with block service disabled. It MUST NOT service blocks to force N to discover A. +// A is a archival node that can only provide blocks. +// Nodes N and A have only R in their initial phonebook, and all nodes are in hybrid mode. +func TestNodeHybridTopology(t *testing.T) { + partitiontest.PartitionTest(t) + + const consensusTest0 = protocol.ConsensusVersion("test0") + + configurableConsensus := make(config.ConsensusProtocols) + + testParams0 := config.Consensus[protocol.ConsensusCurrentVersion] + testParams0.AgreementFilterTimeoutPeriod0 = 500 * time.Millisecond + configurableConsensus[consensusTest0] = testParams0 + + minMoneyAtStart := 1_000_000 + maxMoneyAtStart := 100_000_000_000 + gen := rand.New(rand.NewSource(2)) + + const numAccounts = 3 + acctStake := make([]basics.MicroAlgos, numAccounts) + for i := range acctStake { + acctStake[i] = basics.MicroAlgos{Raw: uint64(minMoneyAtStart + (gen.Int() % (maxMoneyAtStart - minMoneyAtStart)))} + } + acctStake[0] = basics.MicroAlgos{} // no stake at node 0 + + configHook := func(ni nodeInfo, cfg config.Local) (nodeInfo, config.Local) { + cfg = config.GetDefaultLocal() + if ni.idx != 2 { + cfg.EnableBlockService = false + cfg.EnableGossipBlockService = false + cfg.EnableLedgerService = false + cfg.CatchpointInterval = 0 + cfg.Archival = false + } else { + // node 2 is archival + cfg.EnableBlockService = true + cfg.EnableGossipBlockService = true + cfg.EnableLedgerService = true + cfg.CatchpointInterval = 200 + cfg.Archival = true + } + if ni.idx == 0 { + // do not allow node 0 (N) to make any outgoing connections + cfg.GossipFanout = 0 + } + + cfg.NetAddress = ni.wsNetAddr() + cfg.EnableP2PHybridMode = true + cfg.EnableDHTProviders = true + cfg.P2PPersistPeerID = true + genesisDirs, err := cfg.EnsureAndResolveGenesisDirs(ni.rootDir, ni.genesis.ID(), nil) + require.NoError(t, err) + privKey, err := p2p.GetPrivKey(cfg, genesisDirs.RootGenesisDir) + require.NoError(t, err) + ni.p2pID, err = p2p.PeerIDFromPublicKey(privKey.GetPublic()) + require.NoError(t, err) + + cfg.P2PListenAddress = ni.p2pNetAddr() + return ni, cfg + } + + phonebookHook := func(ni []nodeInfo, i int) []string { + switch i { + case 0: + // node 0 (N) only accept connections to work around the peer selector + // ConnectedOut priority. TODO: merge switching to archival peers from master + // when ready. + t.Logf("Node%d phonebook: empty", i) + return []string{} + case 1: + // node 1 (R) connectes to all + t.Logf("Node%d phonebook: %s, %s, %s, %s", i, ni[0].wsNetAddr(), ni[2].wsNetAddr(), ni[0].p2pMultiAddr(), ni[2].p2pMultiAddr()) + return []string{ni[0].wsNetAddr(), ni[2].wsNetAddr(), ni[0].p2pMultiAddr(), ni[2].p2pMultiAddr()} + case 2: + // node 2 (A) connects to R + t.Logf("Node%d phonebook: %s, %s", i, ni[1].wsNetAddr(), ni[1].p2pMultiAddr()) + return []string{ni[1].wsNetAddr(), ni[1].p2pMultiAddr()} + default: + t.Errorf("not expected number of nodes: %d", i) + t.FailNow() + } + return nil + } + + backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) + defer backlogPool.Shutdown() + + nodes, wallets := setupFullNodesEx(t, consensusTest0, backlogPool, configurableConsensus, acctStake, configHook, phonebookHook) + require.Len(t, nodes, 3) + require.Len(t, wallets, 3) + for i := 0; i < len(nodes); i++ { + defer os.Remove(wallets[i]) + defer nodes[i].Stop() + } + + startAndConnectNodes(nodes, 10*time.Second) + + initialRound := nodes[0].ledger.NextRound() + targetRound := initialRound + 10 + + select { + case <-nodes[0].ledger.Wait(targetRound): + e0, err := nodes[0].ledger.Block(targetRound) + require.NoError(t, err) + e1, err := nodes[1].ledger.Block(targetRound) + require.NoError(t, err) + require.Equal(t, e1.Hash(), e0.Hash()) + case <-time.After(120 * time.Second): + require.Fail(t, fmt.Sprintf("no block notification for wallet: %v.", wallets[0])) + } +} diff --git a/tools/block-generator/go.mod b/tools/block-generator/go.mod index 435e1ccf69..ae047a3fb1 100644 --- a/tools/block-generator/go.mod +++ b/tools/block-generator/go.mod @@ -51,19 +51,18 @@ require ( github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect + github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect - github.com/huin/goupnp v1.2.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect + github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/boxo v0.10.0 // indirect github.com/ipfs/go-cid v0.4.1 // indirect @@ -76,7 +75,7 @@ require ( github.com/jbenet/goprocess v0.1.4 // indirect github.com/jmespath/go-jmespath v0.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.17.2 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/koron/go-ssdp v0.0.4 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -84,23 +83,23 @@ require ( github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect - github.com/libp2p/go-libp2p v0.29.1 // indirect + github.com/libp2p/go-libp2p v0.32.2 // indirect github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect github.com/libp2p/go-libp2p-kad-dht v0.24.3 // indirect github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect - github.com/libp2p/go-libp2p-pubsub v0.9.3 // indirect + github.com/libp2p/go-libp2p-pubsub v0.10.0 // indirect github.com/libp2p/go-libp2p-record v0.2.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-nat v0.2.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect - github.com/libp2p/go-reuseport v0.3.0 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect github.com/libp2p/go-yamux/v4 v4.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/miekg/dns v1.1.55 // indirect + github.com/miekg/dns v1.1.56 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect @@ -108,17 +107,17 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr v0.10.1 // indirect + github.com/multiformats/go-multiaddr v0.12.0 // indirect github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multicodec v0.9.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.4.1 // indirect + github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/olivere/elastic v6.2.14+incompatible // indirect - github.com/onsi/ginkgo/v2 v2.11.0 // indirect - github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/onsi/ginkgo/v2 v2.13.0 // indirect + github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect @@ -130,10 +129,9 @@ require ( github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-19 v0.3.3 // indirect - github.com/quic-go/qtls-go1-20 v0.2.3 // indirect - github.com/quic-go/quic-go v0.36.3 // indirect - github.com/quic-go/webtransport-go v0.5.3 // indirect + github.com/quic-go/qtls-go1-20 v0.3.4 // indirect + github.com/quic-go/quic-go v0.39.4 // indirect + github.com/quic-go/webtransport-go v0.6.0 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect @@ -144,19 +142,19 @@ require ( go.opentelemetry.io/otel v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/otel/trace v1.16.0 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/dig v1.17.0 // indirect - go.uber.org/fx v1.20.0 // indirect + go.uber.org/dig v1.17.1 // indirect + go.uber.org/fx v1.20.1 // indirect + go.uber.org/mock v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.24.0 // indirect + go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.14.0 // indirect - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect - golang.org/x/mod v0.12.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sync v0.3.0 // indirect + golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect - golang.org/x/tools v0.11.0 // indirect + golang.org/x/tools v0.14.0 // indirect gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/sohlich/elogrus.v3 v3.0.0-20180410122755-1fa29e2f2009 // indirect diff --git a/tools/block-generator/go.sum b/tools/block-generator/go.sum index 80fcb7825c..037443f8ed 100644 --- a/tools/block-generator/go.sum +++ b/tools/block-generator/go.sum @@ -169,8 +169,6 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -205,8 +203,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= -github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b h1:RMpPgZTSApbPf7xaVel+QkoGPRLFLrwFO89uDUHEGf0= +github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -231,12 +229,12 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= -github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= -github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -291,8 +289,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -318,16 +316,16 @@ github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38y github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= -github.com/libp2p/go-libp2p v0.29.1 h1:yNeg6XgP8gbdc4YSrwiIt5T1TGOrVjH8dzl8h0GIOfQ= -github.com/libp2p/go-libp2p v0.29.1/go.mod h1:20El+LLy3/YhdUYIvGbLnvVJN32nMdqY6KXBENRAfLY= +github.com/libp2p/go-libp2p v0.32.2 h1:s8GYN4YJzgUoyeYNPdW7JZeZ5Ee31iNaIBfGYMAY4FQ= +github.com/libp2p/go-libp2p v0.32.2/go.mod h1:E0LKe+diV/ZVJVnOJby8VC5xzHF0660osg71skcxJvk= github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= github.com/libp2p/go-libp2p-kad-dht v0.24.3 h1:VjxtDVWaaf4UFjGBf+yl2JCiGaHx7+ctAUa9oJCR3QE= github.com/libp2p/go-libp2p-kad-dht v0.24.3/go.mod h1:BShPzRbK6+fN3hk8a0WGAYKpb8m4k+DtchkqouGTrSg= github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0= github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0= -github.com/libp2p/go-libp2p-pubsub v0.9.3 h1:ihcz9oIBMaCK9kcx+yHWm3mLAFBMAUsM4ux42aikDxo= -github.com/libp2p/go-libp2p-pubsub v0.9.3/go.mod h1:RYA7aM9jIic5VV47WXu4GkcRxRhrdElWf8xtyli+Dzc= +github.com/libp2p/go-libp2p-pubsub v0.10.0 h1:wS0S5FlISavMaAbxyQn3dxMOe2eegMfswM471RuHJwA= +github.com/libp2p/go-libp2p-pubsub v0.10.0/go.mod h1:1OxbaT/pFRO5h+Dpze8hdHQ63R0ke55XTs6b6NwLLkw= github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= @@ -337,8 +335,8 @@ github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= -github.com/libp2p/go-reuseport v0.3.0 h1:iiZslO5byUYZEg9iCwJGf5h+sf1Agmqx2V2FDjPyvUw= -github.com/libp2p/go-reuseport v0.3.0/go.mod h1:laea40AimhtfEqysZ71UpYj4S+R9VpH8PgqLo7L+SwI= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -353,8 +351,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -366,8 +364,8 @@ github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= -github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -395,8 +393,8 @@ github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= -github.com/multiformats/go-multiaddr v0.10.1 h1:HghtFrWyZEPrpTvgAMFJi6gFdgHfs2cb0pyfDsk+lqU= -github.com/multiformats/go-multiaddr v0.10.1/go.mod h1:jLEZsA61rwWNZQTHHnqq2HNa+4os/Hz54eqiRnsRqYQ= +github.com/multiformats/go-multiaddr v0.12.0 h1:1QlibTFkoXJuDjjYsMHhE73TnzJQl8FSWatk/0gxGzE= +github.com/multiformats/go-multiaddr v0.12.0/go.mod h1:WmZXgObOQOYp9r3cslLlppkrz1FYSHmE834dfz/lWu8= github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= @@ -408,8 +406,8 @@ github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI1 github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= -github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= +github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= +github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= @@ -424,13 +422,14 @@ github.com/olivere/elastic v6.2.14+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGe github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= -github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= @@ -464,14 +463,12 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-19 v0.3.3 h1:wznEHvJwd+2X3PqftRha0SUKmGsnb6dfArMhy9PeJVE= -github.com/quic-go/qtls-go1-19 v0.3.3/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.2.3 h1:m575dovXn1y2ATOb1XrRFcrv0F+EQmlowTkoraNkDPI= -github.com/quic-go/qtls-go1-20 v0.2.3/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/quic-go/quic-go v0.36.3 h1:f+yOqeGhMoRX7/M3wmEw/djhzKWr15FtQysox85/834= -github.com/quic-go/quic-go v0.36.3/go.mod h1:qxQumdeKw5GmWs1OsTZZnOxzSI+RJWuhf1O8FN35L2o= -github.com/quic-go/webtransport-go v0.5.3 h1:5XMlzemqB4qmOlgIus5zB45AcZ2kCgCy2EptUrfOPWU= -github.com/quic-go/webtransport-go v0.5.3/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= +github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/quic-go v0.39.4 h1:PelfiuG7wXEffUT2yceiqz5V6Pc0TA5ruOd1LcmFc1s= +github.com/quic-go/quic-go v0.39.4/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= +github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY= +github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -585,13 +582,14 @@ go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLk go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= -go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= -go.uber.org/fx v1.20.0 h1:ZMC/pnRvhsthOZh9MZjMq5U8Or3mA9zBSPaLnzs3ihQ= -go.uber.org/fx v1.20.0/go.mod h1:qCUj0btiR3/JnanEr1TYEePfSw6o/4qYJscgvzQ5Ub0= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -599,8 +597,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -618,8 +616,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -632,8 +630,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -673,8 +671,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -736,11 +734,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 153f10d3a0168ac91bbafed9c893ee33df60da2a Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Mon, 29 Jan 2024 19:46:07 -0500 Subject: [PATCH 10/38] p2p: enable p2p http txsync (#5922) --- components/mocks/mockNetwork.go | 11 +-- network/addr_test.go | 2 + network/gossipNode.go | 5 +- network/hybridNetwork.go | 14 ++-- network/p2p/capabilities_test.go | 4 +- network/p2p/http.go | 42 +++++++++-- network/p2p/p2p.go | 3 - network/p2p/streams.go | 6 +- network/p2p/testing/httpNode.go | 116 +++++++++++++++++++++++++++++++ network/p2pNetwork.go | 52 +++++++------- network/p2pNetwork_test.go | 30 ++++---- network/wsNetwork.go | 22 ++++-- network/wsPeer.go | 8 +-- rpcs/httpTxSync.go | 23 +++--- rpcs/txService_test.go | 90 +++++++++++++++++++----- rpcs/txSyncer_test.go | 12 +--- 16 files changed, 324 insertions(+), 116 deletions(-) create mode 100644 network/p2p/testing/httpNode.go diff --git a/components/mocks/mockNetwork.go b/components/mocks/mockNetwork.go index 5d8ae9918c..a24cf8fbe8 100644 --- a/components/mocks/mockNetwork.go +++ b/components/mocks/mockNetwork.go @@ -18,6 +18,7 @@ package mocks import ( "context" + "errors" "net" "net/http" @@ -76,11 +77,6 @@ func (network *MockNetwork) GetPeers(options ...network.PeerOption) []network.Pe return nil } -// GetRoundTripper -- returns the network round tripper -func (network *MockNetwork) GetRoundTripper(peer network.Peer) http.RoundTripper { - return http.DefaultTransport -} - // Ready - always ready func (network *MockNetwork) Ready() chan struct{} { c := make(chan struct{}) @@ -115,3 +111,8 @@ func (network *MockNetwork) GetGenesisID() string { } return network.GenesisID } + +// GetHTTPClient returns a http.Client with a suitable for the network +func (network *MockNetwork) GetHTTPClient(p network.HTTPPeer) (*http.Client, error) { + return nil, errors.New("not implemented") +} diff --git a/network/addr_test.go b/network/addr_test.go index eec2eccc36..c373b2efc6 100644 --- a/network/addr_test.go +++ b/network/addr_test.go @@ -89,10 +89,12 @@ func TestParseHostOrURL(t *testing.T) { t.Run(addr, func(t *testing.T) { _, err := ParseHostOrURL(addr) require.Error(t, err, "url should fail", addr) + require.False(t, IsMultiaddr(addr)) }) t.Run(addr+"-multiaddr", func(t *testing.T) { _, err := ParseHostOrURLOrMultiaddr(addr) require.Error(t, err, "url should fail", addr) + require.False(t, IsMultiaddr(addr)) }) } diff --git a/network/gossipNode.go b/network/gossipNode.go index aa5b2741a9..d973ef8a00 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -83,8 +83,9 @@ type GossipNode interface { // ClearHandlers deregisters all the existing message handlers. ClearHandlers() - // GetRoundTripper returns a Transport that would limit the number of outgoing connections. - GetRoundTripper(peer Peer) http.RoundTripper + // GetHTTPClient returns a http.Client with a suitable for the network Transport + // that would also limit the number of outgoing connections. + GetHTTPClient(peer HTTPPeer) (*http.Client, error) // OnNetworkAdvance notifies the network library that the agreement protocol was able to make a notable progress. // this is the only indication that we have that we haven't formed a clique, where all incoming messages diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go index b6168dc71e..69c09186aa 100644 --- a/network/hybridNetwork.go +++ b/network/hybridNetwork.go @@ -180,14 +180,14 @@ func (n *HybridP2PNetwork) ClearHandlers() { n.wsNetwork.ClearHandlers() } -// GetRoundTripper returns a Transport that would limit the number of outgoing connections. -func (n *HybridP2PNetwork) GetRoundTripper(peer Peer) http.RoundTripper { - // TODO today this is used by HTTPTxSync.Sync after calling GetPeers(network.PeersPhonebookRelays) - switch p := peer.(type) { +// GetHTTPClient returns a http.Client with a suitable for the network Transport +// that would also limit the number of outgoing connections. +func (n *HybridP2PNetwork) GetHTTPClient(peer HTTPPeer) (*http.Client, error) { + switch peer.(type) { case *wsPeer: - return p.net.GetRoundTripper(peer) - case gossipSubPeer: - return p.net.GetRoundTripper(peer) + return n.wsNetwork.GetHTTPClient(peer) + case *wsPeerCore: + return n.p2pNetwork.GetHTTPClient(peer) default: panic("unrecognized peer type") } diff --git a/network/p2p/capabilities_test.go b/network/p2p/capabilities_test.go index d6694e5fb2..08e04f6f62 100644 --- a/network/p2p/capabilities_test.go +++ b/network/p2p/capabilities_test.go @@ -98,7 +98,7 @@ func setupDHTHosts(t *testing.T, numHosts int) []*dht.IpfsDHT { require.NoError(t, err) // this is a workaround for the following issue // "failed to negotiate security protocol: error reading handshake message: noise: message is too short" - // it appears simultenous connectino attempts (dht.New() attempts to connect) causes this handshake error. + // it appears simultaneous connection attempts (dht.New() attempts to connect) causes this handshake error. // https://github.com/libp2p/go-libp2p-noise/issues/70 time.Sleep(200 * time.Millisecond) @@ -158,7 +158,7 @@ func setupCapDiscovery(t *testing.T, numHosts int, numBootstrapPeers int) []*Cap require.NoError(t, err) // this is a workaround for the following issue // "failed to negotiate security protocol: error reading handshake message: noise: message is too short" - // it appears simultenous connectino attempts (dht.New() attempts to connect) causes this handshake error. + // it appears simultaneous connection attempts (dht.New() attempts to connect) causes this handshake error. // https://github.com/libp2p/go-libp2p-noise/issues/70 time.Sleep(200 * time.Millisecond) diff --git a/network/p2p/http.go b/network/p2p/http.go index 1f39fa56c2..33a0ede570 100644 --- a/network/p2p/http.go +++ b/network/p2p/http.go @@ -18,17 +18,47 @@ package p2p import ( "net/http" + "sync" + "github.com/gorilla/mux" "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" libp2phttp "github.com/libp2p/go-libp2p/p2p/http" ) -// MakeHTTPClient creates a http.Client that uses libp2p transport for a goven protocol and peer address. -func MakeHTTPClient(protocolID string, addrInfo peer.AddrInfo) (http.Client, error) { +// algorandP2pHTTPProtocol defines a libp2p protocol name for algorand's http over p2p messages +const algorandP2pHTTPProtocol = "/algorand-http/1.0.0" + +// HTTPServer is a wrapper around libp2phttp.Host that allows registering http handlers with path parameters. +type HTTPServer struct { + libp2phttp.Host + p2phttpMux *mux.Router + p2phttpMuxRegistrarOnce sync.Once +} + +// MakeHTTPServer creates a new HTTPServer +func MakeHTTPServer(streamHost host.Host) *HTTPServer { + httpServer := HTTPServer{ + Host: libp2phttp.Host{StreamHost: streamHost}, + p2phttpMux: mux.NewRouter(), + } + return &httpServer +} + +// RegisterHTTPHandler registers a http handler with a given path. +func (s *HTTPServer) RegisterHTTPHandler(path string, handler http.Handler) { + s.p2phttpMux.Handle(path, handler) + s.p2phttpMuxRegistrarOnce.Do(func() { + s.Host.SetHTTPHandlerAtPath(algorandP2pHTTPProtocol, "/", s.p2phttpMux) + }) +} + +// MakeHTTPClient creates a http.Client that uses libp2p transport for a given protocol and peer address. +func MakeHTTPClient(addrInfo *peer.AddrInfo) (*http.Client, error) { clientStreamHost, err := libp2p.New(libp2p.NoListenAddrs) if err != nil { - return http.Client{}, err + return nil, err } client := libp2phttp.Host{StreamHost: clientStreamHost} @@ -37,10 +67,10 @@ func MakeHTTPClient(protocolID string, addrInfo peer.AddrInfo) (http.Client, err // to make a NamespaceRoundTripper that limits to specific URL paths. // First, we do not want make requests when listing peers (the main MakeHTTPClient invoker). // Secondly, this makes unit testing easier - no need to register fake handlers. - rt, err := client.NewConstrainedRoundTripper(addrInfo) + rt, err := client.NewConstrainedRoundTripper(*addrInfo) if err != nil { - return http.Client{}, err + return nil, err } - return http.Client{Transport: rt}, nil + return &http.Client{Transport: rt}, nil } diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 99f537ed36..7450f34794 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -75,9 +75,6 @@ type serviceImpl struct { // AlgorandWsProtocol defines a libp2p protocol name for algorand's websockets messages const AlgorandWsProtocol = "/algorand-ws/1.0.0" -// AlgorandP2pHTTPProtocol defines a libp2p protocol name for algorand's http over p2p messages -const AlgorandP2pHTTPProtocol = "/algorand-http/1.0.0" - const dialTimeout = 30 * time.Second // MakeHost creates a libp2p host but does not start listening. diff --git a/network/p2p/streams.go b/network/p2p/streams.go index 0e5c59d50c..160f273e17 100644 --- a/network/p2p/streams.go +++ b/network/p2p/streams.go @@ -80,7 +80,7 @@ func (n *streamManager) streamHandler(stream network.Stream) { if stream.Stat().Direction == network.DirUnknown { n.log.Warnf("Unknown direction for a steam %s to/from %s", stream.ID(), remotePeer) } else { - n.log.Warnf("Unexpected outgoing sream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) + n.log.Warnf("Unexpected outgoing stream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) } } n.handler(n.ctx, remotePeer, stream, incoming) @@ -98,7 +98,7 @@ func (n *streamManager) streamHandler(stream network.Stream) { if stream.Stat().Direction == network.DirUnknown { n.log.Warnf("streamHandler: unknown direction for a steam %s to/from %s", stream.ID(), remotePeer) } else { - n.log.Warnf("Unexpected outgoing sream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) + n.log.Warnf("Unexpected outgoing stream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) } } n.handler(n.ctx, remotePeer, stream, incoming) @@ -141,7 +141,7 @@ func (n *streamManager) Connected(net network.Network, conn network.Conn) { // a new stream created above, expected direction is outbound incoming := stream.Stat().Direction == network.DirInbound if incoming { - n.log.Warnf("Unexpected incoming sream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) + n.log.Warnf("Unexpected incoming stream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) } else { if stream.Stat().Direction == network.DirUnknown { n.log.Warnf("Connected: unknown direction for a steam %s to/from %s", stream.ID(), remotePeer) diff --git a/network/p2p/testing/httpNode.go b/network/p2p/testing/httpNode.go new file mode 100644 index 0000000000..f188abcb1a --- /dev/null +++ b/network/p2p/testing/httpNode.go @@ -0,0 +1,116 @@ +// Copyright (C) 2019-2024 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 . + +// This package wraps and re-exports the libp2p functions on order to keep +// all go-libp2p imports in one place. + +package p2p + +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" +) + +// HTTPNode is a mock network node that uses libp2p and http. +type HTTPNode struct { + mocks.MockNetwork + host.Host + httpServer *p2p.HTTPServer + peers []network.Peer + tb testing.TB + genesisID string +} + +// MakeHTTPNode returns a new P2PHTTPNode node. +func MakeHTTPNode(tb testing.TB) *HTTPNode { + p2pHost, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + require.NoError(tb, err) + + return &HTTPNode{ + Host: p2pHost, + httpServer: p2p.MakeHTTPServer(p2pHost), + tb: tb, + } +} + +// RegisterHTTPHandler registers a http handler with a given path. +func (p *HTTPNode) RegisterHTTPHandler(path string, handler http.Handler) { + p.httpServer.RegisterHTTPHandler(path, handler) +} + +// RegisterHandlers not implemented. +func (p *HTTPNode) RegisterHandlers(dispatch []network.TaggedMessageHandler) {} + +// Start starts http service +func (p *HTTPNode) Start() error { + go func() { + err := p.httpServer.Serve() + require.NoError(p.tb, err) + }() + return nil +} + +// Stop stops http service +func (p *HTTPNode) Stop() { + p.httpServer.Close() + p.Host.Close() +} + +// GetGenesisID returns genesisID +func (p *HTTPNode) GetGenesisID() string { return p.genesisID } + +// SetGenesisID sets genesisID +func (p *HTTPNode) SetGenesisID(genesisID string) { p.genesisID = genesisID } + +type httpPeer struct { + addrInfo peer.AddrInfo + tb testing.TB +} + +// GetAddress implements HTTPPeer interface returns the address of the peer +func (p httpPeer) GetAddress() string { + mas, err := peer.AddrInfoToP2pAddrs(&p.addrInfo) + require.NoError(p.tb, err) + require.Len(p.tb, mas, 1) + return mas[0].String() +} + +// GetAddress implements HTTPPeer interface and returns the http client for a peer +func (p httpPeer) GetHTTPClient() *http.Client { + c, err := p2p.MakeHTTPClient(&p.addrInfo) + require.NoError(p.tb, err) + return c +} + +// SetPeers sets peers +func (p *HTTPNode) SetPeers(other *HTTPNode) { + addrInfo := peer.AddrInfo{ID: other.ID(), Addrs: other.Addrs()} + hpeer := httpPeer{addrInfo, p.tb} + p.peers = append(p.peers, hpeer) +} + +// GetPeers returns peers +func (p *HTTPNode) GetPeers(options ...network.PeerOption) []network.Peer { + return p.peers +} diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 4cfec68bad..d546f2fe05 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -32,12 +32,11 @@ import ( "github.com/algorand/go-algorand/network/p2p/peerstore" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-deadlock" - "github.com/gorilla/mux" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" - libp2phttp "github.com/libp2p/go-libp2p/p2p/http" + "github.com/multiformats/go-multiaddr" manet "github.com/multiformats/go-multiaddr/net" ) @@ -72,10 +71,7 @@ type P2PNetwork struct { bootstrapper bootstrapper nodeInfo NodeInfo pstore *peerstore.PeerStore - httpServer libp2phttp.Host - - p2phttpMux *mux.Router - p2phttpMuxRegistarOnce sync.Once + httpServer *p2p.HTTPServer } type bootstrapper struct { @@ -171,7 +167,6 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo peerStats: make(map[peer.ID]*p2pPeerStats), nodeInfo: node, pstore: pstore, - p2phttpMux: mux.NewRouter(), } net.ctx, net.ctxCancel = context.WithCancel(context.Background()) net.handler = msgHandler{ @@ -215,9 +210,7 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo net.capabilitiesDiscovery = disc } - net.httpServer = libp2phttp.Host{ - StreamHost: h, - } + net.httpServer = p2p.MakeHTTPServer(h) err = net.setup() if err != nil { @@ -434,10 +427,7 @@ func (n *P2PNetwork) DisconnectPeers() { // RegisterHTTPHandler path accepts gorilla/mux path annotations func (n *P2PNetwork) RegisterHTTPHandler(path string, handler http.Handler) { - n.p2phttpMux.Handle(path, handler) - n.p2phttpMuxRegistarOnce.Do(func() { - n.httpServer.SetHTTPHandlerAtPath(p2p.AlgorandP2pHTTPProtocol, "/", n.p2phttpMux) - }) + n.httpServer.RegisterHTTPHandler(path, handler) } // RequestConnectOutgoing asks the system to actually connect to peers. @@ -479,7 +469,7 @@ func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { n.wsPeersLock.RUnlock() case PeersPhonebookArchivalNodes: - // query known archvial nodes from DHT if enabled + // query known archival nodes from DHT if enabled if n.config.EnableDHTProviders { const nodesToFind = 5 info, err := n.capabilitiesDiscovery.PeersForCapability(p2p.Archival, nodesToFind) @@ -500,7 +490,7 @@ func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { continue } addr := mas[0].String() - client, err := p2p.MakeHTTPClient(p2p.AlgorandP2pHTTPProtocol, addrInfo) + client, err := p2p.MakeHTTPClient(&info) if err != nil { n.log.Warnf("MakeHTTPClient failed: %v", err) continue @@ -552,9 +542,14 @@ func (n *P2PNetwork) ClearHandlers() { n.handler.ClearHandlers([]Tag{}) } -// GetRoundTripper returns a Transport that would limit the number of outgoing connections. -func (n *P2PNetwork) GetRoundTripper(peer Peer) http.RoundTripper { - return http.DefaultTransport +// GetHTTPClient returns a http.Client with a suitable for the network Transport +// that would also limit the number of outgoing connections. +func (n *P2PNetwork) GetHTTPClient(p HTTPPeer) (*http.Client, error) { + addrInfo, err := peer.AddrInfoFromString(p.GetAddress()) + if err != nil { + return nil, err + } + return p2p.MakeHTTPClient(addrInfo) } // OnNetworkAdvance notifies the network library that the agreement protocol was able to make a notable progress. @@ -569,7 +564,7 @@ func (n *P2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn net.C // wsStreamHandler is a callback that the p2p package calls when a new peer connects and establishes a // stream for the websocket protocol. -func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream network.Stream, incoming bool) { +func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, stream network.Stream, incoming bool) { if stream.Protocol() != p2p.AlgorandWsProtocol { n.log.Warnf("unknown protocol %s", stream.Protocol()) return @@ -595,7 +590,7 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream n if numOutgoingPeers >= n.config.GossipFanout { // this appears to be some auxiliary connection made by libp2p itself like DHT connection. // skip this connection since there are already enough peers - n.log.Debugf("skipping outgoing connection to peer %s: num outgoing %d > fanout %d ", peer, numOutgoingPeers, n.config.GossipFanout) + n.log.Debugf("skipping outgoing connection to peer %s: num outgoing %d > fanout %d ", p2ppeer, numOutgoingPeers, n.config.GossipFanout) stream.Close() return } @@ -611,18 +606,23 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream n ma := stream.Conn().RemoteMultiaddr() addr := ma.String() if addr == "" { - n.log.Warnf("Could not get address for peer %s", peer) + n.log.Warnf("Could not get address for peer %s", p2ppeer) } // create a wsPeer for this stream and added it to the peers map. + client, err := p2p.MakeHTTPClient(&peer.AddrInfo{ID: p2ppeer, Addrs: []multiaddr.Multiaddr{ma}}) + if err != nil { + client = nil + } + peerCore := makePeerCoreWithClient(ctx, n, n.log, n.handler.readBuffer, addr, client, addr) wsp := &wsPeer{ - wsPeerCore: makePeerCore(ctx, n, n.log, n.handler.readBuffer, addr, n.GetRoundTripper(nil), addr), + wsPeerCore: peerCore, conn: &wsPeerConnP2PImpl{stream: stream}, outgoing: !incoming, } wsp.init(n.config, outgoingMessagesBufferSize) n.wsPeersLock.Lock() - n.wsPeers[peer] = wsp - n.wsPeersToIDs[wsp] = peer + n.wsPeers[p2ppeer] = wsp + n.wsPeersToIDs[wsp] = p2ppeer n.wsPeersLock.Unlock() n.wsPeersChangeCounter.Add(1) @@ -633,7 +633,7 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, peer peer.ID, stream n msg = "Accepted incoming connection from peer %s" } localAddr, _ := n.Address() - n.log.With("event", event).With("remote", addr).With("local", localAddr).Infof(msg, peer.String()) + n.log.With("event", event).With("remote", addr).With("local", localAddr).Infof(msg, p2ppeer.String()) if n.log.GetLevel() >= logging.Debug { n.log.Debugf("streams for %s conn %s ", stream.Conn().Stat().Direction.String(), stream.Conn().ID()) diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 5cabaf7a57..49a717de92 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -36,8 +36,6 @@ import ( pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" - peerstore "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" ) @@ -53,9 +51,7 @@ func TestP2PSubmitTX(t *testing.T) { defer netA.Stop() peerInfoA := netA.service.AddrInfo() - fmt.Print("peerInfoA is ", peerInfoA) - addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) - fmt.Printf("addrsA is %v\n", addrsA) + addrsA, err := peer.AddrInfoToP2pAddrs(&peerInfoA) require.NoError(t, err) require.NotZero(t, addrsA[0]) @@ -131,7 +127,7 @@ func TestP2PSubmitWS(t *testing.T) { defer netA.Stop() peerInfoA := netA.service.AddrInfo() - addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) + addrsA, err := peer.AddrInfoToP2pAddrs(&peerInfoA) require.NoError(t, err) require.NotZero(t, addrsA[0]) @@ -337,9 +333,9 @@ func (c *mockResolveController) Resolver() dnsaddr.Resolver { type mockResolver struct{} -func (r *mockResolver) Resolve(ctx context.Context, maddr multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { - ma, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC") - return []multiaddr.Multiaddr{ma}, err +func (r *mockResolver) Resolve(ctx context.Context, _ ma.Multiaddr) ([]ma.Multiaddr, error) { + maddr, err := ma.NewMultiaddr("/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC") + return []ma.Multiaddr{maddr}, err } func TestBootstrapFunc(t *testing.T) { @@ -454,7 +450,7 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { defer netA.Stop() peerInfoA := netA.service.AddrInfo() - addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) + addrsA, err := peer.AddrInfoToP2pAddrs(&peerInfoA) require.NoError(t, err) require.NotZero(t, addrsA[0]) @@ -535,9 +531,9 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { net := nets[idx] peers := net.GetPeers(PeersPhonebookArchivalNodes) uniquePeerIDs := make(map[peer.ID]struct{}) - for _, peer := range peers { - wsPeer := peer.(*wsPeerCore) - pi, err := peerstore.AddrInfoFromString(wsPeer.rootURL) + for _, p := range peers { + wsPeer := p.(*wsPeerCore) + pi, err := peer.AddrInfoFromString(wsPeer.rootURL) require.NoError(t, err) uniquePeerIDs[pi.ID] = struct{}{} } @@ -555,7 +551,7 @@ func TestMultiaddrConversionToFrom(t *testing.T) { t.Parallel() a := "/ip4/192.168.1.1/tcp/8180/p2p/Qmewz5ZHN1AAGTarRbMupNPbZRfg3p5jUGoJ3JYEatJVVk" - ma, err := multiaddr.NewMultiaddr(a) + ma, err := ma.NewMultiaddr(a) require.NoError(t, err) require.Equal(t, a, ma.String()) @@ -601,11 +597,11 @@ func TestP2PHTTPHandler(t *testing.T) { defer netA.Stop() peerInfoA := netA.service.AddrInfo() - addrsA, err := peerstore.AddrInfoToP2pAddrs(&peerInfoA) + addrsA, err := peer.AddrInfoToP2pAddrs(&peerInfoA) require.NoError(t, err) require.NotZero(t, addrsA[0]) - httpClient, err := p2p.MakeHTTPClient(p2p.AlgorandP2pHTTPProtocol, netA.service.AddrInfo()) + httpClient, err := p2p.MakeHTTPClient(&peerInfoA) require.NoError(t, err) resp, err := httpClient.Get("/test") require.NoError(t, err) @@ -615,7 +611,7 @@ func TestP2PHTTPHandler(t *testing.T) { require.NoError(t, err) require.Equal(t, "hello", string(body)) - httpClient, err = p2p.MakeHTTPClient(p2p.AlgorandP2pHTTPProtocol, netA.service.AddrInfo()) + httpClient, err = p2p.MakeHTTPClient(&peerInfoA) require.NoError(t, err) resp, err = httpClient.Get("/bar") require.NoError(t, err) diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 56fb64db99..be283439bd 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -553,14 +553,14 @@ func (wn *WebsocketNetwork) GetPeers(options ...PeerOption) []Peer { var addrs []string addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryRelayRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(nil), "" /*origin address*/) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersPhonebookArchivalNodes: var addrs []string addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryRelayRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(nil), "" /*origin address*/) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersPhonebookArchivers: @@ -568,7 +568,7 @@ func (wn *WebsocketNetwork) GetPeers(options ...PeerOption) []Peer { var addrs []string addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryArchiverRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(nil), "" /*origin address*/) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersConnectedIn: @@ -1095,7 +1095,7 @@ func (wn *WebsocketNetwork) ServeHTTP(response http.ResponseWriter, request *htt } peer := &wsPeer{ - wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, trackedRequest.remoteAddress(), wn.GetRoundTripper(nil), trackedRequest.remoteHost), + wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, trackedRequest.remoteAddress(), wn.getRoundTripper(nil), trackedRequest.remoteHost), conn: wsPeerWebsocketConnImpl{conn}, outgoing: false, InstanceName: trackedRequest.otherInstanceName, @@ -2028,12 +2028,20 @@ func (wn *WebsocketNetwork) numOutgoingPending() int { return len(wn.tryConnectAddrs) } -// GetRoundTripper returns an http.Transport that limits the number of connection +// getRoundTripper returns an http.Transport that limits the number of connection // to comply with connectionsRateLimitingCount. -func (wn *WebsocketNetwork) GetRoundTripper(peer Peer) http.RoundTripper { +func (wn *WebsocketNetwork) getRoundTripper(peer Peer) http.RoundTripper { return &wn.transport } +// GetHTTPClient returns a http.Client with a suitable for the network Transport +// that would also limit the number of outgoing connections. +func (wn *WebsocketNetwork) GetHTTPClient(peer HTTPPeer) (*http.Client, error) { + return &http.Client{ + Transport: &wn.transport, + }, nil +} + // filterASCII filter out the non-ascii printable characters out of the given input string and // and replace these with unprintableCharacterGlyph. // It's used as a security qualifier before logging a network-provided data. @@ -2170,7 +2178,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { } peer := &wsPeer{ - wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.GetRoundTripper(nil), "" /* origin */), + wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /* origin */), conn: wsPeerWebsocketConnImpl{conn}, outgoing: true, incomingMsgFilter: wn.incomingMsgFilter, diff --git a/network/wsPeer.go b/network/wsPeer.go index 06af7c6bad..da5adb7407 100644 --- a/network/wsPeer.go +++ b/network/wsPeer.go @@ -167,7 +167,7 @@ type wsPeerCore struct { readBuffer chan<- IncomingMessage rootURL string originAddress string // incoming connection remote host - client http.Client + client *http.Client } type disconnectReason string @@ -348,11 +348,11 @@ type TCPInfoUnicastPeer interface { // Create a wsPeerCore object func makePeerCore(ctx context.Context, net GossipNode, log logging.Logger, readBuffer chan<- IncomingMessage, rootURL string, roundTripper http.RoundTripper, originAddress string) wsPeerCore { - return makePeerCoreWithClient(ctx, net, log, readBuffer, rootURL, http.Client{Transport: roundTripper}, originAddress) + return makePeerCoreWithClient(ctx, net, log, readBuffer, rootURL, &http.Client{Transport: roundTripper}, originAddress) } // Create a wsPeerCore object -func makePeerCoreWithClient(ctx context.Context, net GossipNode, log logging.Logger, readBuffer chan<- IncomingMessage, rootURL string, client http.Client, originAddress string) wsPeerCore { +func makePeerCoreWithClient(ctx context.Context, net GossipNode, log logging.Logger, readBuffer chan<- IncomingMessage, rootURL string, client *http.Client, originAddress string) wsPeerCore { return wsPeerCore{ net: net, netCtx: ctx, @@ -374,7 +374,7 @@ func (wp *wsPeerCore) GetAddress() string { // GetHTTPClient returns a client for this peer. // http.Client will maintain a cache of connections with some keepalive. func (wp *wsPeerCore) GetHTTPClient() *http.Client { - return &wp.client + return wp.client } func (wp *wsPeerCore) GetNetwork() GossipNode { diff --git a/rpcs/httpTxSync.go b/rpcs/httpTxSync.go index 2c90f40936..4d803bddda 100644 --- a/rpcs/httpTxSync.go +++ b/rpcs/httpTxSync.go @@ -103,19 +103,26 @@ func (hts *HTTPTxSync) Sync(ctx context.Context, bloom *bloom.Filter) (txgroups if !ok { return nil, fmt.Errorf("cannot HTTPTxSync non http peer %T %#v", peer, peer) } + var syncURL string hts.rootURL = hpeer.GetAddress() client := hpeer.GetHTTPClient() if client == nil { - client = &http.Client{} - client.Transport = hts.peers.GetRoundTripper(peer) + client, err = hts.peers.GetHTTPClient(hpeer) + if err != nil { + return nil, fmt.Errorf("HTTPTxSync cannot create a HTTP client for a peer %T %#v: %s", peer, peer, err.Error()) + } } - parsedURL, err := network.ParseHostOrURL(hts.rootURL) - if err != nil { - hts.log.Warnf("txSync bad url %v: %s", hts.rootURL, err) - return nil, err + if network.IsMultiaddr(hts.rootURL) { + syncURL = network.SubstituteGenesisID(hts.peers, path.Join("", TxServiceHTTPPath)) + } else { + parsedURL, err0 := network.ParseHostOrURL(hts.rootURL) + if err0 != nil { + hts.log.Warnf("txSync bad url %v: %s", hts.rootURL, err0) + return nil, err0 + } + parsedURL.Path = network.SubstituteGenesisID(hts.peers, path.Join(parsedURL.Path, TxServiceHTTPPath)) + syncURL = parsedURL.String() } - parsedURL.Path = network.SubstituteGenesisID(hts.peers, path.Join(parsedURL.Path, TxServiceHTTPPath)) - syncURL := parsedURL.String() hts.log.Infof("http sync from %s", syncURL) params := url.Values{} params.Set("bf", bloomParam) diff --git a/rpcs/txService_test.go b/rpcs/txService_test.go index 012d22082e..49cfa3bf49 100644 --- a/rpcs/txService_test.go +++ b/rpcs/txService_test.go @@ -33,6 +33,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" + p2ptesting "github.com/algorand/go-algorand/network/p2p/testing" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/bloom" ) @@ -129,27 +130,84 @@ func nodePair() (*basicRPCNode, *basicRPCNode) { return nodeA, nodeB } +func nodePairP2p(tb testing.TB) (*p2ptesting.HTTPNode, *p2ptesting.HTTPNode) { + nodeA := p2ptesting.MakeHTTPNode(tb) + addrsA := nodeA.Addrs() + require.Greater(tb, len(addrsA), 0) + + nodeB := p2ptesting.MakeHTTPNode(tb) + addrsB := nodeA.Addrs() + require.Greater(tb, len(addrsB), 0) + + nodeA.SetPeers(nodeB) + nodeB.SetPeers(nodeA) + nodeA.SetGenesisID("test genesisID") + nodeB.SetGenesisID("test genesisID") + + nodeA.Start() + nodeB.Start() + + return nodeA, nodeB +} + +// TestTxSync checks txsync on a network with two nodes, A and B func TestTxSync(t *testing.T) { partitiontest.PartitionTest(t) - // A network with two nodes, A and B - nodeA, nodeB := nodePair() - defer nodeA.stop() - defer nodeB.stop() + type txSyncNode interface { + Registrar + network.GossipNode + } - pool := makeMockPendingTxAggregate(3) - RegisterTxService(pool, nodeA, "test genesisID", config.GetDefaultLocal().TxPoolSize, config.GetDefaultLocal().TxSyncServeResponseSize) + tests := []struct { + name string + setup func(t *testing.T) (txSyncNode, txSyncNode, func()) + }{ + { + name: "tcp", + setup: func(t *testing.T) (txSyncNode, txSyncNode, func()) { + nodeA, nodeB := nodePair() + cleanup := func() { + nodeA.stop() + nodeB.stop() + } + return nodeA, nodeB, cleanup + }, + }, + { + name: "p2p", + setup: func(t *testing.T) (txSyncNode, txSyncNode, func()) { + nodeA, nodeB := nodePairP2p(t) + cleanup := func() { + nodeA.Stop() + nodeB.Stop() + } + return nodeA, nodeB, cleanup + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // A network with two nodes, A and B + nodeA, nodeB, cleanupFn := test.setup(t) + defer cleanupFn() - // B tries to fetch block - handler := mockHandler{} - syncInterval := time.Second - syncTimeout := time.Second - syncerPool := makeMockPendingTxAggregate(0) - syncer := MakeTxSyncer(syncerPool, nodeB, &handler, syncInterval, syncTimeout, config.GetDefaultLocal().TxSyncServeResponseSize) - // Since syncer is not Started, set the context here - syncer.ctx, syncer.cancel = context.WithCancel(context.Background()) - require.NoError(t, syncer.sync()) - require.Equal(t, int32(3), handler.messageCounter.Load()) + pool := makeMockPendingTxAggregate(3) + RegisterTxService(pool, nodeA, "test genesisID", config.GetDefaultLocal().TxPoolSize, config.GetDefaultLocal().TxSyncServeResponseSize) + + // B tries to fetch block + handler := mockHandler{} + syncInterval := time.Second + syncTimeout := time.Second + syncerPool := makeMockPendingTxAggregate(0) + syncer := MakeTxSyncer(syncerPool, nodeB, &handler, syncInterval, syncTimeout, config.GetDefaultLocal().TxSyncServeResponseSize) + // Since syncer is not Started, set the context here + syncer.ctx, syncer.cancel = context.WithCancel(context.Background()) + require.NoError(t, syncer.sync()) + require.Equal(t, int32(3), handler.messageCounter.Load()) + }) + } } func BenchmarkTxSync(b *testing.B) { diff --git a/rpcs/txSyncer_test.go b/rpcs/txSyncer_test.go index 61b6a769aa..6ad820b0df 100644 --- a/rpcs/txSyncer_test.go +++ b/rpcs/txSyncer_test.go @@ -170,16 +170,8 @@ func (mca *mockClientAggregator) GetPeers(options ...network.PeerOption) []netwo return mca.peers } -const numberOfPeers = 10 - -func makeMockClientAggregator(t *testing.T, failWithNil bool, failWithError bool) *mockClientAggregator { - clients := make([]network.Peer, 0) - for i := 0; i < numberOfPeers; i++ { - runner := mockRunner{failWithNil: failWithNil, failWithError: failWithError, done: make(chan *rpc.Call)} - clients = append(clients, &mockRPCClient{client: &runner, log: logging.TestingLog(t)}) - } - t.Logf("len(mca.clients) = %d", len(clients)) - return &mockClientAggregator{peers: clients} +func (mca *mockClientAggregator) GetHTTPClient(peer network.HTTPPeer) (*http.Client, error) { + return &http.Client{Transport: http.DefaultTransport}, nil } func TestSyncFromClient(t *testing.T) { From ba8b2cdec5125d1d5f51a1e3e2d59cbf32758f76 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:36:27 -0500 Subject: [PATCH 11/38] p2p: fix infinite loop in dnsaddr resolution (#5926) --- network/p2p/dnsaddr/resolve.go | 6 +++++ network/p2p/dnsaddr/resolve_test.go | 40 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/network/p2p/dnsaddr/resolve.go b/network/p2p/dnsaddr/resolve.go index 56caa84f4b..dbe2b002a1 100644 --- a/network/p2p/dnsaddr/resolve.go +++ b/network/p2p/dnsaddr/resolve.go @@ -35,8 +35,14 @@ func Iterate(initial multiaddr.Multiaddr, controller ResolveController, f func(d if resolver == nil { return errors.New("passed controller has no resolvers Iterate") } + const maxHops = 100 + hops := 0 var toResolve = []multiaddr.Multiaddr{initial} for resolver != nil && len(toResolve) > 0 { + hops++ + if hops > maxHops { + return errors.New("max hops reached while resolving dnsaddr " + initial.String()) + } curr := toResolve[0] maddrs, resolveErr := resolver.Resolve(context.Background(), curr) if resolveErr != nil { diff --git a/network/p2p/dnsaddr/resolve_test.go b/network/p2p/dnsaddr/resolve_test.go index 937e4db183..03190ab5f7 100644 --- a/network/p2p/dnsaddr/resolve_test.go +++ b/network/p2p/dnsaddr/resolve_test.go @@ -21,6 +21,7 @@ import ( "fmt" "net" "testing" + "time" "github.com/multiformats/go-multiaddr" madns "github.com/multiformats/go-multiaddr-dns" @@ -109,3 +110,42 @@ func TestMultiaddrsFromResolverDnsFailure(t *testing.T) { assert.Empty(t, maddrs) assert.ErrorContains(t, err, "always errors") } + +type mockController struct { +} + +func (c mockController) Resolver() Resolver { + return selfResolver{} +} + +func (c mockController) NextResolver() Resolver { + return nil +} + +type selfResolver struct { +} + +func (r selfResolver) Resolve(ctx context.Context, maddr multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { + return []multiaddr.Multiaddr{maddr}, nil +} + +// TestIterate ensures the Iterate() does not hang in infinite loop +// when resolver returns the same dnsaddr +func TestIterate(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + dnsAddr := "/dnsaddr/foobar.com" + require.True(t, isDnsaddr(multiaddr.StringCast(dnsAddr))) + ma, err := multiaddr.NewMultiaddr(dnsAddr) + require.NoError(t, err) + + require.Eventually(t, func() bool { + Iterate( + ma, + mockController{}, + func(dnsaddr multiaddr.Multiaddr, entries []multiaddr.Multiaddr) error { return nil }, + ) + return true + }, 100*time.Millisecond, 50*time.Millisecond) +} From e8c4ae6859ea392da2c416430e19267e40c6d716 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:06:46 -0500 Subject: [PATCH 12/38] p2p: http catchpoints support (#5924) --- agreement/gossip/network_test.go | 3 +- catchup/ledgerFetcher.go | 17 +++++--- catchup/ledgerFetcher_test.go | 54 +++++++++++++++++++++--- components/mocks/mockNetwork.go | 3 +- network/gossipNode.go | 11 ++++- network/hybridNetwork.go | 9 ++-- network/p2p/p2p.go | 51 +++++++++++++++++++++- network/p2p/p2p_test.go | 72 +++++++++++++++++++++++++++++++- network/p2p/streams.go | 20 +++++++++ network/p2p/testing/httpNode.go | 6 +++ network/p2pNetwork.go | 15 ++++++- network/p2pNetwork_test.go | 18 ++++++-- network/wsNetwork.go | 2 +- rpcs/ledgerService.go | 10 ++++- rpcs/ledgerService_test.go | 59 ++++++++++++++++++++++++++ 15 files changed, 320 insertions(+), 30 deletions(-) diff --git a/agreement/gossip/network_test.go b/agreement/gossip/network_test.go index 1a6fa80be9..e1c5f49613 100644 --- a/agreement/gossip/network_test.go +++ b/agreement/gossip/network_test.go @@ -18,7 +18,6 @@ package gossip import ( "context" - "net" "net/http" "sync" "sync/atomic" @@ -156,7 +155,7 @@ func (w *whiteholeNetwork) GetPeers(options ...network.PeerOption) []network.Pee } func (w *whiteholeNetwork) RegisterHTTPHandler(path string, handler http.Handler) { } -func (w *whiteholeNetwork) GetHTTPRequestConnection(request *http.Request) (conn net.Conn) { +func (w *whiteholeNetwork) GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettable) { return nil } diff --git a/catchup/ledgerFetcher.go b/catchup/ledgerFetcher.go index 3225d1deaf..269aed1ff4 100644 --- a/catchup/ledgerFetcher.go +++ b/catchup/ledgerFetcher.go @@ -74,13 +74,18 @@ func makeLedgerFetcher(net network.GossipNode, accessor ledger.CatchpointCatchup } func (lf *ledgerFetcher) requestLedger(ctx context.Context, peer network.HTTPPeer, round basics.Round, method string) (*http.Response, error) { - parsedURL, err := network.ParseHostOrURL(peer.GetAddress()) - if err != nil { - return nil, err - } + var ledgerURL string + if network.IsMultiaddr(peer.GetAddress()) { + ledgerURL = network.SubstituteGenesisID(lf.net, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36)) + } else { - parsedURL.Path = network.SubstituteGenesisID(lf.net, path.Join(parsedURL.Path, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36))) - ledgerURL := parsedURL.String() + parsedURL, err := network.ParseHostOrURL(peer.GetAddress()) + if err != nil { + return nil, err + } + parsedURL.Path = network.SubstituteGenesisID(lf.net, path.Join(parsedURL.Path, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36))) + ledgerURL = parsedURL.String() + } lf.log.Debugf("ledger %s %#v peer %#v %T", method, ledgerURL, peer, peer) request, err := http.NewRequestWithContext(ctx, method, ledgerURL, nil) if err != nil { diff --git a/catchup/ledgerFetcher_test.go b/catchup/ledgerFetcher_test.go index 6bbde32120..321227890b 100644 --- a/catchup/ledgerFetcher_test.go +++ b/catchup/ledgerFetcher_test.go @@ -17,6 +17,7 @@ package catchup import ( + "archive/tar" "context" "fmt" "net" @@ -30,6 +31,8 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/logging" + p2ptesting "github.com/algorand/go-algorand/network/p2p/testing" + "github.com/algorand/go-algorand/rpcs" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -125,7 +128,7 @@ func TestLedgerFetcherErrorResponseHandling(t *testing.T) { } } -func TestLedgerFetcherHeadLedger(t *testing.T) { +func TestLedgerFetcher(t *testing.T) { partitiontest.PartitionTest(t) // create a dummy server. @@ -136,16 +139,19 @@ func TestLedgerFetcherHeadLedger(t *testing.T) { listener, err := net.Listen("tcp", "localhost:") var httpServerResponse = 0 - var contentTypes = make([]string, 0) require.NoError(t, err) go s.Serve(listener) defer s.Close() defer listener.Close() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - for _, contentType := range contentTypes { - w.Header().Add("Content-Type", contentType) + if req.Method == http.MethodHead { + w.WriteHeader(httpServerResponse) + } else { + w.Header().Add("Content-Type", rpcs.LedgerResponseContentType) + w.WriteHeader(httpServerResponse) + wtar := tar.NewWriter(w) + wtar.Close() } - w.WriteHeader(httpServerResponse) }) successPeer := testHTTPPeer(listener.Addr().String()) lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{}, config.GetDefaultLocal()) @@ -169,8 +175,46 @@ func TestLedgerFetcherHeadLedger(t *testing.T) { err = lf.headLedger(context.Background(), &successPeer, basics.Round(0)) require.NoError(t, err) + httpServerResponse = http.StatusOK + err = lf.downloadLedger(context.Background(), &successPeer, basics.Round(0)) + require.NoError(t, err) + // headLedger 500 response httpServerResponse = http.StatusInternalServerError err = lf.headLedger(context.Background(), &successPeer, basics.Round(0)) require.Equal(t, fmt.Errorf("headLedger error response status code %d", http.StatusInternalServerError), err) } + +func TestLedgerFetcherP2P(t *testing.T) { + partitiontest.PartitionTest(t) + + mux := http.NewServeMux() + nodeA := p2ptesting.MakeHTTPNode(t) + nodeA.RegisterHTTPHandler("/v1/ledger/0", mux) + var httpServerResponse = 0 + mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodHead { + w.WriteHeader(httpServerResponse) + } else { + w.Header().Add("Content-Type", rpcs.LedgerResponseContentType) + w.WriteHeader(httpServerResponse) + wtar := tar.NewWriter(w) + wtar.Close() + } + }) + + nodeA.Start() + defer nodeA.Stop() + + successPeer := nodeA.GetHTTPPeer() + lf := makeLedgerFetcher(nodeA, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{}, config.GetDefaultLocal()) + + // headLedger 200 response + httpServerResponse = http.StatusOK + err := lf.headLedger(context.Background(), successPeer, basics.Round(0)) + require.NoError(t, err) + + httpServerResponse = http.StatusOK + err = lf.downloadLedger(context.Background(), successPeer, basics.Round(0)) + require.NoError(t, err) +} diff --git a/components/mocks/mockNetwork.go b/components/mocks/mockNetwork.go index a24cf8fbe8..9c8fc97652 100644 --- a/components/mocks/mockNetwork.go +++ b/components/mocks/mockNetwork.go @@ -19,7 +19,6 @@ package mocks import ( "context" "errors" - "net" "net/http" "github.com/algorand/go-algorand/network" @@ -100,7 +99,7 @@ func (network *MockNetwork) RegisterHTTPHandler(path string, handler http.Handle func (network *MockNetwork) OnNetworkAdvance() {} // GetHTTPRequestConnection - empty implementation -func (network *MockNetwork) GetHTTPRequestConnection(request *http.Request) (conn net.Conn) { +func (network *MockNetwork) GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettable) { return nil } diff --git a/network/gossipNode.go b/network/gossipNode.go index d973ef8a00..0c6e0b06cc 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -18,9 +18,9 @@ package network import ( "context" - "net" "net/http" "strings" + "time" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/protocol" @@ -52,6 +52,13 @@ const ( PeersPhonebookArchivers PeerOption = iota ) +// DeadlineSettable abstracts net.Conn and related types as deadline-settable +type DeadlineSettable interface { + SetDeadline(time.Time) error + SetReadDeadline(time.Time) error + SetWriteDeadline(time.Time) error +} + // GossipNode represents a node in the gossip network type GossipNode interface { Address() (string, bool) @@ -95,7 +102,7 @@ type GossipNode interface { // GetHTTPRequestConnection returns the underlying connection for the given request. Note that the request must be the same // request that was provided to the http handler ( or provide a fallback Context() to that ) - GetHTTPRequestConnection(request *http.Request) (conn net.Conn) + GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettable) // GetGenesisID returns the network-specific genesisID. GetGenesisID() string diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go index 69c09186aa..d66b794f24 100644 --- a/network/hybridNetwork.go +++ b/network/hybridNetwork.go @@ -19,7 +19,6 @@ package network import ( "context" "fmt" - "net" "net/http" "sync" @@ -206,8 +205,12 @@ func (n *HybridP2PNetwork) OnNetworkAdvance() { // GetHTTPRequestConnection returns the underlying connection for the given request. Note that the request must be the same // request that was provided to the http handler ( or provide a fallback Context() to that ) -func (n *HybridP2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn net.Conn) { - return nil +func (n *HybridP2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettable) { + conn = n.wsNetwork.GetHTTPRequestConnection(request) + if conn != nil { + return conn + } + return n.p2pNetwork.GetHTTPRequestConnection(request) } // GetGenesisID returns the network-specific genesisID. diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 7450f34794..b399c569b4 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -35,6 +35,8 @@ import ( "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" + libp2phttp "github.com/libp2p/go-libp2p/p2p/http" "github.com/libp2p/go-libp2p/p2p/muxer/yamux" "github.com/libp2p/go-libp2p/p2p/security/noise" "github.com/libp2p/go-libp2p/p2p/transport/tcp" @@ -56,6 +58,8 @@ type Service interface { ListPeersForTopic(topic string) []peer.ID Subscribe(topic string, val pubsub.ValidatorEx) (*pubsub.Subscription, error) Publish(ctx context.Context, topic string, data []byte) error + + GetStream(peer.ID) (network.Stream, bool) } // serviceImpl manages integration with libp2p and implements the Service interface @@ -116,7 +120,47 @@ func MakeHost(cfg config.Local, datadir string, pstore peerstore.Peerstore) (hos noListenAddrs, libp2p.Security(noise.ID, noise.New), ) - return host, listenAddr, err + return &StreamChainingHost{ + Host: host, + handlers: map[protocol.ID][]network.StreamHandler{}, + }, listenAddr, err +} + +// StreamChainingHost is a wrapper around host.Host that overrides SetStreamHandler +// to allow chaining multiple handlers for the same protocol. +// Note, there should be probably only single handler that writes/reads streams. +type StreamChainingHost struct { + host.Host + handlers map[protocol.ID][]network.StreamHandler + mutex deadlock.Mutex +} + +// SetStreamHandler overrides the host.Host.SetStreamHandler method for chaining multiple handlers. +// Function objects are not comparable so theoretically it could have duplicates. +// The main use case is to track HTTP streams for ProtocolIDForMultistreamSelect = "/http/1.1" +// so it could just filter for such protocol if there any issues with other protocols like kad or mesh. +func (h *StreamChainingHost) SetStreamHandler(pid protocol.ID, handler network.StreamHandler) { + h.mutex.Lock() + defer h.mutex.Unlock() + + handlers := h.handlers[pid] + if len(handlers) == 0 { + // no other handlers, do not set a proxy handler + h.Host.SetStreamHandler(pid, handler) + h.handlers[pid] = append(handlers, handler) + return + } + // otherwise chain the handlers with a copy of the existing handlers + handlers = append(handlers, handler) + // copy to save it in the closure and call lock free + currentHandlers := make([]network.StreamHandler, len(handlers)) + copy(currentHandlers, handlers) + h.Host.SetStreamHandler(pid, func(s network.Stream) { + for _, h := range currentHandlers { + h(s) + } + }) + h.handlers[pid] = handlers } // MakeService creates a P2P service instance @@ -125,6 +169,7 @@ func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, h ho sm := makeStreamManager(ctx, log, h, wsStreamHandler) h.Network().Notify(sm) h.SetStreamHandler(AlgorandWsProtocol, sm.streamHandler) + h.SetStreamHandler(libp2phttp.ProtocolIDForMultistreamSelect, sm.streamHandlerHTTP) ps, err := makePubSub(ctx, cfg, h) if err != nil { @@ -218,6 +263,10 @@ func (s *serviceImpl) ClosePeer(peer peer.ID) error { return s.host.Network().ClosePeer(peer) } +func (s *serviceImpl) GetStream(peerID peer.ID) (network.Stream, bool) { + return s.streams.getStream(peerID) +} + // 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 558131fe48..005dbc8330 100644 --- a/network/p2p/p2p_test.go +++ b/network/p2p/p2p_test.go @@ -17,11 +17,20 @@ package p2p import ( + "context" "fmt" + "sync/atomic" "testing" + "time" - "github.com/algorand/go-algorand/test/partitiontest" + "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-algorand/config" + "github.com/algorand/go-algorand/network/p2p/peerstore" + "github.com/algorand/go-algorand/test/partitiontest" ) // Tests the helper function netAddressToListenAddress which converts @@ -74,3 +83,64 @@ func TestNetAddressToListenAddress(t *testing.T) { }) } } + +func TestP2PStreamingHost(t *testing.T) { + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + dir := t.TempDir() + pstore, err := peerstore.NewPeerStore(nil) + require.NoError(t, err) + h, la, err := MakeHost(cfg, dir, pstore) + require.NoError(t, err) + + var h1calls atomic.Int64 + h1 := func(network.Stream) { + h1calls.Add(1) + } + var h2calls atomic.Int64 + h2 := func(network.Stream) { + h2calls.Add(1) + } + + ma, err := multiaddr.NewMultiaddr(la) + require.NoError(t, err) + h.Network().Listen(ma) + defer h.Close() + + h.SetStreamHandler(AlgorandWsProtocol, h1) + h.SetStreamHandler(AlgorandWsProtocol, h2) + + addrInfo := peer.AddrInfo{ + ID: h.ID(), + Addrs: h.Addrs(), + } + cpstore, err := peerstore.NewPeerStore([]*peer.AddrInfo{&addrInfo}) + require.NoError(t, err) + c, _, err := MakeHost(cfg, dir, cpstore) + require.NoError(t, err) + defer c.Close() + + s1, err := c.NewStream(context.Background(), h.ID(), AlgorandWsProtocol) + require.NoError(t, err) + s1.Write([]byte("hello")) + defer s1.Close() + + require.Eventually(t, func() bool { + return h1calls.Load() == 1 && h2calls.Load() == 1 + }, 5*time.Second, 100*time.Millisecond) + + // ensure a single handler also works as expected + h1calls.Store(0) + h.SetStreamHandler(algorandP2pHTTPProtocol, h1) + + s2, err := c.NewStream(context.Background(), h.ID(), algorandP2pHTTPProtocol) + require.NoError(t, err) + s2.Write([]byte("hello")) + defer s2.Close() + + require.Eventually(t, func() bool { + return h1calls.Load() == 1 + }, 5*time.Second, 100*time.Millisecond) + +} diff --git a/network/p2p/streams.go b/network/p2p/streams.go index 160f273e17..0961141a0c 100644 --- a/network/p2p/streams.go +++ b/network/p2p/streams.go @@ -104,6 +104,20 @@ func (n *streamManager) streamHandler(stream network.Stream) { n.handler(n.ctx, remotePeer, stream, incoming) } +// streamHandlerHTTP tracks the ProtocolIDForMultistreamSelect = "/http/1.1" streams +func (n *streamManager) streamHandlerHTTP(stream network.Stream) { + n.streamsLock.Lock() + defer n.streamsLock.Unlock() + n.streams[stream.Conn().LocalPeer()] = stream +} + +func (n *streamManager) getStream(peerID peer.ID) (network.Stream, bool) { + n.streamsLock.Lock() + defer n.streamsLock.Unlock() + stream, ok := n.streams[peerID] + return stream, ok +} + // Connected is called when a connection is opened func (n *streamManager) Connected(net network.Network, conn network.Conn) { remotePeer := conn.RemotePeer() @@ -160,6 +174,12 @@ func (n *streamManager) Disconnected(net network.Network, conn network.Conn) { stream.Close() delete(n.streams, conn.RemotePeer()) } + + stream, ok = n.streams[conn.LocalPeer()] + if ok { + stream.Close() + delete(n.streams, conn.LocalPeer()) + } } // Listen is called when network starts listening on an addr diff --git a/network/p2p/testing/httpNode.go b/network/p2p/testing/httpNode.go index f188abcb1a..523cdc5d4c 100644 --- a/network/p2p/testing/httpNode.go +++ b/network/p2p/testing/httpNode.go @@ -77,6 +77,12 @@ func (p *HTTPNode) Stop() { p.Host.Close() } +// GetHTTPPeer returns the http peer for connecting to this node +func (p *HTTPNode) GetHTTPPeer() network.Peer { + addrInfo := peer.AddrInfo{ID: p.ID(), Addrs: p.Addrs()} + return httpPeer{addrInfo, p.tb} +} + // GetGenesisID returns genesisID func (p *HTTPNode) GetGenesisID() string { return p.genesisID } diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index d546f2fe05..36b9e74eb0 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -560,7 +560,20 @@ func (n *P2PNetwork) OnNetworkAdvance() {} // GetHTTPRequestConnection returns the underlying connection for the given request. Note that the request must be the same // request that was provided to the http handler ( or provide a fallback Context() to that ) -func (n *P2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn net.Conn) { return nil } +func (n *P2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettable) { + addr := request.Context().Value(http.LocalAddrContextKey).(net.Addr) + peerID, err := peer.Decode(addr.String()) + if err != nil { + n.log.Infof("GetHTTPRequestConnection failed to decode %s", addr.String()) + return nil + } + conn, ok := n.service.GetStream(peerID) + if !ok { + n.log.Warnf("GetHTTPRequestConnection no such stream for peer %s", peerID.String()) + return nil + } + return conn +} // wsStreamHandler is a callback that the p2p package calls when a new peer connects and establishes a // stream for the websocket protocol. diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 49a717de92..e64f4df85e 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -246,6 +246,10 @@ func (s *mockService) Publish(ctx context.Context, topic string, data []byte) er return nil } +func (s *mockService) GetStream(peer.ID) (network.Stream, bool) { + return nil, false +} + func makeMockService(id peer.ID, addrs []ma.Multiaddr) *mockService { return &mockService{ id: id, @@ -568,11 +572,17 @@ func TestMultiaddrConversionToFrom(t *testing.T) { } type p2phttpHandler struct { + tb testing.TB retData string + net GossipNode } func (h *p2phttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte(h.retData)) + if r.URL.Path == "/check-conn" { + c := h.net.GetHTTPRequestConnection(r) + require.NotNil(h.tb, c) + } } func TestP2PHTTPHandler(t *testing.T) { @@ -587,11 +597,11 @@ func TestP2PHTTPHandler(t *testing.T) { netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, &nopeNodeInfo{}) require.NoError(t, err) - h := &p2phttpHandler{"hello"} + h := &p2phttpHandler{t, "hello", nil} netA.RegisterHTTPHandler("/test", h) - h2 := &p2phttpHandler{"world"} - netA.RegisterHTTPHandler("/bar", h2) + h2 := &p2phttpHandler{t, "world", netA} + netA.RegisterHTTPHandler("/check-conn", h2) netA.Start() defer netA.Stop() @@ -613,7 +623,7 @@ func TestP2PHTTPHandler(t *testing.T) { httpClient, err = p2p.MakeHTTPClient(&peerInfoA) require.NoError(t, err) - resp, err = httpClient.Get("/bar") + resp, err = httpClient.Get("/check-conn") require.NoError(t, err) defer resp.Body.Close() diff --git a/network/wsNetwork.go b/network/wsNetwork.go index a05790d644..cdb3b4c635 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -1019,7 +1019,7 @@ func (wn *WebsocketNetwork) checkIncomingConnectionVariables(response http.Respo // request that was provided to the http handler ( or provide a fallback Context() to that ) // if the provided request has no associated connection, it returns nil. ( this should not happen for any http request that was registered // by WebsocketNetwork ) -func (wn *WebsocketNetwork) GetHTTPRequestConnection(request *http.Request) (conn net.Conn) { +func (wn *WebsocketNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettable) { if wn.requestsTracker != nil { conn = wn.requestsTracker.GetRequestConnection(request) } diff --git a/rpcs/ledgerService.go b/rpcs/ledgerService.go index b3742bb985..e020a9bd6a 100644 --- a/rpcs/ledgerService.go +++ b/rpcs/ledgerService.go @@ -60,19 +60,25 @@ type LedgerForService interface { GetCatchpointStream(round basics.Round) (ledger.ReadCloseSizer, error) } +// httpGossipNode is a reduced interface for the gossipNode that only includes the methods needed by the LedgerService +type httpGossipNode interface { + RegisterHTTPHandler(path string, handler http.Handler) + GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettable) +} + // LedgerService represents the Ledger RPC API type LedgerService struct { // running is non-zero once the service is running, and zero when it's not running. it needs to be at a 32-bit aligned address for RasPI support. running atomic.Int32 ledger LedgerForService genesisID string - net network.GossipNode + net httpGossipNode enableService bool stopping sync.WaitGroup } // MakeLedgerService creates a LedgerService around the provider Ledger and registers it with the HTTP router -func MakeLedgerService(config config.Local, ledger LedgerForService, net network.GossipNode, genesisID string) *LedgerService { +func MakeLedgerService(config config.Local, ledger LedgerForService, net httpGossipNode, genesisID string) *LedgerService { service := &LedgerService{ ledger: ledger, genesisID: genesisID, diff --git a/rpcs/ledgerService_test.go b/rpcs/ledgerService_test.go index 1cc52fc9c0..a100f03c2b 100644 --- a/rpcs/ledgerService_test.go +++ b/rpcs/ledgerService_test.go @@ -17,6 +17,9 @@ package rpcs import ( + "archive/tar" + "bytes" + "compress/gzip" "fmt" "io" "net/http" @@ -172,3 +175,59 @@ func TestLedgerService(t *testing.T) { ledgerService.Stop() require.Equal(t, int32(0), ledgerService.running.Load()) } + +type mockSizedStream struct { + *bytes.Buffer +} + +func (mss mockSizedStream) Size() (int64, error) { + return int64(mss.Len()), nil +} + +func (mss mockSizedStream) Close() error { + return nil +} + +type mockLedgerForService struct { +} + +func (l *mockLedgerForService) GetCatchpointStream(round basics.Round) (ledger.ReadCloseSizer, error) { + buf := bytes.NewBuffer(nil) + gz := gzip.NewWriter(buf) + wtar := tar.NewWriter(gz) + wtar.Close() + gz.Close() + + buf2 := bytes.NewBuffer(buf.Bytes()) + return mockSizedStream{buf2}, nil +} + +// TestLedgerServiceP2P creates a ledger service on a node, and a p2p client tries to download +// an empty catchpoint file from the ledger service. +func TestLedgerServiceP2P(t *testing.T) { + partitiontest.PartitionTest(t) + + nodeA, nodeB := nodePairP2p(t) + defer nodeA.Stop() + defer nodeB.Stop() + + genesisID := "test GenesisID" + cfg := config.GetDefaultLocal() + cfg.EnableLedgerService = true + l := mockLedgerForService{} + ledgerService := MakeLedgerService(cfg, &l, nodeA, genesisID) + ledgerService.Start() + defer ledgerService.Stop() + + nodeA.RegisterHTTPHandler(LedgerServiceLedgerPath, ledgerService) + + httpPeer := nodeA.GetHTTPPeer().(network.HTTPPeer) + + req, err := http.NewRequest("GET", fmt.Sprintf("/v1/%s/ledger/0", genesisID), nil) + require.NoError(t, err) + resp, err := httpPeer.GetHTTPClient().Do(req) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + + require.Equal(t, http.StatusOK, resp.StatusCode) +} From 200e0bb6301f55410b18d97bb3d075973f67a743 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:07:02 -0500 Subject: [PATCH 13/38] p2p: support block request redirects for p2p addresses (#5929) --- rpcs/blockService.go | 37 +++++++++++++++++++++++-------------- rpcs/blockService_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/rpcs/blockService.go b/rpcs/blockService.go index a75df523cb..ed27c9a703 100644 --- a/rpcs/blockService.go +++ b/rpcs/blockService.go @@ -378,7 +378,6 @@ func (bs *BlockService) handleCatchupReq(ctx context.Context, reqMsg network.Inc return } respTopics, n = topicBlockBytes(bs.log, bs.ledger, basics.Round(round), string(requestType)) - return } // redirectRequest redirects the request to the next round robin fallback endpoint if available @@ -389,18 +388,24 @@ func (bs *BlockService) redirectRequest(round uint64, response http.ResponseWrit return false } - parsedURL, err := network.ParseHostOrURL(peerAddress) - if err != nil { - bs.log.Debugf("redirectRequest: %s", err.Error()) - return false + var redirectURL string + if network.IsMultiaddr(peerAddress) { + redirectURL = strings.Replace(FormatBlockQuery(round, "", bs.net), "{genesisID}", bs.genesisID, 1) + } else { + parsedURL, err := network.ParseHostOrURL(peerAddress) + if err != nil { + bs.log.Debugf("redirectRequest: %s", err.Error()) + return false + } + parsedURL.Path = strings.Replace(FormatBlockQuery(round, parsedURL.Path, bs.net), "{genesisID}", bs.genesisID, 1) + redirectURL = parsedURL.String() } - parsedURL.Path = strings.Replace(FormatBlockQuery(round, parsedURL.Path, bs.net), "{genesisID}", bs.genesisID, 1) - http.Redirect(response, request, parsedURL.String(), http.StatusTemporaryRedirect) - bs.log.Debugf("redirectRequest: redirected block request to %s", parsedURL.String()) + http.Redirect(response, request, redirectURL, http.StatusTemporaryRedirect) + bs.log.Debugf("redirectRequest: redirected block request to %s", redirectURL) return true } -// getNextCustomFallbackEndpoint returns the next custorm fallback endpoint in RR ordering +// getNextCustomFallbackEndpoint returns the next custom fallback endpoint in RR ordering func (bs *BlockService) getNextCustomFallbackEndpoint() (endpointAddress string) { if len(bs.fallbackEndpoints.endpoints) == 0 { return @@ -493,12 +498,16 @@ func makeFallbackEndpoints(log logging.Logger, customFallbackEndpoints string) ( } endpoints := strings.Split(customFallbackEndpoints, ",") for _, ep := range endpoints { - parsed, err := network.ParseHostOrURL(ep) - if err != nil { - log.Warnf("makeFallbackEndpoints: error parsing %s %s", ep, err.Error()) - continue + if network.IsMultiaddr(ep) { + fe.endpoints = append(fe.endpoints, ep) + } else { + parsed, err := network.ParseHostOrURL(ep) + if err != nil { + log.Warnf("makeFallbackEndpoints: error parsing %s %s", ep, err.Error()) + continue + } + fe.endpoints = append(fe.endpoints, parsed.String()) } - fe.endpoints = append(fe.endpoints, parsed.String()) } return } diff --git a/rpcs/blockService_test.go b/rpcs/blockService_test.go index a908c0e856..d7ad406675 100644 --- a/rpcs/blockService_test.go +++ b/rpcs/blockService_test.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "strings" "sync" "testing" @@ -500,6 +501,7 @@ func TestRedirectExceptions(t *testing.T) { parsedURLNodeB.Path = FormatBlockQuery(uint64(4), parsedURLNodeB.Path, net2) blockURLNodeB := parsedURLNodeB.String() requestNodeB, err := http.NewRequest("GET", blockURLNodeB, nil) + require.NoError(t, err) _, err = client.Do(requestNodeB) require.Error(t, err) @@ -558,8 +560,45 @@ func addBlock(t *testing.T, ledger *data.Ledger) (timestamp int64) { func TestErrMemoryAtCapacity(t *testing.T) { partitiontest.PartitionTest(t) + t.Parallel() macError := errMemoryAtCapacity{capacity: uint64(100), used: uint64(110)} errStr := macError.Error() require.Equal(t, "block service memory over capacity: 110 / 100", errStr) } + +func TestBlockServiceRedirect(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + log := logging.TestingLog(t) + + ep1 := "http://localhost:1234" + ep2 := "/ip4/127.0.0.1/tcp/2345/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN" + endpoints := strings.Join([]string{ep1, ep2}, ",") + fb := makeFallbackEndpoints(log, endpoints) + require.Len(t, fb.endpoints, 2) + require.Equal(t, ep1, fb.endpoints[0]) + require.Equal(t, ep2, fb.endpoints[1]) + + bs := BlockService{ + net: &httpTestPeerSource{}, + fallbackEndpoints: fb, + log: log, + } + + r := httptest.NewRequest("GET", "/", strings.NewReader("")) + w := httptest.NewRecorder() + ok := bs.redirectRequest(10, w, r) + require.True(t, ok) + expectedPath := ep1 + FormatBlockQuery(10, "/", bs.net) + require.Equal(t, expectedPath, w.Result().Header.Get("Location")) + + r = httptest.NewRequest("GET", "/", strings.NewReader("")) + w = httptest.NewRecorder() + ok = bs.redirectRequest(11, w, r) + require.True(t, ok) + // for p2p nodes the url is actually a peer address in p2p network and not part of HTTP path + expectedPath = FormatBlockQuery(11, "", bs.net) + require.Equal(t, expectedPath, w.Result().Header.Get("Location")) +} From 472f01b56bc94b0926fa748a5a05e9672f3fb568 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Wed, 7 Feb 2024 11:35:02 -0500 Subject: [PATCH 14/38] p2p: rate limit outgoing p2p http connections (#5931) --- catchup/ledgerFetcher.go | 5 +- catchup/universalFetcher.go | 5 +- cmd/algod/main.go | 4 +- cmd/goal/node.go | 4 +- network/addr.go | 73 +-------------- network/addr/addr.go | 88 +++++++++++++++++++ network/{ => addr}/addr_test.go | 2 +- network/{ => limitcaller}/dialer.go | 10 +-- .../rateLimitingTransport.go | 48 +++++++--- network/p2p/http.go | 14 +++ network/p2p/peerstore/peerstore.go | 45 ++++------ network/p2p/peerstore/peerstore_test.go | 57 ++++++------ network/p2p/peerstore/utils.go | 4 +- network/p2pNetwork.go | 19 ++-- network/p2pNetwork_test.go | 13 +++ network/{ => phonebook}/phonebook.go | 13 +-- network/{ => phonebook}/phonebook_test.go | 2 +- network/requestLogger_test.go | 7 +- network/requestTracker.go | 5 +- network/requestTracker_test.go | 15 ++-- network/websocketProxy_test.go | 5 +- network/wsNetwork.go | 39 +++++--- network/wsNetwork_test.go | 83 ++++++++++------- node/node_test.go | 2 +- rpcs/blockService.go | 9 +- rpcs/blockService_test.go | 11 +-- rpcs/httpTxSync.go | 5 +- 27 files changed, 352 insertions(+), 235 deletions(-) create mode 100644 network/addr/addr.go rename network/{ => addr}/addr_test.go (99%) rename network/{ => limitcaller}/dialer.go (91%) rename network/{ => limitcaller}/rateLimitingTransport.go (58%) rename network/{ => phonebook}/phonebook.go (96%) rename network/{ => phonebook}/phonebook_test.go (99%) diff --git a/catchup/ledgerFetcher.go b/catchup/ledgerFetcher.go index 269aed1ff4..e5119578b3 100644 --- a/catchup/ledgerFetcher.go +++ b/catchup/ledgerFetcher.go @@ -33,6 +33,7 @@ import ( "github.com/algorand/go-algorand/ledger/encoded" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/go-algorand/util" ) @@ -75,11 +76,11 @@ func makeLedgerFetcher(net network.GossipNode, accessor ledger.CatchpointCatchup func (lf *ledgerFetcher) requestLedger(ctx context.Context, peer network.HTTPPeer, round basics.Round, method string) (*http.Response, error) { var ledgerURL string - if network.IsMultiaddr(peer.GetAddress()) { + if addr.IsMultiaddr(peer.GetAddress()) { ledgerURL = network.SubstituteGenesisID(lf.net, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36)) } else { - parsedURL, err := network.ParseHostOrURL(peer.GetAddress()) + parsedURL, err := addr.ParseHostOrURL(peer.GetAddress()) if err != nil { return nil, err } diff --git a/catchup/universalFetcher.go b/catchup/universalFetcher.go index 4e4d920c23..a66278189b 100644 --- a/catchup/universalFetcher.go +++ b/catchup/universalFetcher.go @@ -32,6 +32,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/rpcs" ) @@ -221,10 +222,10 @@ type HTTPFetcher struct { func (hf *HTTPFetcher) getBlockBytes(ctx context.Context, r basics.Round) (data []byte, err error) { var blockURL string - if network.IsMultiaddr(hf.rootURL) { + if addr.IsMultiaddr(hf.rootURL) { blockURL = rpcs.FormatBlockQuery(uint64(r), "", hf.net) } else { - if parsedURL, err0 := network.ParseHostOrURL(hf.rootURL); err0 == nil { + if parsedURL, err0 := addr.ParseHostOrURL(hf.rootURL); err0 == nil { parsedURL.Path = rpcs.FormatBlockQuery(uint64(r), parsedURL.Path, hf.net) blockURL = parsedURL.String() } else { diff --git a/cmd/algod/main.go b/cmd/algod/main.go index 603f543b89..b2d48687a5 100644 --- a/cmd/algod/main.go +++ b/cmd/algod/main.go @@ -32,7 +32,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" - "github.com/algorand/go-algorand/network" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/protocol" toolsnet "github.com/algorand/go-algorand/tools/network" "github.com/algorand/go-algorand/util" @@ -276,7 +276,7 @@ func run() int { // make sure that the format of each entry is valid: for idx, peer := range peerOverrideArray { - addr, addrErr := network.ParseHostOrURLOrMultiaddr(peer) + addr, addrErr := addr.ParseHostOrURLOrMultiaddr(peer) if addrErr != nil { fmt.Fprintf(os.Stderr, "Provided command line parameter '%s' is not a valid host:port pair\n", peer) return 1 diff --git a/cmd/goal/node.go b/cmd/goal/node.go index 17de96a81a..2db08fd4e5 100644 --- a/cmd/goal/node.go +++ b/cmd/goal/node.go @@ -39,7 +39,7 @@ import ( "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/libgoal" - "github.com/algorand/go-algorand/network" + naddr "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/nodecontrol" "github.com/algorand/go-algorand/util" "github.com/algorand/go-algorand/util/tokens" @@ -751,7 +751,7 @@ func verifyPeerDialArg() bool { // make sure that the format of each entry is valid: for _, peer := range strings.Split(peerDial, ";") { - _, err := network.ParseHostOrURLOrMultiaddr(peer) + _, err := naddr.ParseHostOrURLOrMultiaddr(peer) if err != nil { reportErrorf("Provided peer '%s' is not a valid peer address : %v", peer, err) return false diff --git a/network/addr.go b/network/addr.go index c12a09a4af..00eb368881 100644 --- a/network/addr.go +++ b/network/addr.go @@ -17,82 +17,17 @@ package network import ( - "errors" - "net/url" "path" - "regexp" "strings" - "github.com/multiformats/go-multiaddr" + "github.com/algorand/go-algorand/network/addr" ) -var errURLNoHost = errors.New("could not parse a host from url") - -var errURLColonHost = errors.New("host name starts with a colon") - -// HostColonPortPattern matches "^[-a-zA-Z0-9.]+:\\d+$" e.g. "foo.com.:1234" -var HostColonPortPattern = regexp.MustCompile(`^[-a-zA-Z0-9.]+:\d+$`) - -// ParseHostOrURL handles "host:port" or a full URL. -// Standard library net/url.Parse chokes on "host:port". -func ParseHostOrURL(addr string) (*url.URL, error) { - // If the entire addr is "host:port" grab that right away. - // Don't try url.Parse() because that will grab "host:" as if it were "scheme:" - if HostColonPortPattern.MatchString(addr) { - return &url.URL{Scheme: "http", Host: addr}, nil - } - parsed, err := url.Parse(addr) - if err == nil { - if parsed.Host == "" { - return nil, errURLNoHost - } - return parsed, nil - } - if strings.HasPrefix(addr, "http:") || strings.HasPrefix(addr, "https:") || strings.HasPrefix(addr, "ws:") || strings.HasPrefix(addr, "wss:") || strings.HasPrefix(addr, "://") || strings.HasPrefix(addr, "//") { - return parsed, err - } - // This turns "[::]:4601" into "http://[::]:4601" which url.Parse can do - parsed, e2 := url.Parse("http://" + addr) - if e2 == nil { - // https://datatracker.ietf.org/doc/html/rfc1123#section-2 - // first character is relaxed to allow either a letter or a digit - if parsed.Host[0] == ':' && (len(parsed.Host) < 2 || parsed.Host[1] != ':') { - return nil, errURLColonHost - } - return parsed, nil - } - return parsed, err /* return original err, not our prefix altered try */ -} - -// IsMultiaddr returns true if the provided string is a valid multiaddr. -func IsMultiaddr(addr string) bool { - if strings.HasPrefix(addr, "/") && !strings.HasPrefix(addr, "//") { // multiaddr starts with '/' but not '//' which is possible for scheme relative URLS - _, err := multiaddr.NewMultiaddr(addr) - return err == nil - } - return false -} - -// ParseHostOrURLOrMultiaddr returns an error if it could not parse the provided -// string as a valid "host:port", full URL, or multiaddr. If no error, it returns -// a host:port address, or a multiaddr. -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 - } - url, err := ParseHostOrURL(addr) - if err != nil { - return "", err - } - return url.Host, nil -} - // addrToGossipAddr parses host:port or a URL and returns the URL to the websocket interface at that address. -func (wn *WebsocketNetwork) addrToGossipAddr(addr string) (string, error) { - parsedURL, err := ParseHostOrURL(addr) +func (wn *WebsocketNetwork) addrToGossipAddr(a string) (string, error) { + parsedURL, err := addr.ParseHostOrURL(a) if err != nil { - wn.log.Warnf("could not parse addr %#v: %s", addr, err) + wn.log.Warnf("could not parse addr %#v: %s", a, err) return "", errBadAddr } parsedURL.Scheme = websocketsScheme[parsedURL.Scheme] diff --git a/network/addr/addr.go b/network/addr/addr.go new file mode 100644 index 0000000000..c8c0c0b6ab --- /dev/null +++ b/network/addr/addr.go @@ -0,0 +1,88 @@ +// Copyright (C) 2019-2024 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 addr + +import ( + "errors" + "net/url" + "regexp" + "strings" + + "github.com/multiformats/go-multiaddr" +) + +var errURLNoHost = errors.New("could not parse a host from url") + +var errURLColonHost = errors.New("host name starts with a colon") + +// HostColonPortPattern matches "^[-a-zA-Z0-9.]+:\\d+$" e.g. "foo.com.:1234" +var HostColonPortPattern = regexp.MustCompile(`^[-a-zA-Z0-9.]+:\d+$`) + +// ParseHostOrURL handles "host:port" or a full URL. +// Standard library net/url.Parse chokes on "host:port". +func ParseHostOrURL(addr string) (*url.URL, error) { + // If the entire addr is "host:port" grab that right away. + // Don't try url.Parse() because that will grab "host:" as if it were "scheme:" + if HostColonPortPattern.MatchString(addr) { + return &url.URL{Scheme: "http", Host: addr}, nil + } + parsed, err := url.Parse(addr) + if err == nil { + if parsed.Host == "" { + return nil, errURLNoHost + } + return parsed, nil + } + if strings.HasPrefix(addr, "http:") || strings.HasPrefix(addr, "https:") || strings.HasPrefix(addr, "ws:") || strings.HasPrefix(addr, "wss:") || strings.HasPrefix(addr, "://") || strings.HasPrefix(addr, "//") { + return parsed, err + } + // This turns "[::]:4601" into "http://[::]:4601" which url.Parse can do + parsed, e2 := url.Parse("http://" + addr) + if e2 == nil { + // https://datatracker.ietf.org/doc/html/rfc1123#section-2 + // first character is relaxed to allow either a letter or a digit + if parsed.Host[0] == ':' && (len(parsed.Host) < 2 || parsed.Host[1] != ':') { + return nil, errURLColonHost + } + return parsed, nil + } + return parsed, err /* return original err, not our prefix altered try */ +} + +// IsMultiaddr returns true if the provided string is a valid multiaddr. +func IsMultiaddr(addr string) bool { + if strings.HasPrefix(addr, "/") && !strings.HasPrefix(addr, "//") { // multiaddr starts with '/' but not '//' which is possible for scheme relative URLS + _, err := multiaddr.NewMultiaddr(addr) + return err == nil + } + return false +} + +// ParseHostOrURLOrMultiaddr returns an error if it could not parse the provided +// string as a valid "host:port", full URL, or multiaddr. If no error, it returns +// a host:port address, or a multiaddr. +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 + } + url, err := ParseHostOrURL(addr) + if err != nil { + return "", err + } + return url.Host, nil +} diff --git a/network/addr_test.go b/network/addr/addr_test.go similarity index 99% rename from network/addr_test.go rename to network/addr/addr_test.go index c373b2efc6..c651352dd2 100644 --- a/network/addr_test.go +++ b/network/addr/addr_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package network +package addr import ( "net/url" diff --git a/network/dialer.go b/network/limitcaller/dialer.go similarity index 91% rename from network/dialer.go rename to network/limitcaller/dialer.go index 3aa59f493d..ee9b2e364a 100644 --- a/network/dialer.go +++ b/network/limitcaller/dialer.go @@ -14,13 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package network +package limitcaller import ( "context" "net" "time" + "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-algorand/tools/network/dnssec" "github.com/algorand/go-algorand/util" ) @@ -31,14 +32,13 @@ type netDialer interface { // Dialer establish tcp-level connection with the destination type Dialer struct { - phonebook Phonebook + phonebook phonebook.Phonebook innerDialer netDialer - resolver *net.Resolver } -// makeRateLimitingDialer creates a rate limiting dialer that would limit the connections +// MakeRateLimitingDialer creates a rate limiting dialer that would limit the connections // according to the entries in the phonebook. -func makeRateLimitingDialer(phonebook Phonebook, resolver dnssec.ResolverIf) Dialer { +func MakeRateLimitingDialer(phonebook phonebook.Phonebook, resolver dnssec.ResolverIf) Dialer { var innerDialer netDialer = &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, diff --git a/network/rateLimitingTransport.go b/network/limitcaller/rateLimitingTransport.go similarity index 58% rename from network/rateLimitingTransport.go rename to network/limitcaller/rateLimitingTransport.go index 461a468da5..5b2e4e67f0 100644 --- a/network/rateLimitingTransport.go +++ b/network/limitcaller/rateLimitingTransport.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package network +package limitcaller import ( "errors" @@ -24,22 +24,32 @@ import ( "github.com/algorand/go-algorand/util" ) -// rateLimitingTransport is the transport for execute a single HTTP transaction, obtaining the Response for a given Request. -type rateLimitingTransport struct { - phonebook Phonebook - innerTransport *http.Transport +// ConnectionTimeStore is a subset of the phonebook that is used to store the connection times. +type ConnectionTimeStore interface { + GetConnectionWaitTime(addrOrInfo interface{}) (bool, time.Duration, time.Time) + UpdateConnectionTime(addrOrInfo interface{}, provisionalTime time.Time) bool +} + +// RateLimitingTransport is the transport for execute a single HTTP transaction, obtaining the Response for a given Request. +type RateLimitingTransport struct { + phonebook ConnectionTimeStore + innerTransport http.RoundTripper queueingTimeout time.Duration + targetAddr interface{} // target address for the p2p http request } +// DefaultQueueingTimeout is the default timeout for queueing the request. +const DefaultQueueingTimeout = 10 * time.Second + // ErrConnectionQueueingTimeout indicates that we've exceeded the time allocated for // queueing the current request before the request attempt could be made. var ErrConnectionQueueingTimeout = errors.New("rateLimitingTransport: queueing timeout") -// makeRateLimitingTransport creates a rate limiting http transport that would limit the requests rate +// MakeRateLimitingTransport creates a rate limiting http transport that would limit the requests rate // according to the entries in the phonebook. -func makeRateLimitingTransport(phonebook Phonebook, queueingTimeout time.Duration, dialer *Dialer, maxIdleConnsPerHost int) rateLimitingTransport { +func MakeRateLimitingTransport(phonebook ConnectionTimeStore, queueingTimeout time.Duration, dialer *Dialer, maxIdleConnsPerHost int) RateLimitingTransport { defaultTransport := http.DefaultTransport.(*http.Transport) - return rateLimitingTransport{ + return RateLimitingTransport{ phonebook: phonebook, innerTransport: &http.Transport{ Proxy: defaultTransport.Proxy, @@ -54,14 +64,30 @@ func makeRateLimitingTransport(phonebook Phonebook, queueingTimeout time.Duratio } } +// MakeRateLimitingTransportWithTransport creates a rate limiting http transport that would limit the requests rate +// according to the entries in the phonebook. +func MakeRateLimitingTransportWithTransport(phonebook ConnectionTimeStore, queueingTimeout time.Duration, rt http.RoundTripper, target interface{}, maxIdleConnsPerHost int) RateLimitingTransport { + return RateLimitingTransport{ + phonebook: phonebook, + innerTransport: rt, + queueingTimeout: queueingTimeout, + targetAddr: target, + } +} + // RoundTrip connects to the address on the named network using the provided context. // It waits if needed not to exceed connectionsRateLimitingCount. -func (r *rateLimitingTransport) RoundTrip(req *http.Request) (res *http.Response, err error) { +func (r *RateLimitingTransport) RoundTrip(req *http.Request) (res *http.Response, err error) { var waitTime time.Duration var provisionalTime time.Time queueingDeadline := time.Now().Add(r.queueingTimeout) + var host interface{} = req.Host + if len(req.Host) == 0 && req.URL != nil && len(req.URL.Host) == 0 { + // p2p/http clients have per-connection transport and address info so use that + host = r.targetAddr + } for { - _, waitTime, provisionalTime = r.phonebook.GetConnectionWaitTime(req.Host) + _, waitTime, provisionalTime = r.phonebook.GetConnectionWaitTime(host) if waitTime == 0 { break // break out of the loop and proceed to the connection } @@ -73,6 +99,6 @@ func (r *rateLimitingTransport) RoundTrip(req *http.Request) (res *http.Response return nil, ErrConnectionQueueingTimeout } res, err = r.innerTransport.RoundTrip(req) - r.phonebook.UpdateConnectionTime(req.Host, provisionalTime) + r.phonebook.UpdateConnectionTime(host, provisionalTime) return } diff --git a/network/p2p/http.go b/network/p2p/http.go index 33a0ede570..d12b51034d 100644 --- a/network/p2p/http.go +++ b/network/p2p/http.go @@ -19,7 +19,9 @@ package p2p import ( "net/http" "sync" + "time" + "github.com/algorand/go-algorand/network/limitcaller" "github.com/gorilla/mux" "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/host" @@ -74,3 +76,15 @@ func MakeHTTPClient(addrInfo *peer.AddrInfo) (*http.Client, error) { return &http.Client{Transport: rt}, nil } + +// MakeHTTPClientWithRateLimit creates a http.Client that uses libp2p transport for a given protocol and peer address. +func MakeHTTPClientWithRateLimit(addrInfo *peer.AddrInfo, pstore limitcaller.ConnectionTimeStore, queueingTimeout time.Duration, maxIdleConnsPerHost int) (*http.Client, error) { + cl, err := MakeHTTPClient(addrInfo) + if err != nil { + return nil, err + } + rlrt := limitcaller.MakeRateLimitingTransportWithTransport(pstore, queueingTimeout, cl.Transport, addrInfo, maxIdleConnsPerHost) + cl.Transport = &rlrt + return cl, nil + +} diff --git a/network/p2p/peerstore/peerstore.go b/network/p2p/peerstore/peerstore.go index fa572c5912..d2a088e433 100644 --- a/network/p2p/peerstore/peerstore.go +++ b/network/p2p/peerstore/peerstore.go @@ -22,6 +22,7 @@ import ( "math/rand" "time" + "github.com/algorand/go-algorand/network/phonebook" "github.com/libp2p/go-libp2p/core/peer" libp2p "github.com/libp2p/go-libp2p/core/peerstore" mempstore "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" @@ -32,12 +33,6 @@ import ( // of how many addresses the phonebook actually has. ( with the retry-after logic applied ) const getAllAddresses = math.MaxInt32 -// PhoneBookEntryRoles defines the roles that a single entry on the phonebook can take. -// currently, we have two roles : relay role and archiver role, which are mutually exclusive. -// -//msgp:ignore PhoneBookEntryRoles -type PhoneBookEntryRoles int - const addressDataKey string = "addressData" // PeerStore implements Peerstore and CertifiedAddrBook. @@ -60,7 +55,7 @@ type addressData struct { networkNames map[string]bool // role is the role that this address serves. - role PhoneBookEntryRoles + role phonebook.PhoneBookEntryRoles // persistent is set true for peers whose record should not be removed for the peer list persistent bool @@ -103,13 +98,13 @@ func MakePhonebook(connectionsRateLimitingCount uint, } // GetAddresses returns up to N addresses, but may return fewer -func (ps *PeerStore) GetAddresses(n int, role PhoneBookEntryRoles) []string { +func (ps *PeerStore) GetAddresses(n int, role phonebook.PhoneBookEntryRoles) []string { return shuffleSelect(ps.filterRetryTime(time.Now(), role), n) } // UpdateRetryAfter updates the retryAfter time for the given address. func (ps *PeerStore) UpdateRetryAfter(addr string, retryAfter time.Time) { - info, err := PeerInfoFromDomainPort(addr) + info, err := peerInfoFromDomainPort(addr) if err != nil { return } @@ -130,12 +125,9 @@ func (ps *PeerStore) UpdateRetryAfter(addr string, retryAfter time.Time) { // The connection should be established when the waitTime is 0. // It will register a provisional next connection time when the waitTime is 0. // The provisional time should be updated after the connection with UpdateConnectionTime -func (ps *PeerStore) GetConnectionWaitTime(addr string) (bool, time.Duration, time.Time) { +func (ps *PeerStore) GetConnectionWaitTime(addr interface{}) (bool, time.Duration, time.Time) { curTime := time.Now() - info, err := PeerInfoFromDomainPort(addr) - if err != nil { - return false, 0 /* not used */, curTime /* not used */ - } + info := addr.(*peer.AddrInfo) var timeSince time.Duration var numElmtsToRemove int metadata, err := ps.Get(info.ID, addressDataKey) @@ -157,7 +149,7 @@ func (ps *PeerStore) GetConnectionWaitTime(addr string) (bool, time.Duration, ti } // Remove the expired elements from e.data[addr].recentConnectionTimes - ps.popNElements(numElmtsToRemove, peer.ID(addr)) + ps.popNElements(numElmtsToRemove, info.ID) // If there are max number of connections within the time window, wait metadata, _ = ps.Get(info.ID, addressDataKey) ad, ok = metadata.(addressData) @@ -180,11 +172,8 @@ func (ps *PeerStore) GetConnectionWaitTime(addr string) (bool, time.Duration, ti } // UpdateConnectionTime updates the connection time for the given address. -func (ps *PeerStore) UpdateConnectionTime(addr string, provisionalTime time.Time) bool { - info, err := PeerInfoFromDomainPort(addr) - if err != nil { - return false - } +func (ps *PeerStore) UpdateConnectionTime(addr interface{}, provisionalTime time.Time) bool { + info := addr.(*peer.AddrInfo) metadata, err := ps.Get(info.ID, addressDataKey) if err != nil { return false @@ -217,7 +206,7 @@ func (ps *PeerStore) UpdateConnectionTime(addr string, provisionalTime time.Time } // ReplacePeerList replaces the peer list for the given networkName and role. -func (ps *PeerStore) ReplacePeerList(addressesThey []string, networkName string, role PhoneBookEntryRoles) { +func (ps *PeerStore) ReplacePeerList(addressesThey []string, networkName string, role phonebook.PhoneBookEntryRoles) { // prepare a map of items we'd like to remove. removeItems := make(map[peer.ID]bool, 0) peerIDs := ps.Peers() @@ -232,7 +221,7 @@ func (ps *PeerStore) ReplacePeerList(addressesThey []string, networkName string, } for _, addr := range addressesThey { - info, err := PeerInfoFromDomainPort(addr) + info, err := peerInfoFromDomainPort(addr) if err != nil { return } @@ -261,13 +250,9 @@ func (ps *PeerStore) ReplacePeerList(addressesThey []string, networkName string, // AddPersistentPeers stores addresses of peers which are persistent. // i.e. they won't be replaced by ReplacePeerList calls -func (ps *PeerStore) AddPersistentPeers(dnsAddresses []string, networkName string, role PhoneBookEntryRoles) { - +func (ps *PeerStore) AddPersistentPeers(dnsAddresses []interface{}, networkName string, role phonebook.PhoneBookEntryRoles) { for _, addr := range dnsAddresses { - info, err := PeerInfoFromDomainPort(addr) - if err != nil { - return - } + info := addr.(*peer.AddrInfo) data, _ := ps.Get(info.ID, addressDataKey) if data != nil { // we already have this. @@ -291,7 +276,7 @@ func (ps *PeerStore) Length() int { } // makePhonebookEntryData creates a new address entry for provided network name and role. -func makePhonebookEntryData(networkName string, role PhoneBookEntryRoles, persistent bool) addressData { +func makePhonebookEntryData(networkName string, role phonebook.PhoneBookEntryRoles, persistent bool) addressData { pbData := addressData{ networkNames: make(map[string]bool), recentConnectionTimes: make([]time.Time, 0), @@ -334,7 +319,7 @@ func (ps *PeerStore) popNElements(n int, peerID peer.ID) { _ = ps.Put(peerID, addressDataKey, ad) } -func (ps *PeerStore) filterRetryTime(t time.Time, role PhoneBookEntryRoles) []string { +func (ps *PeerStore) filterRetryTime(t time.Time, role phonebook.PhoneBookEntryRoles) []string { o := make([]string, 0, len(ps.Peers())) for _, peerID := range ps.Peers() { data, _ := ps.Get(peerID, addressDataKey) diff --git a/network/p2p/peerstore/peerstore_test.go b/network/p2p/peerstore/peerstore_test.go index bebc2a7fe8..ebe45b87af 100644 --- a/network/p2p/peerstore/peerstore_test.go +++ b/network/p2p/peerstore/peerstore_test.go @@ -28,6 +28,7 @@ import ( libp2p "github.com/libp2p/go-libp2p/core/peerstore" "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -154,7 +155,7 @@ func TestArrayPhonebookAll(t *testing.T) { require.NoError(t, err) for _, addr := range set { entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) - info, _ := PeerInfoFromDomainPort(addr) + info, _ := peerInfoFromDomainPort(addr) ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) ph.Put(info.ID, addressDataKey, entry) } @@ -169,7 +170,7 @@ func TestArrayPhonebookUniform1(t *testing.T) { require.NoError(t, err) for _, addr := range set { entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) - info, _ := PeerInfoFromDomainPort(addr) + info, _ := peerInfoFromDomainPort(addr) ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) ph.Put(info.ID, addressDataKey, entry) } @@ -184,7 +185,7 @@ func TestArrayPhonebookUniform3(t *testing.T) { require.NoError(t, err) for _, addr := range set { entry := makePhonebookEntryData("", PhoneBookEntryRelayRole, false) - info, _ := PeerInfoFromDomainPort(addr) + info, _ := peerInfoFromDomainPort(addr) ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) ph.Put(info.ID, addressDataKey, entry) } @@ -219,7 +220,9 @@ func TestMultiPhonebook(t *testing.T) { func TestMultiPhonebookPersistentPeers(t *testing.T) { partitiontest.PartitionTest(t) - persistentPeers := []string{"a:4041"} + info, err := peerInfoFromDomainPort("a:4041") + require.NoError(t, err) + persistentPeers := []interface{}{info} set := []string{"b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} pha := make([]string, 0) for _, e := range set[:5] { @@ -236,10 +239,12 @@ func TestMultiPhonebookPersistentPeers(t *testing.T) { ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) - testPhonebookAll(t, append(set, persistentPeers...), ph) + testPhonebookAll(t, append(set, "a:4041"), ph) allAddresses := ph.GetAddresses(len(set)+len(persistentPeers), PhoneBookEntryRelayRole) for _, pp := range persistentPeers { - require.Contains(t, allAddresses, pp) + pp := pp.(*peer.AddrInfo) + // TODO: modify as needed when completely switching from peerID = "host:port" to peer.AddrInfo + require.Contains(t, allAddresses, string(pp.ID)) } } @@ -277,21 +282,21 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { require.NoError(t, err) addr1 := "addrABC:4040" addr2 := "addrXYZ:4041" - info1, _ := PeerInfoFromDomainPort(addr1) - info2, _ := PeerInfoFromDomainPort(addr2) + info1, _ := peerInfoFromDomainPort(addr1) + info2, _ := peerInfoFromDomainPort(addr2) // Address not in. Should return false - addrInPhonebook, _, provisionalTime := entries.GetConnectionWaitTime(addr1) + addrInPhonebook, _, provisionalTime := entries.GetConnectionWaitTime(info1) require.Equal(t, false, addrInPhonebook) - require.Equal(t, false, entries.UpdateConnectionTime(addr1, provisionalTime)) + require.Equal(t, false, entries.UpdateConnectionTime(info1, provisionalTime)) // Test the addresses are populated in the phonebook and a // time can be added to one of them entries.ReplacePeerList([]string{addr1, addr2}, "default", PhoneBookEntryRelayRole) - addrInPhonebook, waitTime, provisionalTime := entries.GetConnectionWaitTime(addr1) + addrInPhonebook, waitTime, provisionalTime := entries.GetConnectionWaitTime(info1) require.Equal(t, true, addrInPhonebook) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(info1, provisionalTime)) data, _ := entries.Get(info1.ID, addressDataKey) require.NotNil(t, data) ad := data.(addressData) @@ -304,9 +309,9 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { } // add another value to addr - addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr1) + addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(info1) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(info1, provisionalTime)) data, _ = entries.Get(info1.ID, addressDataKey) ad = data.(addressData) phBookData = ad.recentConnectionTimes @@ -319,9 +324,9 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { // the first time should be removed and a new one added // there should not be any wait - addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr1) + addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(info1) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(addr1, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(info1, provisionalTime)) data, _ = entries.Get(info1.ID, addressDataKey) ad = data.(addressData) phBookData2 := ad.recentConnectionTimes @@ -336,9 +341,9 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { // add 3 values to another address. should not wait // value 1 - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(info2, provisionalTime)) // introduce a gap between the two requests so that only the first will be removed later when waited // simulate passing a unit of time @@ -350,13 +355,13 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { } // value 2 - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(info2, provisionalTime)) // value 3 - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(info2, provisionalTime)) data2, _ = entries.Get(info2.ID, addressDataKey) ad2 = data2.(addressData) @@ -365,7 +370,7 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { require.Equal(t, 3, len(phBookData)) // add another element to trigger wait - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) require.Greater(t, int64(waitTime), int64(0)) // no element should be removed data2, _ = entries.Get(info2.ID, addressDataKey) @@ -380,9 +385,9 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { } // The wait should be sufficient - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(addr2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(addr2, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(info2, provisionalTime)) // only one element should be removed, and one added data2, _ = entries.Get(info2.ID, addressDataKey) ad2 = data2.(addressData) @@ -409,7 +414,7 @@ func TestPhonebookRoles(t *testing.T) { require.Equal(t, len(relaysSet)+len(archiverSet), len(ph.Peers())) require.Equal(t, len(relaysSet)+len(archiverSet), ph.Length()) - for _, role := range []PhoneBookEntryRoles{PhoneBookEntryRelayRole, PhoneBookEntryArchiverRole} { + for _, role := range []phonebook.PhoneBookEntryRoles{PhoneBookEntryRelayRole, PhoneBookEntryArchiverRole} { for k := 0; k < 100; k++ { for l := 0; l < 3; l++ { entries := ph.GetAddresses(l, role) diff --git a/network/p2p/peerstore/utils.go b/network/p2p/peerstore/utils.go index 02c6b2d8e6..90b0af497c 100644 --- a/network/p2p/peerstore/utils.go +++ b/network/p2p/peerstore/utils.go @@ -53,8 +53,8 @@ func PeerInfoFromAddr(addr string) (*peer.AddrInfo, error) { return info, nil } -// PeerInfoFromDomainPort converts a string of the form domain:port to AddrInfo -func PeerInfoFromDomainPort(domainPort string) (*peer.AddrInfo, error) { +// peerInfoFromDomainPort converts a string of the form domain:port to AddrInfo +func peerInfoFromDomainPort(domainPort string) (*peer.AddrInfo, error) { parts := strings.Split(domainPort, ":") if len(parts) != 2 || parts[0] == "" || parts[1] == "" { return nil, fmt.Errorf("invalid domain port string %s, found %d colon-separated parts", domainPort, len(parts)) diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 36b9e74eb0..945be51455 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -27,6 +27,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/network/limitcaller" "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/network/p2p/dnsaddr" "github.com/algorand/go-algorand/network/p2p/peerstore" @@ -472,13 +473,13 @@ func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { // query known archival nodes from DHT if enabled if n.config.EnableDHTProviders { const nodesToFind = 5 - info, err := n.capabilitiesDiscovery.PeersForCapability(p2p.Archival, nodesToFind) + infos, err := n.capabilitiesDiscovery.PeersForCapability(p2p.Archival, nodesToFind) if err != nil { n.log.Warnf("Error getting archival nodes from capabilities discovery: %v", err) return peers } - n.log.Debugf("Got %d archival node(s) from DHT", len(info)) - for _, addrInfo := range info { + n.log.Debugf("Got %d archival node(s) from DHT", len(infos)) + for _, addrInfo := range infos { info := addrInfo mas, err := peer.AddrInfoToP2pAddrs(&info) if err != nil { @@ -490,7 +491,9 @@ func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { continue } addr := mas[0].String() - client, err := p2p.MakeHTTPClient(&info) + + maxIdleConnsPerHost := int(n.config.ConnectionsRateLimitingCount) + client, err := p2p.MakeHTTPClientWithRateLimit(&info, n.pstore, limitcaller.DefaultQueueingTimeout, maxIdleConnsPerHost) if err != nil { n.log.Warnf("MakeHTTPClient failed: %v", err) continue @@ -549,7 +552,8 @@ func (n *P2PNetwork) GetHTTPClient(p HTTPPeer) (*http.Client, error) { if err != nil { return nil, err } - return p2p.MakeHTTPClient(addrInfo) + maxIdleConnsPerHost := int(n.config.ConnectionsRateLimitingCount) + return p2p.MakeHTTPClientWithRateLimit(addrInfo, n.pstore, limitcaller.DefaultQueueingTimeout, maxIdleConnsPerHost) } // OnNetworkAdvance notifies the network library that the agreement protocol was able to make a notable progress. @@ -622,7 +626,10 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea n.log.Warnf("Could not get address for peer %s", p2ppeer) } // create a wsPeer for this stream and added it to the peers map. - client, err := p2p.MakeHTTPClient(&peer.AddrInfo{ID: p2ppeer, Addrs: []multiaddr.Multiaddr{ma}}) + + addrInfo := &peer.AddrInfo{ID: p2ppeer, Addrs: []multiaddr.Multiaddr{ma}} + maxIdleConnsPerHost := int(n.config.ConnectionsRateLimitingCount) + client, err := p2p.MakeHTTPClientWithRateLimit(addrInfo, n.pstore, limitcaller.DefaultQueueingTimeout, maxIdleConnsPerHost) if err != nil { client = nil } diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index e64f4df85e..c76967290a 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -28,8 +28,11 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/network/limitcaller" "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/network/p2p/dnsaddr" + "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-algorand/test/partitiontest" @@ -621,6 +624,7 @@ func TestP2PHTTPHandler(t *testing.T) { require.NoError(t, err) require.Equal(t, "hello", string(body)) + // check another endpoint that also access the underlying connection/stream httpClient, err = p2p.MakeHTTPClient(&peerInfoA) require.NoError(t, err) resp, err = httpClient.Get("/check-conn") @@ -631,4 +635,13 @@ func TestP2PHTTPHandler(t *testing.T) { require.NoError(t, err) require.Equal(t, "world", string(body)) + // check rate limiting client: + // zero clients allowed, rate limiting window (10s) is greater than queue deadline (1s) + pstore, err := peerstore.MakePhonebook(0, 10*time.Second) + require.NoError(t, err) + pstore.AddPersistentPeers([]interface{}{&peerInfoA}, "net", phonebook.PhoneBookEntryRelayRole) + httpClient, err = p2p.MakeHTTPClientWithRateLimit(&peerInfoA, pstore, 1*time.Second, 1) + require.NoError(t, err) + _, err = httpClient.Get("/test") + require.ErrorIs(t, err, limitcaller.ErrConnectionQueueingTimeout) } diff --git a/network/phonebook.go b/network/phonebook/phonebook.go similarity index 96% rename from network/phonebook.go rename to network/phonebook/phonebook.go index 3f196e0605..f0c4275026 100644 --- a/network/phonebook.go +++ b/network/phonebook/phonebook.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package network +package phonebook import ( "math" @@ -55,12 +55,12 @@ type Phonebook interface { // The connection should be established when the waitTime is 0. // It will register a provisional next connection time when the waitTime is 0. // The provisional time should be updated after the connection with UpdateConnectionTime - GetConnectionWaitTime(addr string) (addrInPhonebook bool, + GetConnectionWaitTime(addr interface{}) (addrInPhonebook bool, waitTime time.Duration, provisionalTime time.Time) // UpdateConnectionTime will update the provisional connection time. // Returns true of the addr was in the phonebook - UpdateConnectionTime(addr string, provisionalTime time.Time) bool + UpdateConnectionTime(addr interface{}, provisionalTime time.Time) bool // ReplacePeerList merges a set of addresses with that passed in for networkName // new entries in dnsAddresses are being added @@ -231,8 +231,10 @@ func (e *phonebookImpl) UpdateRetryAfter(addr string, retryAfter time.Time) { // The connection should be established when the waitTime is 0. // It will register a provisional next connection time when the waitTime is 0. // The provisional time should be updated after the connection with UpdateConnectionTime -func (e *phonebookImpl) GetConnectionWaitTime(addr string) (addrInPhonebook bool, +func (e *phonebookImpl) GetConnectionWaitTime(a interface{}) (addrInPhonebook bool, waitTime time.Duration, provisionalTime time.Time) { + + addr := a.(string) e.lock.Lock() defer e.lock.Unlock() @@ -276,7 +278,8 @@ func (e *phonebookImpl) GetConnectionWaitTime(addr string) (addrInPhonebook bool // UpdateConnectionTime will update the provisional connection time. // Returns true of the addr was in the phonebook -func (e *phonebookImpl) UpdateConnectionTime(addr string, provisionalTime time.Time) bool { +func (e *phonebookImpl) UpdateConnectionTime(a interface{}, provisionalTime time.Time) bool { + addr := a.(string) e.lock.Lock() defer e.lock.Unlock() diff --git a/network/phonebook_test.go b/network/phonebook/phonebook_test.go similarity index 99% rename from network/phonebook_test.go rename to network/phonebook/phonebook_test.go index 36365c5916..66e34e3aee 100644 --- a/network/phonebook_test.go +++ b/network/phonebook/phonebook_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package network +package phonebook import ( "testing" diff --git a/network/requestLogger_test.go b/network/requestLogger_test.go index cb1d7b963d..af45c6cc08 100644 --- a/network/requestLogger_test.go +++ b/network/requestLogger_test.go @@ -25,6 +25,7 @@ import ( "github.com/algorand/go-algorand/config" "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/test/partitiontest" ) @@ -51,7 +52,7 @@ func TestRequestLogger(t *testing.T) { netA := &WebsocketNetwork{ log: dl, config: defaultConfig, - phonebook: MakePhonebook(1, 1*time.Millisecond), + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), GenesisID: "go-test-network-genesis", NetworkID: config.Devtestnet, } @@ -67,8 +68,8 @@ func TestRequestLogger(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook = MakePhonebook(1, 1*time.Millisecond) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook = phonebook.MakePhonebook(1, 1*time.Millisecond) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer func() { t.Log("stopping B"); netB.Stop(); t.Log("B done") }() diff --git a/network/requestTracker.go b/network/requestTracker.go index c88d4e5cf0..2e08f94cee 100644 --- a/network/requestTracker.go +++ b/network/requestTracker.go @@ -31,6 +31,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" + "github.com/algorand/go-algorand/network/addr" ) const ( @@ -94,12 +95,12 @@ func makeTrackerRequest(remoteAddr, remoteHost, remotePort string, createTime ti // - remoteAddr is used otherwise. func (tr *TrackerRequest) remoteAddress() string { if len(tr.otherPublicAddr) != 0 { - url, err := ParseHostOrURL(tr.otherPublicAddr) + url, err := addr.ParseHostOrURL(tr.otherPublicAddr) if err == nil && len(tr.remoteHost) > 0 && url.Hostname() == tr.remoteHost { return tr.otherPublicAddr } } - url, err := ParseHostOrURL(tr.remoteAddr) + url, err := addr.ParseHostOrURL(tr.remoteAddr) if err != nil { // tr.remoteAddr can't be parsed so try to use tr.remoteHost // there is a chance it came from a proxy and has a meaningful value diff --git a/network/requestTracker_test.go b/network/requestTracker_test.go index 0a8c934c53..f2c098b388 100644 --- a/network/requestTracker_test.go +++ b/network/requestTracker_test.go @@ -27,6 +27,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -88,7 +89,7 @@ func TestRateLimiting(t *testing.T) { wn := &WebsocketNetwork{ log: log, config: testConfig, - phonebook: MakePhonebook(1, 1), + phonebook: phonebook.MakePhonebook(1, 1), GenesisID: "go-test-network-genesis", NetworkID: config.Devtestnet, } @@ -115,15 +116,15 @@ func TestRateLimiting(t *testing.T) { clientsCount := int(testConfig.ConnectionsRateLimitingCount + 5) networks := make([]*WebsocketNetwork, clientsCount) - phonebooks := make([]Phonebook, clientsCount) + phonebooks := make([]phonebook.Phonebook, clientsCount) for i := 0; i < clientsCount; i++ { networks[i] = makeTestWebsocketNodeWithConfig(t, noAddressConfig) networks[i].config.GossipFanout = 1 - phonebooks[i] = MakePhonebook(networks[i].config.ConnectionsRateLimitingCount, + phonebooks[i] = phonebook.MakePhonebook(networks[i].config.ConnectionsRateLimitingCount, time.Duration(networks[i].config.ConnectionsRateLimitingWindowSeconds)*time.Second) - phonebooks[i].ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) - networks[i].phonebook = MakePhonebook(1, 1*time.Millisecond) - networks[i].phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + phonebooks[i].ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) + networks[i].phonebook = phonebook.MakePhonebook(1, 1*time.Millisecond) + networks[i].phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) defer func(net *WebsocketNetwork, i int) { t.Logf("stopping network %d", i) net.Stop() @@ -153,7 +154,7 @@ func TestRateLimiting(t *testing.T) { case <-readyCh: // it's closed, so this client got connected. connectedClients++ - phonebookLen := len(phonebooks[i].GetAddresses(1, PhoneBookEntryRelayRole)) + phonebookLen := len(phonebooks[i].GetAddresses(1, phonebook.PhoneBookEntryRelayRole)) // if this channel is ready, than we should have an address, since it didn't get blocked. require.Equal(t, 1, phonebookLen) default: diff --git a/network/websocketProxy_test.go b/network/websocketProxy_test.go index 73298ccd64..cefe4b687a 100644 --- a/network/websocketProxy_test.go +++ b/network/websocketProxy_test.go @@ -28,6 +28,7 @@ import ( "testing" "time" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/websocket" "github.com/stretchr/testify/require" @@ -71,7 +72,7 @@ func (w *websocketProxy) ServeHTTP(response http.ResponseWriter, request *http.R } // set X-Forwarded-For - url, err := ParseHostOrURL(request.RemoteAddr) + url, err := addr.ParseHostOrURL(request.RemoteAddr) if err != nil { http.Error(response, err.Error(), http.StatusInternalServerError) return @@ -254,7 +255,7 @@ func TestWebsocketProxyWsNet(t *testing.T) { gossipA, err := netA.addrToGossipAddr(addrA) require.NoError(t, err) - parsedA, err := ParseHostOrURL(gossipA) + parsedA, err := addr.ParseHostOrURL(gossipA) require.NoError(t, err) // setup the proxy diff --git a/network/wsNetwork.go b/network/wsNetwork.go index cdb3b4c635..2ead8ff019 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -43,8 +43,11 @@ import ( "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" + "github.com/algorand/go-algorand/network/addr" + "github.com/algorand/go-algorand/network/limitcaller" "github.com/algorand/go-algorand/network/limitlistener" "github.com/algorand/go-algorand/network/p2p" + "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-algorand/protocol" tools_network "github.com/algorand/go-algorand/tools/network" "github.com/algorand/go-algorand/tools/network/dnssec" @@ -196,7 +199,7 @@ type WebsocketNetwork struct { broadcaster msgBroadcaster handler msgHandler - phonebook Phonebook + phonebook phonebook.Phonebook GenesisID string NetworkID protocol.NetworkID @@ -258,8 +261,8 @@ type WebsocketNetwork struct { // transport and dialer are customized to limit the number of // connection in compliance with connectionsRateLimitingCount. - transport rateLimitingTransport - dialer Dialer + transport limitcaller.RateLimitingTransport + dialer limitcaller.Dialer // messagesOfInterest specifies the message types that this node // wants to receive. nil means default. non-nil causes this @@ -551,14 +554,14 @@ func (wn *WebsocketNetwork) GetPeers(options ...PeerOption) []Peer { case PeersPhonebookRelays: // return copy of phonebook, which probably also contains peers we're connected to, but if it doesn't maybe we shouldn't be making new connections to those peers (because they disappeared from the directory) var addrs []string - addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryRelayRole) + addrs = wn.phonebook.GetAddresses(1000, phonebook.PhoneBookEntryRelayRole) for _, addr := range addrs { peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersPhonebookArchivalNodes: var addrs []string - addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryRelayRole) + addrs = wn.phonebook.GetAddresses(1000, phonebook.PhoneBookEntryRelayRole) for _, addr := range addrs { peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) @@ -566,7 +569,7 @@ func (wn *WebsocketNetwork) GetPeers(options ...PeerOption) []Peer { case PeersPhonebookArchivers: // return copy of phonebook, which probably also contains peers we're connected to, but if it doesn't maybe we shouldn't be making new connections to those peers (because they disappeared from the directory) var addrs []string - addrs = wn.phonebook.GetAddresses(1000, PhoneBookEntryArchiverRole) + addrs = wn.phonebook.GetAddresses(1000, phonebook.PhoneBookEntryArchiverRole) for _, addr := range addrs { peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) outPeers = append(outPeers, &peerCore) @@ -593,8 +596,8 @@ func (wn *WebsocketNetwork) setup() { wn.nodeInfo = &nopeNodeInfo{} } maxIdleConnsPerHost := int(wn.config.ConnectionsRateLimitingCount) - wn.dialer = makeRateLimitingDialer(wn.phonebook, preferredResolver) - wn.transport = makeRateLimitingTransport(wn.phonebook, 10*time.Second, &wn.dialer, maxIdleConnsPerHost) + wn.dialer = limitcaller.MakeRateLimitingDialer(wn.phonebook, preferredResolver) + wn.transport = limitcaller.MakeRateLimitingTransport(wn.phonebook, limitcaller.DefaultQueueingTimeout, &wn.dialer, maxIdleConnsPerHost) wn.upgrader.ReadBufferSize = 4096 wn.upgrader.WriteBufferSize = 4096 @@ -1640,12 +1643,12 @@ func (wn *WebsocketNetwork) refreshRelayArchivePhonebookAddresses() { func (wn *WebsocketNetwork) updatePhonebookAddresses(relayAddrs []string, archiveAddrs []string) { if len(relayAddrs) > 0 { wn.log.Debugf("got %d relay dns addrs, %#v", len(relayAddrs), relayAddrs[:imin(5, len(relayAddrs))]) - wn.phonebook.ReplacePeerList(relayAddrs, string(wn.NetworkID), PhoneBookEntryRelayRole) + wn.phonebook.ReplacePeerList(relayAddrs, string(wn.NetworkID), phonebook.PhoneBookEntryRelayRole) } else { wn.log.Infof("got no relay DNS addrs for network %s", wn.NetworkID) } if len(archiveAddrs) > 0 { - wn.phonebook.ReplacePeerList(archiveAddrs, string(wn.NetworkID), PhoneBookEntryArchiverRole) + wn.phonebook.ReplacePeerList(archiveAddrs, string(wn.NetworkID), phonebook.PhoneBookEntryArchiverRole) } } @@ -1662,7 +1665,7 @@ func (wn *WebsocketNetwork) checkNewConnectionsNeeded() bool { return false } // get more than we need so that we can ignore duplicates - newAddrs := wn.phonebook.GetAddresses(desired+numOutgoingTotal, PhoneBookEntryRelayRole) + newAddrs := wn.phonebook.GetAddresses(desired+numOutgoingTotal, phonebook.PhoneBookEntryRelayRole) for _, na := range newAddrs { if na == wn.config.PublicAddress { // filter out self-public address, so we won't try to connect to ourselves. @@ -2267,13 +2270,21 @@ func (wn *WebsocketNetwork) SetPeerData(peer Peer, key string, value interface{} // NewWebsocketNetwork constructor for websockets based gossip network func NewWebsocketNetwork(log logging.Logger, config config.Local, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID, nodeInfo NodeInfo, peerID p2p.PeerID, idSigner identityChallengeSigner) (wn *WebsocketNetwork, err error) { - phonebook := MakePhonebook(config.ConnectionsRateLimitingCount, + pb := phonebook.MakePhonebook(config.ConnectionsRateLimitingCount, time.Duration(config.ConnectionsRateLimitingWindowSeconds)*time.Second) - phonebook.AddPersistentPeers(phonebookAddresses, string(networkID), PhoneBookEntryRelayRole) + + addresses := make([]string, 0, len(phonebookAddresses)) + for _, a := range phonebookAddresses { + _, err := addr.ParseHostOrURL(a) + if err == nil { + addresses = append(addresses, a) + } + } + pb.AddPersistentPeers(addresses, string(networkID), phonebook.PhoneBookEntryRelayRole) wn = &WebsocketNetwork{ log: log, config: config, - phonebook: phonebook, + phonebook: pb, GenesisID: genesisID, NetworkID: networkID, nodeInfo: nodeInfo, diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index 32b9e4e5b9..1db042cb85 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -41,6 +41,7 @@ import ( "github.com/algorand/go-algorand/internal/rapidgen" "github.com/algorand/go-algorand/network/p2p" + "github.com/algorand/go-algorand/network/phonebook" "pgregory.net/rapid" "github.com/stretchr/testify/assert" @@ -130,7 +131,7 @@ func makeTestWebsocketNodeWithConfig(t testing.TB, conf config.Local, opts ...te wn := &WebsocketNetwork{ log: log, config: conf, - phonebook: MakePhonebook(1, 1*time.Millisecond), + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), GenesisID: genesisID, NetworkID: config.Devtestnet, } @@ -323,7 +324,7 @@ func setupWebsocketNetworkABwithLogger(t *testing.T, countTarget int, log loggin addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer func() { if !success { @@ -459,7 +460,7 @@ func TestWebsocketProposalPayloadCompression(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") messages := [][]byte{ @@ -638,7 +639,7 @@ func TestWebsocketNetworkNoAddress(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") @@ -703,7 +704,7 @@ func lineNetwork(t *testing.T, numNodes int) (nodes []*WebsocketNetwork, counter if i > 0 { addrPrev, postListen := nodes[i-1].Address() require.True(t, postListen) - nodes[i].phonebook.ReplacePeerList([]string{addrPrev}, "default", PhoneBookEntryRelayRole) + nodes[i].phonebook.ReplacePeerList([]string{addrPrev}, "default", phonebook.PhoneBookEntryRelayRole) nodes[i].RegisterHandlers([]TaggedMessageHandler{{Tag: protocol.TxnTag, MessageHandler: &counters[i]}}) } nodes[i].Start() @@ -1056,7 +1057,7 @@ func makeTestFilterWebsocketNode(t *testing.T, nodename string) *WebsocketNetwor wn := &WebsocketNetwork{ log: logging.TestingLog(t).With("node", nodename), config: dc, - phonebook: MakePhonebook(1, 1*time.Millisecond), + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), GenesisID: genesisID, NetworkID: config.Devtestnet, } @@ -1079,7 +1080,7 @@ func TestDupFilter(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") counter := &messageCounterHandler{t: t, limit: 1, done: make(chan struct{})} @@ -1092,7 +1093,7 @@ func TestDupFilter(t *testing.T) { require.True(t, postListen) netC := makeTestFilterWebsocketNode(t, "c") netC.config.GossipFanout = 1 - netC.phonebook.ReplacePeerList([]string{addrB}, "default", PhoneBookEntryRelayRole) + netC.phonebook.ReplacePeerList([]string{addrB}, "default", phonebook.PhoneBookEntryRelayRole) netC.Start() defer netC.Stop() @@ -1170,8 +1171,8 @@ func TestGetPeers(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - phbMulti := MakePhonebook(1, 1*time.Millisecond) - phbMulti.ReplacePeerList([]string{addrA}, "phba", PhoneBookEntryRelayRole) + phbMulti := phonebook.MakePhonebook(1, 1*time.Millisecond) + phbMulti.ReplacePeerList([]string{addrA}, "phba", phonebook.PhoneBookEntryRelayRole) netB.phonebook = phbMulti netB.Start() defer netB.Stop() @@ -1182,7 +1183,7 @@ func TestGetPeers(t *testing.T) { waitReady(t, netB, readyTimeout.C) t.Log("b ready") - phbMulti.ReplacePeerList([]string{"a", "b", "c"}, "ph", PhoneBookEntryRelayRole) + phbMulti.ReplacePeerList([]string{"a", "b", "c"}, "ph", phonebook.PhoneBookEntryRelayRole) //addrB, _ := netB.Address() @@ -2179,7 +2180,7 @@ func BenchmarkWebsocketNetworkBasic(t *testing.B) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") returns := make(chan uint64, 100) @@ -2261,7 +2262,7 @@ func TestWebsocketNetworkPrio(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") @@ -2308,7 +2309,7 @@ func TestWebsocketNetworkPrioLimit(t *testing.T) { netB.SetPrioScheme(&prioB) netB.config.GossipFanout = 1 netB.config.NetAddress = "" - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.RegisterHandlers([]TaggedMessageHandler{{Tag: protocol.TxnTag, MessageHandler: counterB}}) netB.Start() defer netStop(t, netB, "B") @@ -2322,7 +2323,7 @@ func TestWebsocketNetworkPrioLimit(t *testing.T) { netC.SetPrioScheme(&prioC) netC.config.GossipFanout = 1 netC.config.NetAddress = "" - netC.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netC.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netC.RegisterHandlers([]TaggedMessageHandler{{Tag: protocol.TxnTag, MessageHandler: counterC}}) netC.Start() defer func() { t.Log("stopping C"); netC.Stop(); t.Log("C done") }() @@ -2407,7 +2408,7 @@ func TestWebsocketNetworkManyIdle(t *testing.T) { for i := 0; i < numClients; i++ { client := makeTestWebsocketNodeWithConfig(t, clientConf) client.config.GossipFanout = 1 - client.phonebook.ReplacePeerList([]string{relayAddr}, "default", PhoneBookEntryRelayRole) + client.phonebook.ReplacePeerList([]string{relayAddr}, "default", phonebook.PhoneBookEntryRelayRole) client.Start() defer client.Stop() @@ -2532,7 +2533,7 @@ func TestDelayedMessageDrop(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") counter := newMessageCounter(t, 5) @@ -2564,7 +2565,7 @@ func TestSlowPeerDisconnection(t *testing.T) { wn := &WebsocketNetwork{ log: log, config: defaultConfig, - phonebook: MakePhonebook(1, 1*time.Millisecond), + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), GenesisID: genesisID, NetworkID: config.Devtestnet, } @@ -2585,7 +2586,7 @@ func TestSlowPeerDisconnection(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") @@ -2639,7 +2640,7 @@ func TestForceMessageRelaying(t *testing.T) { wn := &WebsocketNetwork{ log: log, config: defaultConfig, - phonebook: MakePhonebook(1, 1*time.Millisecond), + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), GenesisID: genesisID, NetworkID: config.Devtestnet, } @@ -2662,14 +2663,14 @@ func TestForceMessageRelaying(t *testing.T) { noAddressConfig.NetAddress = "" netB := makeTestWebsocketNodeWithConfig(t, noAddressConfig) netB.config.GossipFanout = 1 - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") noAddressConfig.ForceRelayMessages = true netC := makeTestWebsocketNodeWithConfig(t, noAddressConfig) netC.config.GossipFanout = 1 - netC.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netC.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netC.Start() defer func() { t.Log("stopping C"); netC.Stop(); t.Log("C done") }() @@ -2733,7 +2734,7 @@ func TestCheckProtocolVersionMatch(t *testing.T) { wn := &WebsocketNetwork{ log: log, config: defaultConfig, - phonebook: MakePhonebook(1, 1*time.Millisecond), + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), GenesisID: genesisID, NetworkID: config.Devtestnet, } @@ -2813,7 +2814,7 @@ func TestWebsocketNetworkTopicRoundtrip(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") @@ -2913,7 +2914,7 @@ func TestWebsocketNetworkMessageOfInterest(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Logf("netA %s", addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) // have netB asking netA to send it ft2, deregister ping handler to make sure that we aren't exceeding the maximum MOI messagesize // Max MOI size is calculated by encoding all of the valid tags, since we are using a custom tag here we must deregister one in the default set. @@ -3039,7 +3040,7 @@ func TestWebsocketNetworkTXMessageOfInterestRelay(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") @@ -3123,7 +3124,7 @@ func TestWebsocketNetworkTXMessageOfInterestForceTx(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") @@ -3205,7 +3206,7 @@ func TestWebsocketNetworkTXMessageOfInterestNPN(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") require.False(t, netB.relayMessages) @@ -3313,7 +3314,7 @@ func TestWebsocketNetworkTXMessageOfInterestPN(t *testing.T) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") require.False(t, netB.relayMessages) @@ -3435,7 +3436,7 @@ func testWebsocketDisconnection(t *testing.T, disconnectFunc func(wn *WebsocketN addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer netStop(t, netB, "B") @@ -3630,7 +3631,7 @@ func BenchmarkVariableTransactionMessageBlockSizes(t *testing.B) { addrA, postListen := netA.Address() require.True(t, postListen) t.Log(addrA) - netB.phonebook.ReplacePeerList([]string{addrA}, "default", PhoneBookEntryRelayRole) + netB.phonebook.ReplacePeerList([]string{addrA}, "default", phonebook.PhoneBookEntryRelayRole) netB.Start() defer func() { netB.Stop() }() @@ -4543,3 +4544,23 @@ func TestSendMessageCallbackDrain(t *testing.T) { 50*time.Millisecond, ) } + +// TestWsNetworkPhonebookMix ensures p2p addresses are not added into wsNetwork via phonebook +func TestWsNetworkPhonebookMix(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + net, err := NewWebsocketNetwork( + logging.TestingLog(t), + config.GetDefaultLocal(), + []string{"127.0.0.1:1234", "/ip4/127.0.0.1/tcp/1234", "/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"}, + "test", + "net", + nil, + "", + nil, + ) + require.NoError(t, err) + addrs := net.phonebook.GetAddresses(10, phonebook.PhoneBookEntryRelayRole) + require.Len(t, addrs, 1) +} diff --git a/node/node_test.go b/node/node_test.go index f82df98b84..a83ea7ab39 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -841,7 +841,7 @@ func TestMaxSizesCorrect(t *testing.T) { // N -- R -- A and ensures N can discover A and download blocks from it. // // N is a non-part node that joins the network later -// R is a non-arhival relay node with block service disabled. It MUST NOT service blocks to force N to discover A. +// R is a non-archival relay node with block service disabled. It MUST NOT service blocks to force N to discover A. // A is a archival node that can only provide blocks. // Nodes N and A have only R in their initial phonebook, and all nodes are in hybrid mode. func TestNodeHybridTopology(t *testing.T) { diff --git a/rpcs/blockService.go b/rpcs/blockService.go index ed27c9a703..380c2cd998 100644 --- a/rpcs/blockService.go +++ b/rpcs/blockService.go @@ -41,6 +41,7 @@ import ( "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/metrics" ) @@ -389,10 +390,10 @@ func (bs *BlockService) redirectRequest(round uint64, response http.ResponseWrit } var redirectURL string - if network.IsMultiaddr(peerAddress) { + if addr.IsMultiaddr(peerAddress) { redirectURL = strings.Replace(FormatBlockQuery(round, "", bs.net), "{genesisID}", bs.genesisID, 1) } else { - parsedURL, err := network.ParseHostOrURL(peerAddress) + parsedURL, err := addr.ParseHostOrURL(peerAddress) if err != nil { bs.log.Debugf("redirectRequest: %s", err.Error()) return false @@ -498,10 +499,10 @@ func makeFallbackEndpoints(log logging.Logger, customFallbackEndpoints string) ( } endpoints := strings.Split(customFallbackEndpoints, ",") for _, ep := range endpoints { - if network.IsMultiaddr(ep) { + if addr.IsMultiaddr(ep) { fe.endpoints = append(fe.endpoints, ep) } else { - parsed, err := network.ParseHostOrURL(ep) + parsed, err := addr.ParseHostOrURL(ep) if err != nil { log.Warnf("makeFallbackEndpoints: error parsing %s %s", ep, err.Error()) continue diff --git a/rpcs/blockService_test.go b/rpcs/blockService_test.go index d7ad406675..5ff2153c9e 100644 --- a/rpcs/blockService_test.go +++ b/rpcs/blockService_test.go @@ -39,6 +39,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -168,7 +169,7 @@ func TestRedirectFallbackEndpoints(t *testing.T) { nodeA.RegisterHTTPHandler(BlockServiceBlockPath, bs1) nodeB.RegisterHTTPHandler(BlockServiceBlockPath, bs2) - parsedURL, err := network.ParseHostOrURL(nodeA.rootURL()) + parsedURL, err := addr.ParseHostOrURL(nodeA.rootURL()) require.NoError(t, err) client := http.Client{} @@ -213,7 +214,7 @@ func TestBlockServiceShutdown(t *testing.T) { nodeA.start() defer nodeA.stop() - parsedURL, err := network.ParseHostOrURL(nodeA.rootURL()) + parsedURL, err := addr.ParseHostOrURL(nodeA.rootURL()) require.NoError(t, err) client := http.Client{} @@ -295,7 +296,7 @@ func TestRedirectOnFullCapacity(t *testing.T) { nodeB.RegisterHTTPHandler(BlockServiceBlockPath, bs2) - parsedURL, err := network.ParseHostOrURL(nodeA.rootURL()) + parsedURL, err := addr.ParseHostOrURL(nodeA.rootURL()) require.NoError(t, err) client := http.Client{} @@ -476,7 +477,7 @@ func TestRedirectExceptions(t *testing.T) { nodeA.RegisterHTTPHandler(BlockServiceBlockPath, bs1) nodeB.RegisterHTTPHandler(BlockServiceBlockPath, bs2) - parsedURL, err := network.ParseHostOrURL(nodeA.rootURL()) + parsedURL, err := addr.ParseHostOrURL(nodeA.rootURL()) require.NoError(t, err) client := http.Client{} @@ -495,7 +496,7 @@ func TestRedirectExceptions(t *testing.T) { require.NoError(t, err) require.Equal(t, response.StatusCode, http.StatusNotFound) - parsedURLNodeB, err := network.ParseHostOrURL(nodeB.rootURL()) + parsedURLNodeB, err := addr.ParseHostOrURL(nodeB.rootURL()) require.NoError(t, err) parsedURLNodeB.Path = FormatBlockQuery(uint64(4), parsedURLNodeB.Path, net2) diff --git a/rpcs/httpTxSync.go b/rpcs/httpTxSync.go index 4d803bddda..4e1fd29883 100644 --- a/rpcs/httpTxSync.go +++ b/rpcs/httpTxSync.go @@ -31,6 +31,7 @@ import ( "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/bloom" ) @@ -112,10 +113,10 @@ func (hts *HTTPTxSync) Sync(ctx context.Context, bloom *bloom.Filter) (txgroups return nil, fmt.Errorf("HTTPTxSync cannot create a HTTP client for a peer %T %#v: %s", peer, peer, err.Error()) } } - if network.IsMultiaddr(hts.rootURL) { + if addr.IsMultiaddr(hts.rootURL) { syncURL = network.SubstituteGenesisID(hts.peers, path.Join("", TxServiceHTTPPath)) } else { - parsedURL, err0 := network.ParseHostOrURL(hts.rootURL) + parsedURL, err0 := addr.ParseHostOrURL(hts.rootURL) if err0 != nil { hts.log.Warnf("txSync bad url %v: %s", hts.rootURL, err0) return nil, err0 From e506ffd586cc2e9c85a0cbb53bd39f95535eadfb Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:46:57 -0500 Subject: [PATCH 15/38] network: unify wsPeerCore use for HTTP and p2p HTTP transport (#5933) --- catchup/fetcher_test.go | 7 ++- catchup/ledgerFetcher.go | 15 +------ catchup/ledgerFetcher_test.go | 2 +- catchup/universalFetcher.go | 14 +----- components/mocks/mockNetwork.go | 2 +- network/addr/addr_test.go | 3 ++ network/gossipNode.go | 2 +- network/hybridNetwork.go | 13 +++--- network/netprio.go | 2 +- network/p2pNetwork.go | 12 +++--- network/p2pNetwork_test.go | 2 +- network/wsNetwork.go | 75 +++++++++++++++++++++------------ network/wsNetwork_test.go | 56 +++++++++++++++++++++++- network/wsPeer.go | 16 +++---- rpcs/httpTxSync.go | 18 ++------ rpcs/txService_test.go | 6 ++- rpcs/txSyncer_test.go | 9 +++- 17 files changed, 149 insertions(+), 105 deletions(-) diff --git a/catchup/fetcher_test.go b/catchup/fetcher_test.go index 5d0e194812..fc080fd061 100644 --- a/catchup/fetcher_test.go +++ b/catchup/fetcher_test.go @@ -201,8 +201,13 @@ type testHTTPPeer string func (p *testHTTPPeer) GetAddress() string { return string(*p) } + func (p *testHTTPPeer) GetHTTPClient() *http.Client { - return &http.Client{} + return &http.Client{ + Transport: &network.HTTPPAddressBoundTransport{ + Addr: p.GetAddress(), + InnerTransport: http.DefaultTransport}, + } } func (p *testHTTPPeer) GetHTTPPeer() network.HTTPPeer { return p diff --git a/catchup/ledgerFetcher.go b/catchup/ledgerFetcher.go index e5119578b3..916627db8f 100644 --- a/catchup/ledgerFetcher.go +++ b/catchup/ledgerFetcher.go @@ -23,7 +23,6 @@ import ( "fmt" "io" "net/http" - "path" "strconv" "time" @@ -33,7 +32,6 @@ import ( "github.com/algorand/go-algorand/ledger/encoded" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" - "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/go-algorand/util" ) @@ -75,18 +73,7 @@ func makeLedgerFetcher(net network.GossipNode, accessor ledger.CatchpointCatchup } func (lf *ledgerFetcher) requestLedger(ctx context.Context, peer network.HTTPPeer, round basics.Round, method string) (*http.Response, error) { - var ledgerURL string - if addr.IsMultiaddr(peer.GetAddress()) { - ledgerURL = network.SubstituteGenesisID(lf.net, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36)) - } else { - - parsedURL, err := addr.ParseHostOrURL(peer.GetAddress()) - if err != nil { - return nil, err - } - parsedURL.Path = network.SubstituteGenesisID(lf.net, path.Join(parsedURL.Path, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36))) - ledgerURL = parsedURL.String() - } + ledgerURL := network.SubstituteGenesisID(lf.net, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36)) lf.log.Debugf("ledger %s %#v peer %#v %T", method, ledgerURL, peer, peer) request, err := http.NewRequestWithContext(ctx, method, ledgerURL, nil) if err != nil { diff --git a/catchup/ledgerFetcher_test.go b/catchup/ledgerFetcher_test.go index 321227890b..a080aca31e 100644 --- a/catchup/ledgerFetcher_test.go +++ b/catchup/ledgerFetcher_test.go @@ -163,7 +163,7 @@ func TestLedgerFetcher(t *testing.T) { // headLedger parseURL failure parseFailurePeer := testHTTPPeer("foobar") err = lf.headLedger(context.Background(), &parseFailurePeer, basics.Round(0)) - require.Equal(t, fmt.Errorf("could not parse a host from url"), err) + require.ErrorContains(t, err, "could not parse a host from url") // headLedger 404 response httpServerResponse = http.StatusNotFound diff --git a/catchup/universalFetcher.go b/catchup/universalFetcher.go index a66278189b..2ca44c3067 100644 --- a/catchup/universalFetcher.go +++ b/catchup/universalFetcher.go @@ -32,7 +32,6 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" - "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/rpcs" ) @@ -220,18 +219,7 @@ type HTTPFetcher struct { // getBlockBytes gets a block. // Core piece of FetcherClient interface func (hf *HTTPFetcher) getBlockBytes(ctx context.Context, r basics.Round) (data []byte, err error) { - var blockURL string - - if addr.IsMultiaddr(hf.rootURL) { - blockURL = rpcs.FormatBlockQuery(uint64(r), "", hf.net) - } else { - if parsedURL, err0 := addr.ParseHostOrURL(hf.rootURL); err0 == nil { - parsedURL.Path = rpcs.FormatBlockQuery(uint64(r), parsedURL.Path, hf.net) - blockURL = parsedURL.String() - } else { - return nil, err0 - } - } + blockURL := rpcs.FormatBlockQuery(uint64(r), "", hf.net) hf.log.Debugf("block GET %#v peer %#v %T", blockURL, hf.peer, hf.peer) request, err := http.NewRequest("GET", blockURL, nil) diff --git a/components/mocks/mockNetwork.go b/components/mocks/mockNetwork.go index 9c8fc97652..7e1ab29126 100644 --- a/components/mocks/mockNetwork.go +++ b/components/mocks/mockNetwork.go @@ -112,6 +112,6 @@ func (network *MockNetwork) GetGenesisID() string { } // GetHTTPClient returns a http.Client with a suitable for the network -func (network *MockNetwork) GetHTTPClient(p network.HTTPPeer) (*http.Client, error) { +func (network *MockNetwork) GetHTTPClient(address string) (*http.Client, error) { return nil, errors.New("not implemented") } diff --git a/network/addr/addr_test.go b/network/addr/addr_test.go index c651352dd2..bceeb079f2 100644 --- a/network/addr/addr_test.go +++ b/network/addr/addr_test.go @@ -31,6 +31,8 @@ type urlCase struct { func TestParseHostOrURL(t *testing.T) { partitiontest.PartitionTest(t) + t.Parallel() + urlTestCases := []urlCase{ {"localhost:123", url.URL{Scheme: "http", Host: "localhost:123"}}, {"http://localhost:123", url.URL{Scheme: "http", Host: "localhost:123"}}, @@ -102,6 +104,7 @@ func TestParseHostOrURL(t *testing.T) { func TestParseHostURLOrMultiaddr(t *testing.T) { partitiontest.PartitionTest(t) + t.Parallel() validMultiAddrs := []string{ "/ip4/127.0.0.1/tcp/8080", diff --git a/network/gossipNode.go b/network/gossipNode.go index 0c6e0b06cc..ebe7d7960f 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -92,7 +92,7 @@ type GossipNode interface { // GetHTTPClient returns a http.Client with a suitable for the network Transport // that would also limit the number of outgoing connections. - GetHTTPClient(peer HTTPPeer) (*http.Client, error) + GetHTTPClient(address string) (*http.Client, error) // OnNetworkAdvance notifies the network library that the agreement protocol was able to make a notable progress. // this is the only indication that we have that we haven't formed a clique, where all incoming messages diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go index d66b794f24..7abb2ab569 100644 --- a/network/hybridNetwork.go +++ b/network/hybridNetwork.go @@ -24,6 +24,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/protocol" ) @@ -181,15 +182,11 @@ func (n *HybridP2PNetwork) ClearHandlers() { // GetHTTPClient returns a http.Client with a suitable for the network Transport // that would also limit the number of outgoing connections. -func (n *HybridP2PNetwork) GetHTTPClient(peer HTTPPeer) (*http.Client, error) { - switch peer.(type) { - case *wsPeer: - return n.wsNetwork.GetHTTPClient(peer) - case *wsPeerCore: - return n.p2pNetwork.GetHTTPClient(peer) - default: - panic("unrecognized peer type") +func (n *HybridP2PNetwork) GetHTTPClient(address string) (*http.Client, error) { + if addr.IsMultiaddr(address) { + return n.p2pNetwork.GetHTTPClient(address) } + return n.wsNetwork.GetHTTPClient(address) } // OnNetworkAdvance notifies the network library that the agreement protocol was able to make a notable progress. diff --git a/network/netprio.go b/network/netprio.go index 5d91dad8d1..9c6c510608 100644 --- a/network/netprio.go +++ b/network/netprio.go @@ -46,7 +46,7 @@ func prioResponseHandler(message IncomingMessage) OutgoingMessage { addr, err := wn.prioScheme.VerifyPrioResponse(challenge, message.Data) if err != nil { - wn.log.Warnf("prioScheme.VerifyPrioResponse from %s: %v", peer.rootURL, err) + wn.log.Warnf("prioScheme.VerifyPrioResponse from %s: %v", peer.GetAddress(), err) } else { weight := wn.prioScheme.GetPrioWeight(addr) diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 945be51455..be59e09d62 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -499,16 +499,16 @@ func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { continue } - peerCore := makePeerCoreWithClient( + peerCore := makePeerCore( n.ctx, n, n.log, n.handler.readBuffer, - addr /*rootURL*/, client, "", /*origin address*/ + addr, client, "", /*origin address*/ ) peers = append(peers, &peerCore) } if n.log.GetLevel() >= logging.Debug && len(peers) > 0 { addrs := make([]string, 0, len(peers)) for _, peer := range peers { - addrs = append(addrs, peer.(*wsPeerCore).rootURL) + addrs = append(addrs, peer.(*wsPeerCore).GetAddress()) } n.log.Debugf("Archival node(s) from DHT: %v", addrs) } @@ -547,8 +547,8 @@ func (n *P2PNetwork) ClearHandlers() { // GetHTTPClient returns a http.Client with a suitable for the network Transport // that would also limit the number of outgoing connections. -func (n *P2PNetwork) GetHTTPClient(p HTTPPeer) (*http.Client, error) { - addrInfo, err := peer.AddrInfoFromString(p.GetAddress()) +func (n *P2PNetwork) GetHTTPClient(address string) (*http.Client, error) { + addrInfo, err := peer.AddrInfoFromString(address) if err != nil { return nil, err } @@ -633,7 +633,7 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea if err != nil { client = nil } - peerCore := makePeerCoreWithClient(ctx, n, n.log, n.handler.readBuffer, addr, client, addr) + peerCore := makePeerCore(ctx, n, n.log, n.handler.readBuffer, addr, client, addr) wsp := &wsPeer{ wsPeerCore: peerCore, conn: &wsPeerConnP2PImpl{stream: stream}, diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index c76967290a..ebed7c1afc 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -540,7 +540,7 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { uniquePeerIDs := make(map[peer.ID]struct{}) for _, p := range peers { wsPeer := p.(*wsPeerCore) - pi, err := peer.AddrInfoFromString(wsPeer.rootURL) + pi, err := peer.AddrInfoFromString(wsPeer.GetAddress()) require.NoError(t, err) uniquePeerIDs[pi.ID] = struct{}{} } diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 2ead8ff019..02517dcf83 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -27,6 +27,7 @@ import ( "net/http" "net/textproto" "net/url" + "path" "regexp" "runtime" "strconv" @@ -556,14 +557,16 @@ func (wn *WebsocketNetwork) GetPeers(options ...PeerOption) []Peer { var addrs []string addrs = wn.phonebook.GetAddresses(1000, phonebook.PhoneBookEntryRelayRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) + client, _ := wn.GetHTTPClient(addr) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, client, "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersPhonebookArchivalNodes: var addrs []string addrs = wn.phonebook.GetAddresses(1000, phonebook.PhoneBookEntryRelayRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) + client, _ := wn.GetHTTPClient(addr) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, client, "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersPhonebookArchivers: @@ -571,7 +574,8 @@ func (wn *WebsocketNetwork) GetPeers(options ...PeerOption) []Peer { var addrs []string addrs = wn.phonebook.GetAddresses(1000, phonebook.PhoneBookEntryArchiverRole) for _, addr := range addrs { - peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /*origin address*/) + client, _ := wn.GetHTTPClient(addr) + peerCore := makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, client, "" /*origin address*/) outPeers = append(outPeers, &peerCore) } case PeersConnectedIn: @@ -1097,8 +1101,9 @@ func (wn *WebsocketNetwork) ServeHTTP(response http.ResponseWriter, request *htt wn.requestsLogger.SetStatusCode(response, http.StatusSwitchingProtocols) } + client, _ := wn.GetHTTPClient(trackedRequest.remoteAddress()) peer := &wsPeer{ - wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, trackedRequest.remoteAddress(), wn.getRoundTripper(nil), trackedRequest.remoteHost), + wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, trackedRequest.remoteAddress(), client, trackedRequest.remoteHost), conn: wsPeerWebsocketConnImpl{conn}, outgoing: false, InstanceName: trackedRequest.otherInstanceName, @@ -1541,7 +1546,7 @@ func (wn *WebsocketNetwork) isConnectedTo(addr string) bool { wn.peersLock.RLock() defer wn.peersLock.RUnlock() for _, peer := range wn.peers { - if addr == peer.rootURL { + if addr == peer.GetAddress() { return true } } @@ -2030,20 +2035,33 @@ func (wn *WebsocketNetwork) numOutgoingPending() int { return len(wn.tryConnectAddrs) } -// getRoundTripper returns an http.Transport that limits the number of connection -// to comply with connectionsRateLimitingCount. -func (wn *WebsocketNetwork) getRoundTripper(peer Peer) http.RoundTripper { - return &wn.transport -} - // GetHTTPClient returns a http.Client with a suitable for the network Transport // that would also limit the number of outgoing connections. -func (wn *WebsocketNetwork) GetHTTPClient(peer HTTPPeer) (*http.Client, error) { +func (wn *WebsocketNetwork) GetHTTPClient(address string) (*http.Client, error) { return &http.Client{ - Transport: &wn.transport, + Transport: &HTTPPAddressBoundTransport{address, &wn.transport}, }, nil } +// HTTPPAddressBoundTransport is a http.RoundTripper that sets the scheme and host of the request URL to the given address +type HTTPPAddressBoundTransport struct { + Addr string + InnerTransport http.RoundTripper +} + +// RoundTrip implements http.RoundTripper by adding the schema, host, port, path prefix from the +// parsed address to the request URL and then calling the inner transport. +func (t *HTTPPAddressBoundTransport) RoundTrip(req *http.Request) (*http.Response, error) { + url, err := addr.ParseHostOrURL(t.Addr) + if err != nil { + return nil, err + } + req.URL.Scheme = url.Scheme + req.URL.Host = url.Host + req.URL.Path = path.Join(url.Path, req.URL.Path) + return t.InnerTransport.RoundTrip(req) +} + // filterASCII filter out the non-ascii printable characters out of the given input string and // and replace these with unprintableCharacterGlyph. // It's used as a security qualifier before logging a network-provided data. @@ -2062,9 +2080,9 @@ func filterASCII(unfilteredString string) (filteredString string) { } // tryConnect opens websocket connection and checks initial connection parameters. -// addr should be 'host:port' or a URL, gossipAddr is the websocket endpoint URL -func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { - defer wn.tryConnectReleaseAddr(addr, gossipAddr) +// netAddr should be 'host:port' or a URL, gossipAddr is the websocket endpoint URL +func (wn *WebsocketNetwork) tryConnect(netAddr, gossipAddr string) { + defer wn.tryConnectReleaseAddr(netAddr, gossipAddr) defer func() { if xpanic := recover(); xpanic != nil { wn.log.Errorf("panic in tryConnect: %v", xpanic) @@ -2080,7 +2098,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { var idChallenge identityChallengeValue if wn.identityScheme != nil { - idChallenge = wn.identityScheme.AttachChallenge(requestHeader, addr) + idChallenge = wn.identityScheme.AttachChallenge(requestHeader, netAddr) } // for backward compatibility, include the ProtocolVersion header as well. @@ -2125,7 +2143,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { // we've got a retry-after header. // convert it to a timestamp so that we could use it. retryAfterTime := time.Now().Add(time.Duration(retryAfter) * time.Second) - wn.phonebook.UpdateRetryAfter(addr, retryAfterTime) + wn.phonebook.UpdateRetryAfter(netAddr, retryAfterTime) } default: wn.log.Warnf("ws connect(%s) fail - bad handshake, Status code = %d, Headers = %#v, Body = %s", gossipAddr, response.StatusCode, response.Header, errString) @@ -2166,7 +2184,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { peerID, idVerificationMessage, err = wn.identityScheme.VerifyResponse(response.Header, idChallenge) if err != nil { networkPeerIdentityError.Inc(nil) - wn.log.With("err", err).With("remote", addr).With("local", localAddr).Warn("peer supplied an invalid identity response, abandoning peering") + wn.log.With("err", err).With("remote", netAddr).With("local", localAddr).Warn("peer supplied an invalid identity response, abandoning peering") closeEarly("Invalid identity response") return } @@ -2179,8 +2197,9 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { wn.throttledOutgoingConnections.Add(int32(1)) } + client, _ := wn.GetHTTPClient(netAddr) peer := &wsPeer{ - wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, addr, wn.getRoundTripper(nil), "" /* origin */), + wsPeerCore: makePeerCore(wn.ctx, wn, wn.log, wn.handler.readBuffer, netAddr, client, "" /* origin */), conn: wsPeerWebsocketConnImpl{conn}, outgoing: true, incomingMsgFilter: wn.incomingMsgFilter, @@ -2202,7 +2221,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { wn.peersLock.Unlock() if !ok { networkPeerIdentityDisconnect.Inc(nil) - wn.log.With("remote", addr).With("local", localAddr).Warn("peer deduplicated before adding because the identity is already known") + wn.log.With("remote", netAddr).With("local", localAddr).Warn("peer deduplicated before adding because the identity is already known") closeEarly("Duplicate connection") return } @@ -2210,7 +2229,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { peer.init(wn.config, wn.outgoingMessagesBufferSize) wn.addPeer(peer) - wn.log.With("event", "ConnectedOut").With("remote", addr).With("local", localAddr).Infof("Made outgoing connection to peer %v", addr) + wn.log.With("event", "ConnectedOut").With("remote", netAddr).With("local", localAddr).Infof("Made outgoing connection to peer %v", netAddr) wn.log.EventWithDetails(telemetryspec.Network, telemetryspec.ConnectPeerEvent, telemetryspec.PeerEventDetails{ Address: justHost(conn.RemoteAddr().String()), @@ -2226,7 +2245,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { if len(idVerificationMessage) > 0 { sent := peer.writeNonBlock(context.Background(), idVerificationMessage, true, crypto.Digest{}, time.Now()) if !sent { - wn.log.With("remote", addr).With("local", localAddr).Warn("could not send identity challenge verification") + wn.log.With("remote", netAddr).With("local", localAddr).Warn("could not send identity challenge verification") } } @@ -2241,7 +2260,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { mbytes := append([]byte(protocol.NetPrioResponseTag), resp...) sent := peer.writeNonBlock(context.Background(), mbytes, true, crypto.Digest{}, time.Now()) if !sent { - wn.log.With("remote", addr).With("local", localAddr).Warnf("could not send priority response to %v", addr) + wn.log.With("remote", netAddr).With("local", localAddr).Warnf("could not send priority response to %v", netAddr) } } } @@ -2316,11 +2335,11 @@ func (wn *WebsocketNetwork) removePeer(peer *wsPeer, reason disconnectReason) { // first logging, then take the lock and do the actual accounting. // definitely don't change this to do the logging while holding the lock. localAddr, _ := wn.Address() - logEntry := wn.log.With("event", "Disconnected").With("remote", peer.rootURL).With("local", localAddr) + logEntry := wn.log.With("event", "Disconnected").With("remote", peer.GetAddress()).With("local", localAddr) if peer.outgoing && peer.peerMessageDelay > 0 { logEntry = logEntry.With("messageDelay", peer.peerMessageDelay) } - logEntry.Infof("Peer %s disconnected: %s", peer.rootURL, reason) + logEntry.Infof("Peer %s disconnected: %s", peer.GetAddress(), reason) peerAddr := peer.OriginAddress() // we might be able to get addr out of conn, or it might be closed if peerAddr == "" && peer.conn != nil { @@ -2331,12 +2350,12 @@ func (wn *WebsocketNetwork) removePeer(peer *wsPeer, reason disconnectReason) { } if peerAddr == "" { // didn't get addr from peer, try from url - url, err := url.Parse(peer.rootURL) + url, err := url.Parse(peer.GetAddress()) if err == nil { peerAddr = justHost(url.Host) } else { // use whatever it is - peerAddr = justHost(peer.rootURL) + peerAddr = justHost(peer.GetAddress()) } } eventDetails := telemetryspec.PeerEventDetails{ diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index 1db042cb85..982b1f7a3a 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -2366,8 +2366,8 @@ func TestWebsocketNetworkPrioLimit(t *testing.T) { } if failed { - t.Errorf("NetA had the following two peers priorities : [0]:%s=%d [1]:%s=%d", netA.peers[0].rootURL, netA.peers[0].prioWeight, netA.peers[1].rootURL, netA.peers[1].prioWeight) - t.Errorf("first peer before broadcasting was %s", firstPeer.rootURL) + t.Errorf("NetA had the following two peers priorities : [0]:%s=%d [1]:%s=%d", netA.peers[0].GetAddress(), netA.peers[0].prioWeight, netA.peers[1].GetAddress(), netA.peers[1].prioWeight) + t.Errorf("first peer before broadcasting was %s", firstPeer.GetAddress()) } } @@ -4564,3 +4564,55 @@ func TestWsNetworkPhonebookMix(t *testing.T) { addrs := net.phonebook.GetAddresses(10, phonebook.PhoneBookEntryRelayRole) require.Len(t, addrs, 1) } + +type testRecordingTransport struct { + resultURL string +} + +func (rt *testRecordingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + rt.resultURL = req.URL.String() + return &http.Response{StatusCode: 200}, nil +} + +func TestHTTPPAddressBoundTransport(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // first ensure url.URL.String() on path-only URLs works as expected + var url = &url.URL{} + url.Path = "/test" + require.Equal(t, "/test", url.String()) + + // now test some combinations of address and path + const path = "/test/path" + const expErr = "ERR" + tests := []struct { + addr string + expected string + }{ + {"", expErr}, + {":", expErr}, + {"host:1234/lbr", expErr}, + {"host:1234", "http://host:1234" + path}, + {"http://host:1234", "http://host:1234" + path}, + {"http://host:1234/lbr", "http://host:1234/lbr" + path}, + } + + for _, test := range tests { + recorder := testRecordingTransport{} + tr := HTTPPAddressBoundTransport{ + Addr: test.addr, + InnerTransport: &recorder, + } + req, err := http.NewRequest("GET", path, nil) + require.NoError(t, err) + resp, err := tr.RoundTrip(req) + if test.expected == expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + require.Equal(t, test.expected, recorder.resultURL) + } + } +} diff --git a/network/wsPeer.go b/network/wsPeer.go index da5adb7407..d2786f19d9 100644 --- a/network/wsPeer.go +++ b/network/wsPeer.go @@ -347,26 +347,20 @@ type TCPInfoUnicastPeer interface { } // Create a wsPeerCore object -func makePeerCore(ctx context.Context, net GossipNode, log logging.Logger, readBuffer chan<- IncomingMessage, rootURL string, roundTripper http.RoundTripper, originAddress string) wsPeerCore { - return makePeerCoreWithClient(ctx, net, log, readBuffer, rootURL, &http.Client{Transport: roundTripper}, originAddress) -} - -// Create a wsPeerCore object -func makePeerCoreWithClient(ctx context.Context, net GossipNode, log logging.Logger, readBuffer chan<- IncomingMessage, rootURL string, client *http.Client, originAddress string) wsPeerCore { +func makePeerCore(ctx context.Context, net GossipNode, log logging.Logger, readBuffer chan<- IncomingMessage, addr string, client *http.Client, originAddress string) wsPeerCore { return wsPeerCore{ net: net, netCtx: ctx, log: log, readBuffer: readBuffer, - rootURL: rootURL, + rootURL: addr, originAddress: originAddress, client: client, } } -// GetAddress returns the root url to use to connect to this peer. -// This implements HTTPPeer interface and used by external services to determine where to connect to. -// TODO: should GetAddress be added to Peer interface? +// GetAddress returns the root url to use to identify or connect to this peer. +// This implements HTTPPeer interface and used to distinguish between peers. func (wp *wsPeerCore) GetAddress() string { return wp.rootURL } @@ -515,7 +509,7 @@ func (wp *wsPeer) Respond(ctx context.Context, reqMsg IncomingMessage, outMsg Ou // setup values not trivially assigned func (wp *wsPeer) init(config config.Local, sendBufferLength int) { - wp.log.Debugf("wsPeer init outgoing=%v %#v", wp.outgoing, wp.rootURL) + wp.log.Debugf("wsPeer init outgoing=%v %#v", wp.outgoing, wp.GetAddress()) wp.closing = make(chan struct{}) wp.sendBufferHighPrio = make(chan sendMessages, sendBufferLength) wp.sendBufferBulk = make(chan sendMessages, sendBufferLength) diff --git a/rpcs/httpTxSync.go b/rpcs/httpTxSync.go index 4e1fd29883..311a87cf7b 100644 --- a/rpcs/httpTxSync.go +++ b/rpcs/httpTxSync.go @@ -24,14 +24,12 @@ import ( "math/rand" "net/http" "net/url" - "path" "strings" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" - "github.com/algorand/go-algorand/network/addr" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/bloom" ) @@ -106,24 +104,16 @@ func (hts *HTTPTxSync) Sync(ctx context.Context, bloom *bloom.Filter) (txgroups } var syncURL string hts.rootURL = hpeer.GetAddress() + client := hpeer.GetHTTPClient() if client == nil { - client, err = hts.peers.GetHTTPClient(hpeer) + client, err = hts.peers.GetHTTPClient(hts.rootURL) if err != nil { return nil, fmt.Errorf("HTTPTxSync cannot create a HTTP client for a peer %T %#v: %s", peer, peer, err.Error()) } } - if addr.IsMultiaddr(hts.rootURL) { - syncURL = network.SubstituteGenesisID(hts.peers, path.Join("", TxServiceHTTPPath)) - } else { - parsedURL, err0 := addr.ParseHostOrURL(hts.rootURL) - if err0 != nil { - hts.log.Warnf("txSync bad url %v: %s", hts.rootURL, err0) - return nil, err0 - } - parsedURL.Path = network.SubstituteGenesisID(hts.peers, path.Join(parsedURL.Path, TxServiceHTTPPath)) - syncURL = parsedURL.String() - } + syncURL = network.SubstituteGenesisID(hts.peers, TxServiceHTTPPath) + hts.log.Infof("http sync from %s", syncURL) params := url.Values{} params.Set("bf", bloomParam) diff --git a/rpcs/txService_test.go b/rpcs/txService_test.go index 49cfa3bf49..fcfae1044d 100644 --- a/rpcs/txService_test.go +++ b/rpcs/txService_test.go @@ -64,7 +64,11 @@ func (p testHTTPPeer) GetAddress() string { return string(p) } func (p *testHTTPPeer) GetHTTPClient() *http.Client { - return &http.Client{} + return &http.Client{ + Transport: &network.HTTPPAddressBoundTransport{ + Addr: p.GetAddress(), + InnerTransport: http.DefaultTransport}, + } } func (p *testHTTPPeer) GetHTTPPeer() network.HTTPPeer { return p diff --git a/rpcs/txSyncer_test.go b/rpcs/txSyncer_test.go index 6ad820b0df..eb3e4eab60 100644 --- a/rpcs/txSyncer_test.go +++ b/rpcs/txSyncer_test.go @@ -157,6 +157,7 @@ func (client *mockRPCClient) Sync(ctx context.Context, bloom *bloom.Filter) (txg func (client *mockRPCClient) GetAddress() string { return client.rootURL } + func (client *mockRPCClient) GetHTTPClient() *http.Client { return nil } @@ -170,8 +171,12 @@ func (mca *mockClientAggregator) GetPeers(options ...network.PeerOption) []netwo return mca.peers } -func (mca *mockClientAggregator) GetHTTPClient(peer network.HTTPPeer) (*http.Client, error) { - return &http.Client{Transport: http.DefaultTransport}, nil +func (mca *mockClientAggregator) GetHTTPClient(address string) (*http.Client, error) { + return &http.Client{ + Transport: &network.HTTPPAddressBoundTransport{ + Addr: address, + InnerTransport: http.DefaultTransport}, + }, nil } func TestSyncFromClient(t *testing.T) { From 504c6ba8a9232802124501250650994b8256041f Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Wed, 21 Feb 2024 16:08:41 -0500 Subject: [PATCH 16/38] bump config version to v34 --- config/localTemplate.go | 12 +-- config/local_defaults.go | 2 +- installer/config.json.example | 2 +- test/testdata/configs/config-v34.json | 141 ++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 test/testdata/configs/config-v34.json diff --git a/config/localTemplate.go b/config/localTemplate.go index 837fed86aa..0e05a41167 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -42,7 +42,7 @@ type Local struct { // Version tracks the current version of the defaults so we can migrate old -> new // This is specifically important whenever we decide to change the default value // for an existing parameter. This field tag must be updated any time we add a new version. - Version uint32 `version[0]:"0" version[1]:"1" version[2]:"2" version[3]:"3" version[4]:"4" version[5]:"5" version[6]:"6" version[7]:"7" version[8]:"8" version[9]:"9" version[10]:"10" version[11]:"11" version[12]:"12" version[13]:"13" version[14]:"14" version[15]:"15" version[16]:"16" version[17]:"17" version[18]:"18" version[19]:"19" version[20]:"20" version[21]:"21" version[22]:"22" version[23]:"23" version[24]:"24" version[25]:"25" version[26]:"26" version[27]:"27" version[28]:"28" version[29]:"29" version[30]:"30" version[31]:"31" version[32]:"32" version[33]:"33"` + Version uint32 `version[0]:"0" version[1]:"1" version[2]:"2" version[3]:"3" version[4]:"4" version[5]:"5" version[6]:"6" version[7]:"7" version[8]:"8" version[9]:"9" version[10]:"10" version[11]:"11" version[12]:"12" version[13]:"13" version[14]:"14" version[15]:"15" version[16]:"16" version[17]:"17" version[18]:"18" version[19]:"19" version[20]:"20" version[21]:"21" version[22]:"22" version[23]:"23" version[24]:"24" version[25]:"25" version[26]:"26" version[27]:"27" version[28]:"28" version[29]:"29" version[30]:"30" version[31]:"31" version[32]:"32" version[33]:"33" version[34]:"34"` // Archival nodes retain a full copy of the block history. Non-Archival nodes will delete old blocks and only retain what's need to properly validate blockchain messages (the precise number of recent blocks depends on the consensus parameters. Currently the last 1321 blocks are required). This means that non-Archival nodes require significantly less storage than Archival nodes. If setting this to true for the first time, the existing ledger may need to be deleted to get the historical values stored as the setting only affects current blocks forward. To do this, shutdown the node and delete all .sqlite files within the data/testnet-version directory, except the crash.sqlite file. Restart the node and wait for the node to sync. Archival bool `version[0]:"false"` @@ -598,10 +598,13 @@ type Local struct { EnableP2P bool `version[31]:"false"` // EnableP2PHybridMode turns on both websockets and P2P networking. - EnableP2PHybridMode bool `version[33]:"false"` + EnableP2PHybridMode bool `version[34]:"false"` // P2PListenAddress sets the listen address used for P2P networking, if hybrid mode is set. - P2PListenAddress string `version[33]:""` + P2PListenAddress string `version[34]:""` + + // EnableDHT will turn on the hash table for use with capabilities advertisement + EnableDHTProviders bool `version[34]:"false"` // P2PPersistPeerID will write the private key used for the node's PeerID to the P2PPrivateKeyLocation. // This is only used when P2PEnable is true. If P2PPrivateKey is not specified, it uses the default location. @@ -614,9 +617,6 @@ type Local struct { // DisableAPIAuth turns off authentication for public (non-admin) API endpoints. DisableAPIAuth bool `version[30]:"false"` - - // EnableDHT will turn on the hash table for use with capabilities advertisement - EnableDHTProviders bool `version[33]:"false"` } // DNSBootstrapArray returns an array of one or more DNS Bootstrap identifiers diff --git a/config/local_defaults.go b/config/local_defaults.go index 8817893bd8..ca9b8f8509 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -20,7 +20,7 @@ package config var defaultLocal = Local{ - Version: 33, + Version: 34, AccountUpdatesStatsInterval: 5000000000, AccountsRebuildSynchronousMode: 1, AgreementIncomingBundlesQueueLength: 15, diff --git a/installer/config.json.example b/installer/config.json.example index 433efcf82e..d695679067 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -1,5 +1,5 @@ { - "Version": 33, + "Version": 34, "AccountUpdatesStatsInterval": 5000000000, "AccountsRebuildSynchronousMode": 1, "AgreementIncomingBundlesQueueLength": 15, diff --git a/test/testdata/configs/config-v34.json b/test/testdata/configs/config-v34.json new file mode 100644 index 0000000000..d695679067 --- /dev/null +++ b/test/testdata/configs/config-v34.json @@ -0,0 +1,141 @@ +{ + "Version": 34, + "AccountUpdatesStatsInterval": 5000000000, + "AccountsRebuildSynchronousMode": 1, + "AgreementIncomingBundlesQueueLength": 15, + "AgreementIncomingProposalsQueueLength": 50, + "AgreementIncomingVotesQueueLength": 20000, + "AnnounceParticipationKey": true, + "Archival": false, + "BaseLoggerDebugLevel": 4, + "BlockDBDir": "", + "BlockServiceCustomFallbackEndpoints": "", + "BlockServiceMemCap": 500000000, + "BroadcastConnectionsLimit": -1, + "CadaverDirectory": "", + "CadaverSizeTarget": 0, + "CatchpointDir": "", + "CatchpointFileHistoryLength": 365, + "CatchpointInterval": 10000, + "CatchpointTracking": 0, + "CatchupBlockDownloadRetryAttempts": 1000, + "CatchupBlockValidateMode": 0, + "CatchupFailurePeerRefreshRate": 10, + "CatchupGossipBlockFetchTimeoutSec": 4, + "CatchupHTTPBlockFetchTimeoutSec": 4, + "CatchupLedgerDownloadRetryAttempts": 50, + "CatchupParallelBlocks": 16, + "ColdDataDir": "", + "ConnectionsRateLimitingCount": 60, + "ConnectionsRateLimitingWindowSeconds": 1, + "CrashDBDir": "", + "DNSBootstrapID": ".algorand.network?backup=.algorand.net&dedup=.algorand-.(network|net)", + "DNSSecurityFlags": 1, + "DeadlockDetection": 0, + "DeadlockDetectionThreshold": 30, + "DisableAPIAuth": false, + "DisableLedgerLRUCache": false, + "DisableLocalhostConnectionRateLimit": true, + "DisableNetworking": false, + "DisableOutgoingConnectionThrottling": false, + "EnableAccountUpdatesStats": false, + "EnableAgreementReporting": false, + "EnableAgreementTimeMetrics": false, + "EnableAssembleStats": false, + "EnableBlockService": false, + "EnableDHTProviders": false, + "EnableDeveloperAPI": false, + "EnableExperimentalAPI": false, + "EnableFollowMode": false, + "EnableGossipBlockService": true, + "EnableGossipService": true, + "EnableIncomingMessageFilter": false, + "EnableLedgerService": false, + "EnableMetricReporting": false, + "EnableOutgoingNetworkMessageFiltering": true, + "EnableP2P": false, + "EnableP2PHybridMode": false, + "EnablePingHandler": true, + "EnableProcessBlockStats": false, + "EnableProfiler": false, + "EnableRequestLogger": false, + "EnableRuntimeMetrics": false, + "EnableTopAccountsReporting": false, + "EnableTxBacklogAppRateLimiting": true, + "EnableTxBacklogRateLimiting": true, + "EnableTxnEvalTracer": false, + "EnableUsageLog": false, + "EnableVerbosedTransactionSyncLogging": false, + "EndpointAddress": "127.0.0.1:0", + "FallbackDNSResolverAddress": "", + "ForceFetchTransactions": false, + "ForceRelayMessages": false, + "GossipFanout": 4, + "HeartbeatUpdateInterval": 600, + "HotDataDir": "", + "IncomingConnectionsLimit": 2400, + "IncomingMessageFilterBucketCount": 5, + "IncomingMessageFilterBucketSize": 512, + "LedgerSynchronousMode": 2, + "LogArchiveDir": "", + "LogArchiveMaxAge": "", + "LogArchiveName": "node.archive.log", + "LogFileDir": "", + "LogSizeLimit": 1073741824, + "MaxAPIBoxPerApplication": 100000, + "MaxAPIResourcesPerAccount": 100000, + "MaxAcctLookback": 4, + "MaxBlockHistoryLookback": 0, + "MaxCatchpointDownloadDuration": 43200000000000, + "MaxConnectionsPerIP": 15, + "MinCatchpointFileDownloadBytesPerSecond": 20480, + "NetAddress": "", + "NetworkMessageTraceServer": "", + "NetworkProtocolVersion": "", + "NodeExporterListenAddress": ":9100", + "NodeExporterPath": "./node_exporter", + "OptimizeAccountsDatabaseOnStartup": false, + "OutgoingMessageFilterBucketCount": 3, + "OutgoingMessageFilterBucketSize": 128, + "P2PListenAddress": "", + "P2PPersistPeerID": false, + "P2PPrivateKeyLocation": "", + "ParticipationKeysRefreshInterval": 60000000000, + "PeerConnectionsUpdateInterval": 3600, + "PeerPingPeriodSeconds": 0, + "PriorityPeers": {}, + "ProposalAssemblyTime": 500000000, + "PublicAddress": "", + "ReconnectTime": 60000000000, + "ReservedFDs": 256, + "RestConnectionsHardLimit": 2048, + "RestConnectionsSoftLimit": 1024, + "RestReadTimeoutSeconds": 15, + "RestWriteTimeoutSeconds": 120, + "RunHosted": false, + "StateproofDir": "", + "StorageEngine": "sqlite", + "SuggestedFeeBlockHistory": 3, + "SuggestedFeeSlidingWindowSize": 50, + "TLSCertFile": "", + "TLSKeyFile": "", + "TelemetryToLog": true, + "TrackerDBDir": "", + "TransactionSyncDataExchangeRate": 0, + "TransactionSyncSignificantMessageThreshold": 0, + "TxBacklogAppTxPerSecondRate": 100, + "TxBacklogAppTxRateLimiterMaxSize": 1048576, + "TxBacklogRateLimitingCongestionPct": 50, + "TxBacklogReservedCapacityPerPeer": 20, + "TxBacklogServiceRateWindowSeconds": 10, + "TxBacklogSize": 26000, + "TxIncomingFilterMaxSize": 500000, + "TxIncomingFilteringFlags": 1, + "TxPoolExponentialIncreaseFactor": 2, + "TxPoolSize": 75000, + "TxSyncIntervalSeconds": 60, + "TxSyncServeResponseSize": 1000000, + "TxSyncTimeoutSeconds": 30, + "UseXForwardedForAddressField": "", + "VerifiedTranscationsCacheSize": 150000 +} From ef82b2b4cd28976647ecb07841838ac008823951 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:27:43 -0500 Subject: [PATCH 17/38] p2p: introduce Gossip peer capability (#5935) --- network/p2p/capabilities.go | 2 + network/p2p/capabilities_test.go | 6 +- network/p2p/dnsaddr/resolve_test.go | 20 +- network/p2p/p2p.go | 28 +- network/p2p/p2p_test.go | 4 +- network/p2p/peerstore/peerstore.go | 33 +- network/p2p/peerstore/peerstore_test.go | 141 ++++++-- network/p2p/pubsub.go | 2 +- network/p2pNetwork.go | 295 +++++++++++----- network/p2pNetwork_test.go | 434 ++++++++++++++++++++++-- node/node.go | 3 + node/node_test.go | 103 +++++- 12 files changed, 869 insertions(+), 202 deletions(-) diff --git a/network/p2p/capabilities.go b/network/p2p/capabilities.go index 1ead897e6b..f489773975 100644 --- a/network/p2p/capabilities.go +++ b/network/p2p/capabilities.go @@ -41,6 +41,8 @@ const ( Archival Capability = "archival" // Catchpoints storing nodes Catchpoints = "catchpointStoring" + // Gossip nodes are non permissioned relays + Gossip = "gossip" ) const operationTimeout = time.Second * 5 diff --git a/network/p2p/capabilities_test.go b/network/p2p/capabilities_test.go index 08e04f6f62..2b98b49806 100644 --- a/network/p2p/capabilities_test.go +++ b/network/p2p/capabilities_test.go @@ -47,7 +47,7 @@ func TestCapabilities_Discovery(t *testing.T) { testSize := 3 for i := 0; i < testSize; i++ { tempdir := t.TempDir() - ps, err := peerstore.NewPeerStore(nil) + ps, err := peerstore.NewPeerStore(nil, "") require.NoError(t, err) h, _, err := MakeHost(config.GetDefaultLocal(), tempdir, ps) require.NoError(t, err) @@ -83,7 +83,7 @@ func setupDHTHosts(t *testing.T, numHosts int) []*dht.IpfsDHT { tmpdir := t.TempDir() pk, err := GetPrivKey(cfg, tmpdir) require.NoError(t, err) - ps, err := peerstore.NewPeerStore([]*peer.AddrInfo{}) + ps, err := peerstore.NewPeerStore([]*peer.AddrInfo{}, "") require.NoError(t, err) h, err := libp2p.New( libp2p.ListenAddrStrings("/dns4/localhost/tcp/0"), @@ -134,7 +134,7 @@ func setupCapDiscovery(t *testing.T, numHosts int, numBootstrapPeers int) []*Cap tmpdir := t.TempDir() pk, err := GetPrivKey(cfg, tmpdir) require.NoError(t, err) - ps, err := peerstore.NewPeerStore([]*peer.AddrInfo{}) + ps, err := peerstore.NewPeerStore([]*peer.AddrInfo{}, "") require.NoError(t, err) h, err := libp2p.New( libp2p.ListenAddrStrings("/dns4/localhost/tcp/0"), diff --git a/network/p2p/dnsaddr/resolve_test.go b/network/p2p/dnsaddr/resolve_test.go index 03190ab5f7..30acbd3e5f 100644 --- a/network/p2p/dnsaddr/resolve_test.go +++ b/network/p2p/dnsaddr/resolve_test.go @@ -37,22 +37,22 @@ func TestIsDnsaddr(t *testing.T) { t.Parallel() testcases := []struct { - name string - addr string - expected bool + name string + addr string + isDnsaddr bool }{ - {name: "DnsAddr", addr: "/dnsaddr/foobar.com", expected: true}, - {name: "DnsAddrWithPeerId", addr: "/dnsaddr/foobar.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", expected: true}, - {name: "DnsAddrWithIPPeerId", addr: "/dnsaddr/foobar.com/ip4/127.0.0.1/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", expected: true}, - {name: "Dns4Addr", addr: "/dns4/foobar.com/", expected: false}, - {name: "Dns6Addr", addr: "/dns6/foobar.com/", expected: false}, - {name: "Dns4AddrWithPeerId", addr: "/dns4/foobar.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", expected: false}, + {name: "DnsAddr", addr: "/dnsaddr/foobar.com", isDnsaddr: true}, + {name: "DnsAddrWithPeerId", addr: "/dnsaddr/foobar.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", isDnsaddr: true}, + {name: "DnsAddrWithIPPeerId", addr: "/dnsaddr/foobar.com/ip4/127.0.0.1/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", isDnsaddr: true}, + {name: "Dns4Addr", addr: "/dns4/foobar.com/", isDnsaddr: false}, + {name: "Dns6Addr", addr: "/dns6/foobar.com/", isDnsaddr: false}, + {name: "Dns4AddrWithPeerId", addr: "/dns4/foobar.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", isDnsaddr: false}, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { maddr, err := multiaddr.NewMultiaddr(testcase.addr) require.NoError(t, err) - require.Equal(t, testcase.expected, isDnsaddr(maddr)) + require.Equal(t, testcase.isDnsaddr, isDnsaddr(maddr)) }) } } diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index b399c569b4..e67403a3a1 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -25,8 +25,9 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" + pstore "github.com/algorand/go-algorand/network/p2p/peerstore" + "github.com/algorand/go-algorand/network/phonebook" "github.com/algorand/go-deadlock" - "github.com/multiformats/go-multiaddr" "github.com/libp2p/go-libp2p" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -34,14 +35,20 @@ import ( "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" libp2phttp "github.com/libp2p/go-libp2p/p2p/http" "github.com/libp2p/go-libp2p/p2p/muxer/yamux" "github.com/libp2p/go-libp2p/p2p/security/noise" "github.com/libp2p/go-libp2p/p2p/transport/tcp" + "github.com/multiformats/go-multiaddr" ) +// SubNextCancellable is an abstraction for pubsub.Subscription +type SubNextCancellable interface { + Next(ctx context.Context) (*pubsub.Message, error) + Cancel() +} + // Service defines the interface used by the network integrating with underlying p2p implementation type Service interface { Start() error @@ -56,7 +63,7 @@ type Service interface { Conns() []network.Conn ListPeersForTopic(topic string) []peer.ID - Subscribe(topic string, val pubsub.ValidatorEx) (*pubsub.Subscription, error) + Subscribe(topic string, val pubsub.ValidatorEx) (SubNextCancellable, error) Publish(ctx context.Context, topic string, data []byte) error GetStream(peer.ID) (network.Stream, bool) @@ -83,7 +90,7 @@ const dialTimeout = 30 * time.Second // MakeHost creates a libp2p host but does not start listening. // Use host.Network().Listen() on the returned address to start listening. -func MakeHost(cfg config.Local, datadir string, pstore peerstore.Peerstore) (host.Host, string, error) { +func MakeHost(cfg config.Local, datadir string, pstore *pstore.PeerStore) (host.Host, string, error) { // load stored peer ID, or make ephemeral peer ID privKey, err := GetPrivKey(cfg, datadir) if err != nil { @@ -216,20 +223,21 @@ func (s *serviceImpl) IDSigner() *PeerIDChallengeSigner { // DialPeersUntilTargetCount attempts to establish connections to the provided phonebook addresses func (s *serviceImpl) DialPeersUntilTargetCount(targetConnCount int) { - peerIDs := s.host.Peerstore().Peers() - for _, peerID := range peerIDs { + ps := s.host.Peerstore().(*pstore.PeerStore) + peerIDs := ps.GetAddresses(targetConnCount, phonebook.PhoneBookEntryRelayRole) + for _, peerInfo := range peerIDs { + peerInfo := peerInfo.(*peer.AddrInfo) // if we are at our target count stop trying to connect if len(s.host.Network().Conns()) == targetConnCount { return } // if we are already connected to this peer, skip it - if len(s.host.Network().ConnsToPeer(peerID)) > 0 { + if len(s.host.Network().ConnsToPeer(peerInfo.ID)) > 0 { continue } - peerInfo := s.host.Peerstore().PeerInfo(peerID) - err := s.DialNode(context.Background(), &peerInfo) // leaving the calls as blocking for now, to not over-connect beyond fanout + err := s.DialNode(context.Background(), peerInfo) // leaving the calls as blocking for now, to not over-connect beyond fanout if err != nil { - s.log.Warnf("failed to connect to peer %s: %v", peerID, err) + s.log.Warnf("failed to connect to peer %s: %v", peerInfo.ID, err) } } } diff --git a/network/p2p/p2p_test.go b/network/p2p/p2p_test.go index 005dbc8330..4935238a9e 100644 --- a/network/p2p/p2p_test.go +++ b/network/p2p/p2p_test.go @@ -89,7 +89,7 @@ func TestP2PStreamingHost(t *testing.T) { cfg := config.GetDefaultLocal() dir := t.TempDir() - pstore, err := peerstore.NewPeerStore(nil) + pstore, err := peerstore.NewPeerStore(nil, "") require.NoError(t, err) h, la, err := MakeHost(cfg, dir, pstore) require.NoError(t, err) @@ -115,7 +115,7 @@ func TestP2PStreamingHost(t *testing.T) { ID: h.ID(), Addrs: h.Addrs(), } - cpstore, err := peerstore.NewPeerStore([]*peer.AddrInfo{&addrInfo}) + cpstore, err := peerstore.NewPeerStore([]*peer.AddrInfo{&addrInfo}, "") require.NoError(t, err) c, _, err := MakeHost(cfg, dir, cpstore) require.NoError(t, err) diff --git a/network/p2p/peerstore/peerstore.go b/network/p2p/peerstore/peerstore.go index d2a088e433..9bbb48ab46 100644 --- a/network/p2p/peerstore/peerstore.go +++ b/network/p2p/peerstore/peerstore.go @@ -68,18 +68,20 @@ type peerStoreCAB interface { } // NewPeerStore creates a new peerstore backed by a datastore. -func NewPeerStore(addrInfo []*peer.AddrInfo) (*PeerStore, error) { +func NewPeerStore(addrInfo []*peer.AddrInfo, network string) (*PeerStore, error) { ps, err := mempstore.NewPeerstore() if err != nil { return nil, fmt.Errorf("cannot initialize a peerstore: %w", err) } // initialize peerstore with addresses + peers := make([]interface{}, len(addrInfo)) for i := 0; i < len(addrInfo); i++ { - info := addrInfo[i] - ps.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) + peers[i] = addrInfo[i] } + pstore := &PeerStore{peerStoreCAB: ps} + pstore.AddPersistentPeers(peers, network, phonebook.PhoneBookEntryRelayRole) return pstore, nil } @@ -98,7 +100,7 @@ func MakePhonebook(connectionsRateLimitingCount uint, } // GetAddresses returns up to N addresses, but may return fewer -func (ps *PeerStore) GetAddresses(n int, role phonebook.PhoneBookEntryRoles) []string { +func (ps *PeerStore) GetAddresses(n int, role phonebook.PhoneBookEntryRoles) []interface{} { return shuffleSelect(ps.filterRetryTime(time.Now(), role), n) } @@ -206,7 +208,7 @@ func (ps *PeerStore) UpdateConnectionTime(addr interface{}, provisionalTime time } // ReplacePeerList replaces the peer list for the given networkName and role. -func (ps *PeerStore) ReplacePeerList(addressesThey []string, networkName string, role phonebook.PhoneBookEntryRoles) { +func (ps *PeerStore) ReplacePeerList(addressesThey []interface{}, networkName string, role phonebook.PhoneBookEntryRoles) { // prepare a map of items we'd like to remove. removeItems := make(map[peer.ID]bool, 0) peerIDs := ps.Peers() @@ -221,10 +223,7 @@ func (ps *PeerStore) ReplacePeerList(addressesThey []string, networkName string, } for _, addr := range addressesThey { - info, err := peerInfoFromDomainPort(addr) - if err != nil { - return - } + info := addr.(*peer.AddrInfo) data, _ := ps.Get(info.ID, addressDataKey) if data != nil { // we already have this. @@ -294,7 +293,7 @@ func (ps *PeerStore) deletePhonebookEntry(peerID peer.ID, networkName string) { } ad := data.(addressData) delete(ad.networkNames, networkName) - if 0 == len(ad.networkNames) { + if len(ad.networkNames) == 0 { ps.ClearAddrs(peerID) _ = ps.Put(peerID, addressDataKey, nil) } @@ -319,21 +318,23 @@ func (ps *PeerStore) popNElements(n int, peerID peer.ID) { _ = ps.Put(peerID, addressDataKey, ad) } -func (ps *PeerStore) filterRetryTime(t time.Time, role phonebook.PhoneBookEntryRoles) []string { - o := make([]string, 0, len(ps.Peers())) +func (ps *PeerStore) filterRetryTime(t time.Time, role phonebook.PhoneBookEntryRoles) []interface{} { + o := make([]interface{}, 0, len(ps.Peers())) for _, peerID := range ps.Peers() { data, _ := ps.Get(peerID, addressDataKey) if data != nil { ad := data.(addressData) if t.After(ad.retryAfter) && role == ad.role { - o = append(o, string(peerID)) + mas := ps.Addrs(peerID) + info := peer.AddrInfo{ID: peerID, Addrs: mas} + o = append(o, &info) } } } return o } -func shuffleSelect(set []string, n int) []string { +func shuffleSelect(set []interface{}, n int) []interface{} { if n >= len(set) || n == getAllAddresses { // return shuffled copy of everything out := slices.Clone(set) @@ -350,13 +351,13 @@ func shuffleSelect(set []string, n int) []string { } } } - out := make([]string, n) + out := make([]interface{}, n) for i, index := range indexSample { out[i] = set[index] } return out } -func shuffleStrings(set []string) { +func shuffleStrings(set []interface{}) { rand.Shuffle(len(set), func(i, j int) { set[i], set[j] = set[j], set[i] }) } diff --git a/network/p2p/peerstore/peerstore_test.go b/network/p2p/peerstore/peerstore_test.go index ebe45b87af..83edae8fb6 100644 --- a/network/p2p/peerstore/peerstore_test.go +++ b/network/p2p/peerstore/peerstore_test.go @@ -51,7 +51,7 @@ func TestPeerstore(t *testing.T) { } addrInfo, _ := PeerInfoFromAddrs(peerAddrs) - ps, err := NewPeerStore(addrInfo) + ps, err := NewPeerStore(addrInfo, "net-id") require.NoError(t, err) defer ps.Close() @@ -89,12 +89,13 @@ func TestPeerstore(t *testing.T) { } -func testPhonebookAll(t *testing.T, set []string, ph *PeerStore) { +func testPhonebookAll(t *testing.T, set []*peer.AddrInfo, ph *PeerStore) { actual := ph.GetAddresses(len(set), PhoneBookEntryRelayRole) for _, got := range actual { + info := got.(*peer.AddrInfo) ok := false for _, known := range set { - if got == known { + if info.ID == known.ID { ok = true break } @@ -106,7 +107,8 @@ func testPhonebookAll(t *testing.T, set []string, ph *PeerStore) { for _, known := range set { ok := false for _, got := range actual { - if got == known { + info := got.(*peer.AddrInfo) + if info.ID == known.ID { ok = true break } @@ -117,18 +119,19 @@ func testPhonebookAll(t *testing.T, set []string, ph *PeerStore) { } } -func testPhonebookUniform(t *testing.T, set []string, ph *PeerStore, getsize int) { +func testPhonebookUniform(t *testing.T, set []*peer.AddrInfo, ph *PeerStore, getsize int) { uniformityTestLength := 250000 / len(set) expected := (uniformityTestLength * getsize) / len(set) counts := make(map[string]int) for i := 0; i < len(set); i++ { - counts[set[i]] = 0 + counts[set[i].ID.String()] = 0 } for i := 0; i < uniformityTestLength; i++ { actual := ph.GetAddresses(getsize, PhoneBookEntryRelayRole) for _, xa := range actual { - if _, ok := counts[xa]; ok { - counts[xa]++ + info := xa.(*peer.AddrInfo) + if _, ok := counts[info.ID.String()]; ok { + counts[info.ID.String()]++ } } } @@ -151,6 +154,13 @@ func TestArrayPhonebookAll(t *testing.T) { partitiontest.PartitionTest(t) set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + infoSet := make([]*peer.AddrInfo, 0) + for _, addr := range set { + info, err := peerInfoFromDomainPort(addr) + require.NoError(t, err) + infoSet = append(infoSet, info) + } + ph, err := MakePhonebook(1, 1*time.Millisecond) require.NoError(t, err) for _, addr := range set { @@ -159,13 +169,20 @@ func TestArrayPhonebookAll(t *testing.T) { ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) ph.Put(info.ID, addressDataKey, entry) } - testPhonebookAll(t, set, ph) + testPhonebookAll(t, infoSet, ph) } func TestArrayPhonebookUniform1(t *testing.T) { partitiontest.PartitionTest(t) set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + infoSet := make([]*peer.AddrInfo, 0) + for _, addr := range set { + info, err := peerInfoFromDomainPort(addr) + require.NoError(t, err) + infoSet = append(infoSet, info) + } + ph, err := MakePhonebook(1, 1*time.Millisecond) require.NoError(t, err) for _, addr := range set { @@ -174,13 +191,20 @@ func TestArrayPhonebookUniform1(t *testing.T) { ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) ph.Put(info.ID, addressDataKey, entry) } - testPhonebookUniform(t, set, ph, 1) + testPhonebookUniform(t, infoSet, ph, 1) } func TestArrayPhonebookUniform3(t *testing.T) { partitiontest.PartitionTest(t) set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} + infoSet := make([]*peer.AddrInfo, 0) + for _, addr := range set { + info, err := peerInfoFromDomainPort(addr) + require.NoError(t, err) + infoSet = append(infoSet, info) + } + ph, err := MakePhonebook(1, 1*time.Millisecond) require.NoError(t, err) for _, addr := range set { @@ -189,19 +213,25 @@ func TestArrayPhonebookUniform3(t *testing.T) { ph.AddAddrs(info.ID, info.Addrs, libp2p.AddressTTL) ph.Put(info.ID, addressDataKey, entry) } - testPhonebookUniform(t, set, ph, 3) + testPhonebookUniform(t, infoSet, ph, 3) } func TestMultiPhonebook(t *testing.T) { partitiontest.PartitionTest(t) set := []string{"a:4041", "b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} - pha := make([]string, 0) - for _, e := range set[:5] { + infoSet := make([]*peer.AddrInfo, 0) + for _, addr := range set { + info, err := peerInfoFromDomainPort(addr) + require.NoError(t, err) + infoSet = append(infoSet, info) + } + pha := make([]interface{}, 0) + for _, e := range infoSet[:5] { pha = append(pha, e) } - phb := make([]string, 0) - for _, e := range set[5:] { + phb := make([]interface{}, 0) + for _, e := range infoSet[5:] { phb = append(phb, e) } @@ -210,9 +240,9 @@ func TestMultiPhonebook(t *testing.T) { ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) - testPhonebookAll(t, set, ph) - testPhonebookUniform(t, set, ph, 1) - testPhonebookUniform(t, set, ph, 3) + testPhonebookAll(t, infoSet, ph) + testPhonebookUniform(t, infoSet, ph, 1) + testPhonebookUniform(t, infoSet, ph, 3) } // TestMultiPhonebookPersistentPeers validates that the peers added via Phonebook.AddPersistentPeers @@ -224,12 +254,19 @@ func TestMultiPhonebookPersistentPeers(t *testing.T) { require.NoError(t, err) persistentPeers := []interface{}{info} set := []string{"b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} - pha := make([]string, 0) - for _, e := range set[:5] { + infoSet := make([]*peer.AddrInfo, 0) + for _, addr := range set { + info, err := peerInfoFromDomainPort(addr) + require.NoError(t, err) + infoSet = append(infoSet, info) + } + + pha := make([]interface{}, 0) + for _, e := range infoSet[:5] { pha = append(pha, e) } - phb := make([]string, 0) - for _, e := range set[5:] { + phb := make([]interface{}, 0) + for _, e := range infoSet[5:] { phb = append(phb, e) } ph, err := MakePhonebook(1, 1*time.Millisecond) @@ -239,12 +276,19 @@ func TestMultiPhonebookPersistentPeers(t *testing.T) { ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) - testPhonebookAll(t, append(set, "a:4041"), ph) + testPhonebookAll(t, append(infoSet, info), ph) allAddresses := ph.GetAddresses(len(set)+len(persistentPeers), PhoneBookEntryRelayRole) for _, pp := range persistentPeers { pp := pp.(*peer.AddrInfo) - // TODO: modify as needed when completely switching from peerID = "host:port" to peer.AddrInfo - require.Contains(t, allAddresses, string(pp.ID)) + found := false + for _, addr := range allAddresses { + addr := addr.(*peer.AddrInfo) + if addr.ID == pp.ID { + found = true + break + } + } + require.True(t, found, fmt.Sprintf("%s not found in %v", string(pp.ID), allAddresses)) } } @@ -252,12 +296,19 @@ func TestMultiPhonebookDuplicateFiltering(t *testing.T) { partitiontest.PartitionTest(t) set := []string{"b:4042", "c:4043", "d:4044", "e:4045", "f:4046", "g:4047", "h:4048", "i:4049", "j:4010"} - pha := make([]string, 0) - for _, e := range set[:7] { + infoSet := make([]*peer.AddrInfo, 0) + for _, addr := range set { + info, err := peerInfoFromDomainPort(addr) + require.NoError(t, err) + infoSet = append(infoSet, info) + } + + pha := make([]interface{}, 0) + for _, e := range infoSet[:7] { pha = append(pha, e) } - phb := make([]string, 0) - for _, e := range set[3:] { + phb := make([]interface{}, 0) + for _, e := range infoSet[3:] { phb = append(phb, e) } ph, err := MakePhonebook(1, 1*time.Millisecond) @@ -265,9 +316,9 @@ func TestMultiPhonebookDuplicateFiltering(t *testing.T) { ph.ReplacePeerList(pha, "pha", PhoneBookEntryRelayRole) ph.ReplacePeerList(phb, "phb", PhoneBookEntryRelayRole) - testPhonebookAll(t, set, ph) - testPhonebookUniform(t, set, ph, 1) - testPhonebookUniform(t, set, ph, 3) + testPhonebookAll(t, infoSet, ph) + testPhonebookUniform(t, infoSet, ph, 1) + testPhonebookUniform(t, infoSet, ph, 3) } func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { @@ -292,7 +343,7 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { // Test the addresses are populated in the phonebook and a // time can be added to one of them - entries.ReplacePeerList([]string{addr1, addr2}, "default", PhoneBookEntryRelayRole) + entries.ReplacePeerList([]interface{}{info1, info2}, "default", PhoneBookEntryRelayRole) addrInPhonebook, waitTime, provisionalTime := entries.GetConnectionWaitTime(info1) require.Equal(t, true, addrInPhonebook) require.Equal(t, time.Duration(0), waitTime) @@ -407,10 +458,24 @@ func TestPhonebookRoles(t *testing.T) { relaysSet := []string{"relay1:4040", "relay2:4041", "relay3:4042"} archiverSet := []string{"archiver1:1111", "archiver2:1112", "archiver3:1113"} + infoRelaySet := make([]interface{}, 0) + for _, addr := range relaysSet { + info, err := peerInfoFromDomainPort(addr) + require.NoError(t, err) + infoRelaySet = append(infoRelaySet, info) + } + + infoArchiverSet := make([]interface{}, 0) + for _, addr := range archiverSet { + info, err := peerInfoFromDomainPort(addr) + require.NoError(t, err) + infoArchiverSet = append(infoArchiverSet, info) + } + ph, err := MakePhonebook(1, 1) require.NoError(t, err) - ph.ReplacePeerList(relaysSet, "default", PhoneBookEntryRelayRole) - ph.ReplacePeerList(archiverSet, "default", PhoneBookEntryArchiverRole) + ph.ReplacePeerList(infoRelaySet, "default", PhoneBookEntryRelayRole) + ph.ReplacePeerList(infoArchiverSet, "default", PhoneBookEntryArchiverRole) require.Equal(t, len(relaysSet)+len(archiverSet), len(ph.Peers())) require.Equal(t, len(relaysSet)+len(archiverSet), ph.Length()) @@ -420,11 +485,13 @@ func TestPhonebookRoles(t *testing.T) { entries := ph.GetAddresses(l, role) if role == PhoneBookEntryRelayRole { for _, entry := range entries { - require.Contains(t, entry, "relay") + entry := entry.(*peer.AddrInfo) + require.Contains(t, string(entry.ID), "relay") } } else if role == PhoneBookEntryArchiverRole { for _, entry := range entries { - require.Contains(t, entry, "archiver") + entry := entry.(*peer.AddrInfo) + require.Contains(t, string(entry.ID), "archiver") } } } diff --git a/network/p2p/pubsub.go b/network/p2p/pubsub.go index 372c9249c8..5d1c144632 100644 --- a/network/p2p/pubsub.go +++ b/network/p2p/pubsub.go @@ -133,7 +133,7 @@ func (s *serviceImpl) getOrCreateTopic(topicName string) (*pubsub.Topic, error) } // Subscribe returns a subscription to the given topic -func (s *serviceImpl) Subscribe(topic string, val pubsub.ValidatorEx) (*pubsub.Subscription, error) { +func (s *serviceImpl) Subscribe(topic string, val pubsub.ValidatorEx) (SubNextCancellable, error) { if err := s.pubsub.RegisterTopicValidator(topic, val); err != nil { return nil, err } diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index be59e09d62..ce969e1791 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -31,6 +31,7 @@ import ( "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/network/p2p/dnsaddr" "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" @@ -67,33 +68,38 @@ type P2PNetwork struct { wsPeersChangeCounter atomic.Int32 wsPeersConnectivityCheckTicker *time.Ticker + relayMessages bool // True if we should relay messages from other nodes (nominally true for relays, false otherwise) + wantTXGossip atomic.Bool + capabilitiesDiscovery *p2p.CapabilitiesDiscovery - bootstrapper bootstrapper - nodeInfo NodeInfo - pstore *peerstore.PeerStore - httpServer *p2p.HTTPServer + bootstrapperStart func() + bootstrapperStop func() + nodeInfo NodeInfo + pstore *peerstore.PeerStore + httpServer *p2p.HTTPServer } type bootstrapper struct { - cfg config.Local - networkID protocol.NetworkID - phonebookPeers []*peer.AddrInfo - resolveControler dnsaddr.ResolveController - started bool + cfg config.Local + networkID protocol.NetworkID + phonebookPeers []*peer.AddrInfo + resolveController dnsaddr.ResolveController + started atomic.Bool + log logging.Logger } func (b *bootstrapper) start() { - b.started = true + b.started.Store(true) } func (b *bootstrapper) stop() { - b.started = false + b.started.Store(false) } func (b *bootstrapper) BootstrapFunc() []peer.AddrInfo { // not started yet, do not give it any peers - if !b.started { + if !b.started.Load() { return nil } @@ -108,30 +114,76 @@ func (b *bootstrapper) BootstrapFunc() []peer.AddrInfo { return addrs } - return getBootstrapPeers(b.cfg, b.networkID, b.resolveControler) + return dnsLookupBootstrapPeers(b.log, b.cfg, b.networkID, b.resolveController) } -// getBootstrapPeers looks up a list of Multiaddrs strings from the dnsaddr records at the primary +// dnsLookupBootstrapPeers looks up a list of Multiaddrs strings from the dnsaddr records at the primary // SRV record domain. -func getBootstrapPeers(cfg config.Local, network protocol.NetworkID, controller dnsaddr.ResolveController) []peer.AddrInfo { +func dnsLookupBootstrapPeers(log logging.Logger, cfg config.Local, network protocol.NetworkID, controller dnsaddr.ResolveController) []peer.AddrInfo { var addrs []peer.AddrInfo bootstraps := cfg.DNSBootstrapArray(network) for _, dnsBootstrap := range bootstraps { - resolvedAddrs, err := dnsaddr.MultiaddrsFromResolver(dnsBootstrap.PrimarySRVBootstrap, controller) - if err != nil { - continue + var resolvedAddrs, resolvedAddrsBackup []multiaddr.Multiaddr + var errPrim, errBackup error + resolvedAddrs, errPrim = dnsaddr.MultiaddrsFromResolver(dnsBootstrap.PrimarySRVBootstrap, controller) + if errPrim != nil { + log.Infof("Failed to resolve bootstrap peers from %s: %v", dnsBootstrap.PrimarySRVBootstrap, errPrim) } - for _, resolvedAddr := range resolvedAddrs { - info, err0 := peer.AddrInfoFromP2pAddr(resolvedAddr) - if err0 != nil { - continue + if dnsBootstrap.BackupSRVBootstrap != "" { + resolvedAddrsBackup, errBackup = dnsaddr.MultiaddrsFromResolver(dnsBootstrap.BackupSRVBootstrap, controller) + if errBackup != nil { + log.Infof("Failed to resolve bootstrap peers from %s: %v", dnsBootstrap.BackupSRVBootstrap, errBackup) } - addrs = append(addrs, *info) + } + + if len(resolvedAddrs) > 0 || len(resolvedAddrsBackup) > 0 { + resolvedAddrInfos := mergeP2PMultiaddrResolvedAddresses(resolvedAddrs, resolvedAddrsBackup) + addrs = append(addrs, resolvedAddrInfos...) } } return addrs } +func mergeP2PMultiaddrResolvedAddresses(primary, backup []multiaddr.Multiaddr) []peer.AddrInfo { + // deduplicate addresses by PeerID + unique := make(map[peer.ID]*peer.AddrInfo) + for _, addr := range primary { + info, err0 := peer.AddrInfoFromP2pAddr(addr) + if err0 != nil { + continue + } + unique[info.ID] = info + } + for _, addr := range backup { + info, err0 := peer.AddrInfoFromP2pAddr(addr) + if err0 != nil { + continue + } + unique[info.ID] = info + } + var result []peer.AddrInfo + for _, addr := range unique { + result = append(result, *addr) + } + return result +} + +func mergeP2PAddrInfoResolvedAddresses(primary, backup []peer.AddrInfo) []peer.AddrInfo { + // deduplicate addresses by PeerID + unique := make(map[peer.ID]peer.AddrInfo) + for _, addr := range primary { + unique[addr.ID] = addr + } + for _, addr := range backup { + unique[addr.ID] = addr + } + var result []peer.AddrInfo + for _, addr := range unique { + result = append(result, addr) + } + return result +} + type p2pPeerStats struct { txReceived atomic.Uint64 } @@ -152,23 +204,26 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo for malAddr, malErr := range malformedAddrs { log.Infof("Ignoring malformed phonebook address %s: %s", malAddr, malErr) } - pstore, err := peerstore.NewPeerStore(addrInfo) + pstore, err := peerstore.NewPeerStore(addrInfo, string(networkID)) if err != nil { return nil, err } + relayMessages := cfg.IsGossipServer() || cfg.ForceRelayMessages net := &P2PNetwork{ - log: log, - config: cfg, - genesisID: genesisID, - networkID: networkID, - topicTags: map[protocol.Tag]string{"TX": p2p.TXTopicName}, - wsPeers: make(map[peer.ID]*wsPeer), - wsPeersToIDs: make(map[*wsPeer]peer.ID), - peerStats: make(map[peer.ID]*p2pPeerStats), - nodeInfo: node, - pstore: pstore, + log: log, + config: cfg, + genesisID: genesisID, + networkID: networkID, + topicTags: map[protocol.Tag]string{protocol.TxnTag: p2p.TXTopicName}, + wsPeers: make(map[peer.ID]*wsPeer), + wsPeersToIDs: make(map[*wsPeer]peer.ID), + peerStats: make(map[peer.ID]*p2pPeerStats), + nodeInfo: node, + pstore: pstore, + relayMessages: relayMessages, } + net.ctx, net.ctxCancel = context.WithCancel(context.Background()) net.handler = msgHandler{ ctx: net.ctx, @@ -196,11 +251,14 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo } bootstrapper := &bootstrapper{ - cfg: cfg, - networkID: networkID, - phonebookPeers: addrInfo, - resolveControler: dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecuritySRVEnforced(), ""), + cfg: cfg, + networkID: networkID, + phonebookPeers: addrInfo, + resolveController: dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecuritySRVEnforced(), ""), + log: net.log, } + net.bootstrapperStart = bootstrapper.start + net.bootstrapperStop = bootstrapper.stop if cfg.EnableDHTProviders { disc, err0 := p2p.MakeCapabilitiesDiscovery(net.ctx, cfg, h, networkID, net.log, bootstrapper.BootstrapFunc) @@ -240,13 +298,18 @@ func (n *P2PNetwork) PeerIDSigner() identityChallengeSigner { // Start threads, listen on sockets. func (n *P2PNetwork) Start() error { - n.wg.Add(1) - n.bootstrapper.start() + n.bootstrapperStart() err := n.service.Start() if err != nil { return err } - go n.txTopicHandleLoop() + + wantTXGossip := n.relayMessages || n.config.ForceFetchTransactions || n.nodeInfo.IsParticipating() + if wantTXGossip { + n.wantTXGossip.Store(true) + n.wg.Add(1) + go n.txTopicHandleLoop() + } if n.wsPeersConnectivityCheckTicker != nil { n.wsPeersConnectivityCheckTicker.Stop() @@ -263,7 +326,6 @@ func (n *P2PNetwork) Start() error { n.wg.Add(1) go n.broadcaster.broadcastThread(&n.wg, n) - n.service.DialPeersUntilTargetCount(n.config.GossipFanout) n.wg.Add(1) go n.meshThread() @@ -289,7 +351,7 @@ func (n *P2PNetwork) Stop() { n.innerStop() n.ctxCancel() n.service.Close() - n.bootstrapper.stop() + n.bootstrapperStop() n.httpServer.Close() n.wg.Wait() } @@ -314,14 +376,46 @@ func (n *P2PNetwork) innerStop() { closeGroup.Wait() } +// meshThreadInner fetches nodes from DHT and attempts to connect to them +func (n *P2PNetwork) meshThreadInner() { + defer n.service.DialPeersUntilTargetCount(n.config.GossipFanout) + + // fetch peers from DNS + var dnsPeers, dhtPeers []peer.AddrInfo + dnsPeers = dnsLookupBootstrapPeers(n.log, n.config, n.networkID, dnsaddr.NewMultiaddrDNSResolveController(n.config.DNSSecuritySRVEnforced(), "")) + + // discover peers from DHT + if n.capabilitiesDiscovery != nil { + var err error + dhtPeers, err = n.capabilitiesDiscovery.PeersForCapability(p2p.Gossip, n.config.GossipFanout) + if err != nil { + n.log.Warnf("Error getting relay nodes from capabilities discovery: %v", err) + return + } + n.log.Debugf("Discovered %d gossip peers from DHT", len(dhtPeers)) + } + + peers := mergeP2PAddrInfoResolvedAddresses(dnsPeers, dhtPeers) + replace := make([]interface{}, 0, len(peers)) + for i := range peers { + replace = append(replace, &peers[i]) + } + n.pstore.ReplacePeerList(replace, string(n.networkID), phonebook.PhoneBookEntryRelayRole) +} + func (n *P2PNetwork) meshThread() { defer n.wg.Done() - timer := time.NewTicker(meshThreadInterval) + timer := time.NewTicker(1) // start immediately and reset after defer timer.Stop() + var resetTimer bool for { select { case <-timer.C: - n.service.DialPeersUntilTargetCount(n.config.GossipFanout) + n.meshThreadInner() + if !resetTimer { + timer.Reset(meshThreadInterval) + resetTimer = true + } case <-n.ctx.Done(): return } @@ -382,7 +476,10 @@ func (n *P2PNetwork) Broadcast(ctx context.Context, tag protocol.Tag, data []byt // Relay message func (n *P2PNetwork) Relay(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error { - return n.Broadcast(ctx, tag, data, wait, except) + if n.relayMessages { + return n.Broadcast(ctx, tag, data, wait, except) + } + return nil } // Disconnect from a peer, probably due to protocol errors. @@ -435,6 +532,33 @@ func (n *P2PNetwork) RegisterHTTPHandler(path string, handler http.Handler) { // `replace` optionally drops existing connections before making new ones. // `quit` chan allows cancellation. func (n *P2PNetwork) RequestConnectOutgoing(replace bool, quit <-chan struct{}) { + n.meshThreadInner() +} + +func addrInfoToWsPeerCore(n *P2PNetwork, addrInfo *peer.AddrInfo) (wsPeerCore, bool) { + mas, err := peer.AddrInfoToP2pAddrs(addrInfo) + if err != nil { + n.log.Warnf("Archival AddrInfo conversion error: %v", err) + return wsPeerCore{}, false + } + if len(mas) == 0 { + n.log.Warnf("Archival AddrInfo: empty multiaddr for : %v", addrInfo) + return wsPeerCore{}, false + } + addr := mas[0].String() + + maxIdleConnsPerHost := int(n.config.ConnectionsRateLimitingCount) + client, err := p2p.MakeHTTPClientWithRateLimit(addrInfo, n.pstore, limitcaller.DefaultQueueingTimeout, maxIdleConnsPerHost) + if err != nil { + n.log.Warnf("MakeHTTPClient failed: %v", err) + return wsPeerCore{}, false + } + + peerCore := makePeerCore( + n.ctx, n, n.log, n.handler.readBuffer, + addr, client, "", /*origin address*/ + ) + return peerCore, true } // GetPeers returns a list of Peers we could potentially send a direct message to. @@ -451,24 +575,21 @@ func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { } n.wsPeersLock.RUnlock() case PeersPhonebookRelays: - // TODO: query peerstore for PhoneBookEntryRelayRole - // TODO: currently peerstore is not populated in a way to store roles - // return all nodes at the moment - - // // return copy of phonebook, which probably also contains peers we're connected to, but if it doesn't maybe we shouldn't be making new connections to those peers (because they disappeared from the directory) - // addrs := n.pstore.GetAddresses(1000, PhoneBookEntryRelayRole) - // for _, addr := range addrs { - // peerCore := makePeerCore(n.ctx, n, n.log, n.handler.readBuffer, addr, n.GetRoundTripper(nil), "" /*origin address*/) - // peers = append(peers, &peerCore) - // } - - // temporary return all nodes - n.wsPeersLock.RLock() - for _, peer := range n.wsPeers { - peers = append(peers, Peer(peer)) + const maxNodes = 100 + peerIDs := n.pstore.GetAddresses(maxNodes, phonebook.PhoneBookEntryRelayRole) + for _, peerInfo := range peerIDs { + peerInfo := peerInfo.(*peer.AddrInfo) + if peerCore, ok := addrInfoToWsPeerCore(n, peerInfo); ok { + peers = append(peers, &peerCore) + } + } + if n.log.GetLevel() >= logging.Debug && len(peers) > 0 { + addrs := make([]string, 0, len(peers)) + for _, peer := range peers { + addrs = append(addrs, peer.(*wsPeerCore).GetAddress()) + } + n.log.Debugf("Relay node(s) from peerstore: %v", addrs) } - n.wsPeersLock.RUnlock() - case PeersPhonebookArchivalNodes: // query known archival nodes from DHT if enabled if n.config.EnableDHTProviders { @@ -480,30 +601,11 @@ func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { } n.log.Debugf("Got %d archival node(s) from DHT", len(infos)) for _, addrInfo := range infos { + // TODO: remove after go1.22 info := addrInfo - mas, err := peer.AddrInfoToP2pAddrs(&info) - if err != nil { - n.log.Warnf("Archival AddrInfo conversion error: %v", err) - continue - } - if len(mas) == 0 { - n.log.Warnf("Archival AddrInfo: empty multiaddr for : %v", addrInfo) - continue + if peerCore, ok := addrInfoToWsPeerCore(n, &info); ok { + peers = append(peers, &peerCore) } - addr := mas[0].String() - - maxIdleConnsPerHost := int(n.config.ConnectionsRateLimitingCount) - client, err := p2p.MakeHTTPClientWithRateLimit(&info, n.pstore, limitcaller.DefaultQueueingTimeout, maxIdleConnsPerHost) - if err != nil { - n.log.Warnf("MakeHTTPClient failed: %v", err) - continue - } - - peerCore := makePeerCore( - n.ctx, n, n.log, n.handler.readBuffer, - addr, client, "", /*origin address*/ - ) - peers = append(peers, &peerCore) } if n.log.GetLevel() >= logging.Debug && len(peers) > 0 { addrs := make([]string, 0, len(peers)) @@ -560,7 +662,19 @@ func (n *P2PNetwork) GetHTTPClient(address string) (*http.Client, error) { // this is the only indication that we have that we haven't formed a clique, where all incoming messages // arrive very quickly, but might be missing some votes. The usage of this call is expected to have similar // characteristics as with a watchdog timer. -func (n *P2PNetwork) OnNetworkAdvance() {} +func (n *P2PNetwork) OnNetworkAdvance() { + if n.nodeInfo != nil { + old := n.wantTXGossip.Load() + new := n.nodeInfo.IsParticipating() + if old != new { + n.wantTXGossip.Store(new) + if new { + n.wg.Add(1) + go n.txTopicHandleLoop() + } + } + } +} // GetHTTPRequestConnection returns the underlying connection for the given request. Note that the request must be the same // request that was provided to the http handler ( or provide a fallback Context() to that ) @@ -720,6 +834,7 @@ func (n *P2PNetwork) txTopicHandleLoop() { n.log.Errorf("Failed to subscribe to topic %s: %v", p2p.TXTopicName, err) return } + n.log.Debugf("Subscribed to topic %s", p2p.TXTopicName) for { msg, err := sub.Next(n.ctx) @@ -727,6 +842,7 @@ func (n *P2PNetwork) txTopicHandleLoop() { if err != pubsub.ErrSubscriptionCancelled && err != context.Canceled { n.log.Errorf("Error reading from subscription %v, peerId %s", err, n.service.ID()) } + n.log.Debugf("Canceling subscription to topic %s due Next error", p2p.TXTopicName) sub.Cancel() return } @@ -735,6 +851,13 @@ func (n *P2PNetwork) txTopicHandleLoop() { // from gossipsub's point of view, it's just waiting to hear back from the validator, // and txHandler does all its work in the validator, so we don't need to do anything here _ = msg + + // participation or configuration change, cancel subscription and quit + if !n.wantTXGossip.Load() { + n.log.Debugf("Canceling subscription to topic %s due participation change", p2p.TXTopicName) + sub.Cancel() + return + } } } diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index ebed7c1afc..cc08cc0cc8 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -37,16 +37,24 @@ import ( "github.com/algorand/go-algorand/test/partitiontest" pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" ) +func (n *P2PNetwork) hasPeers() bool { + n.wsPeersLock.RLock() + defer n.wsPeersLock.RUnlock() + return len(n.wsPeers) > 0 +} + func TestP2PSubmitTX(t *testing.T) { partitiontest.PartitionTest(t) cfg := config.GetDefaultLocal() + cfg.ForceFetchTransactions = true log := logging.TestingLog(t) netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, &nopeNodeInfo{}) require.NoError(t, err) @@ -66,7 +74,6 @@ func TestP2PSubmitTX(t *testing.T) { defer netB.Stop() netC, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) - require.NoError(t, err) netC.Start() defer netC.Stop() @@ -81,7 +88,13 @@ func TestP2PSubmitTX(t *testing.T) { 2*time.Second, 50*time.Millisecond, ) + require.Eventually(t, func() bool { + return netA.hasPeers() && netB.hasPeers() && netC.hasPeers() + }, 2*time.Second, 50*time.Millisecond) + + // for some reason the above check is not enough in race builds on CI time.Sleep(time.Second) // give time for peers to connect. + // now we should be connected in a line: B <-> A <-> C where both B and C are connected to A but not each other // Since we aren't using the transaction handler in this test, we need to register a pass-through handler @@ -117,6 +130,90 @@ func TestP2PSubmitTX(t *testing.T) { ) } +// TestP2PSubmitTXNoGossip tests nodes without gossip enabled cannot receive transactions +func TestP2PSubmitTXNoGossip(t *testing.T) { + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.ForceFetchTransactions = true + log := logging.TestingLog(t) + netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + netA.Start() + defer netA.Stop() + + peerInfoA := netA.service.AddrInfo() + addrsA, err := peer.AddrInfoToP2pAddrs(&peerInfoA) + require.NoError(t, err) + require.NotZero(t, addrsA[0]) + + multiAddrStr := addrsA[0].String() + phoneBookAddresses := []string{multiAddrStr} + netB, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + netB.Start() + defer netB.Stop() + + require.Eventually( + t, + func() bool { + return len(netA.service.ListPeersForTopic(p2p.TXTopicName)) == 1 && + len(netB.service.ListPeersForTopic(p2p.TXTopicName)) == 1 + }, + 2*time.Second, + 50*time.Millisecond, + ) + + // run netC in NPN mode (no relay => no gossip sup => no TX receiving) + cfg.ForceFetchTransactions = false + netC, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + netC.Start() + defer netC.Stop() + + require.Eventually(t, func() bool { + return netA.hasPeers() && netB.hasPeers() && netC.hasPeers() + }, 2*time.Second, 50*time.Millisecond) + + time.Sleep(time.Second) // give time for peers to connect. + + // ensure netC cannot receive messages + passThroughHandler := []TaggedMessageHandler{ + {Tag: protocol.TxnTag, MessageHandler: HandlerFunc(func(msg IncomingMessage) OutgoingMessage { + return OutgoingMessage{Action: Broadcast} + })}, + } + + netB.RegisterHandlers(passThroughHandler) + netC.RegisterHandlers(passThroughHandler) + for i := 0; i < 10; i++ { + err = netA.Broadcast(context.Background(), protocol.TxnTag, []byte(fmt.Sprintf("test %d", i)), false, nil) + require.NoError(t, err) + } + + // check netB received the messages + require.Eventually( + t, + func() bool { + netB.peerStatsMu.Lock() + netBpeerStatsA, ok := netB.peerStats[netA.service.ID()] + netB.peerStatsMu.Unlock() + if !ok { + return false + } + return netBpeerStatsA.txReceived.Load() == 10 + }, + 1*time.Second, + 50*time.Millisecond, + ) + + // check netB did not receive the messages + netC.peerStatsMu.Lock() + _, ok := netC.peerStats[netA.service.ID()] + netC.peerStatsMu.Unlock() + require.False(t, ok) +} + func TestP2PSubmitWS(t *testing.T) { partitiontest.PartitionTest(t) @@ -148,17 +245,12 @@ func TestP2PSubmitWS(t *testing.T) { require.NoError(t, err) defer netC.Stop() - require.Eventually( - t, - func() bool { - return len(netA.service.ListPeersForTopic(p2p.TXTopicName)) == 2 && - len(netB.service.ListPeersForTopic(p2p.TXTopicName)) == 1 && - len(netC.service.ListPeersForTopic(p2p.TXTopicName)) == 1 - }, - 2*time.Second, - 50*time.Millisecond, - ) - time.Sleep(time.Second) // XX give time for peers to connect. Knowing about them being subscribed to topics is clearly not enough + require.Eventually(t, func() bool { + return netA.hasPeers() && netB.hasPeers() && netC.hasPeers() + }, 2*time.Second, 50*time.Millisecond) + + time.Sleep(time.Second) // give time for peers to connect. + // now we should be connected in a line: B <-> A <-> C where both B and C are connected to A but not each other testTag := protocol.AgreementVoteTag @@ -242,7 +334,7 @@ func (s *mockService) ListPeersForTopic(topic string) []peer.ID { return nil } -func (s *mockService) Subscribe(topic string, val pubsub.ValidatorEx) (*pubsub.Subscription, error) { +func (s *mockService) Subscribe(topic string, val pubsub.ValidatorEx) (p2p.SubNextCancellable, error) { return nil, nil } func (s *mockService) Publish(ctx context.Context, topic string, data []byte) error { @@ -341,18 +433,27 @@ func (c *mockResolveController) Resolver() dnsaddr.Resolver { type mockResolver struct{} func (r *mockResolver) Resolve(ctx context.Context, _ ma.Multiaddr) ([]ma.Multiaddr, error) { - maddr, err := ma.NewMultiaddr("/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC") + // return random stuff each time + _, publicKey, err := crypto.GenerateKeyPair(crypto.RSA, 2048) + if err != nil { + panic(err) + } + peerID, err := peer.IDFromPublicKey(publicKey) + if err != nil { + panic(err) + } + maddr, err := ma.NewMultiaddr("/ip4/127.0.0.1/p2p/" + peerID.String()) return []ma.Multiaddr{maddr}, err } -func TestBootstrapFunc(t *testing.T) { +func TestP2PBootstrapFunc(t *testing.T) { t.Parallel() partitiontest.PartitionTest(t) b := bootstrapper{} require.Nil(t, b.BootstrapFunc()) - b.started = true + b.started.Store(true) p := peer.AddrInfo{ID: "test"} b.phonebookPeers = []*peer.AddrInfo{&p} require.Equal(t, []peer.AddrInfo{p}, b.BootstrapFunc()) @@ -363,7 +464,7 @@ func TestBootstrapFunc(t *testing.T) { b.cfg.DNSBootstrapID = ".algodev.network" b.cfg.DNSSecurityFlags = 0 b.networkID = "devnet" - b.resolveControler = &mockResolveController{} + b.resolveController = &mockResolveController{} addrs := b.BootstrapFunc() @@ -373,7 +474,7 @@ func TestBootstrapFunc(t *testing.T) { require.GreaterOrEqual(t, len(addr.Addrs), 1) } -func TestGetBootstrapPeersFailure(t *testing.T) { +func TestP2PdnsLookupBootstrapPeersFailure(t *testing.T) { t.Parallel() partitiontest.PartitionTest(t) @@ -382,12 +483,12 @@ func TestGetBootstrapPeersFailure(t *testing.T) { cfg.DNSBootstrapID = "non-existent.algodev.network" controller := nilResolveController{} - addrs := getBootstrapPeers(cfg, "test", &controller) + addrs := dnsLookupBootstrapPeers(logging.TestingLog(t), cfg, "test", &controller) require.Equal(t, 0, len(addrs)) } -func TestGetBootstrapPeersInvalidAddr(t *testing.T) { +func TestP2PdnsLookupBootstrapPeersInvalidAddr(t *testing.T) { t.Parallel() partitiontest.PartitionTest(t) @@ -396,11 +497,29 @@ func TestGetBootstrapPeersInvalidAddr(t *testing.T) { cfg.DNSBootstrapID = ".algodev.network" controller := nilResolveController{} - addrs := getBootstrapPeers(cfg, "testInvalidAddr", &controller) + addrs := dnsLookupBootstrapPeers(logging.TestingLog(t), cfg, "testInvalidAddr", &controller) require.Equal(t, 0, len(addrs)) } +func TestP2PdnsLookupBootstrapPeersWithBackup(t *testing.T) { + t.Parallel() + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.DNSSecurityFlags = 0 + cfg.DNSBootstrapID = ".algodev.network" + + controller := &mockResolveController{} + addrs := dnsLookupBootstrapPeers(logging.TestingLog(t), cfg, "test", controller) + require.GreaterOrEqual(t, len(addrs), 1) + + cfg.DNSBootstrapID = ".algodev.network?backup=.backup.algodev.network" + addrs = dnsLookupBootstrapPeers(logging.TestingLog(t), cfg, "test", controller) + require.GreaterOrEqual(t, len(addrs), 2) + +} + type capNodeInfo struct { nopeNodeInfo cap p2p.Capability @@ -427,8 +546,8 @@ func waitForRouting(t *testing.T, disc *p2p.CapabilitiesDiscovery) { } } -// TestP2PNetworkDHTCapabilities runs nodes with capabilites and ensures that connected nodes -// can discover themself. The other nodes receive the first node in bootstrap list before starting. +// TestP2PNetworkDHTCapabilities runs nodes with capabilities and ensures that connected nodes +// can discover itself. The other nodes receive the first node in bootstrap list before starting. // There is two variations of the test: only netA advertises capabilities, and all nodes advertise. func TestP2PNetworkDHTCapabilities(t *testing.T) { partitiontest.PartitionTest(t) @@ -475,16 +594,10 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { require.NoError(t, err) defer netC.Stop() - require.Eventually( - t, - func() bool { - return len(netA.service.ListPeersForTopic(p2p.TXTopicName)) > 0 && - len(netB.service.ListPeersForTopic(p2p.TXTopicName)) > 0 && - len(netC.service.ListPeersForTopic(p2p.TXTopicName)) > 0 - }, - 2*time.Second, - 50*time.Millisecond, - ) + require.Eventually(t, func() bool { + return netA.hasPeers() && netB.hasPeers() && netC.hasPeers() + }, 2*time.Second, 50*time.Millisecond) + t.Logf("peers connected") nets := []*P2PNetwork{netA, netB, netC} @@ -506,9 +619,13 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { t.Logf("DHT is ready") - // ensure all peers are connected + // ensure all peers are connected - wait for connectivity as needed for _, disc := range discs { - require.Equal(t, 2, len(disc.Host().Network().Peers())) + go func(disc *p2p.CapabilitiesDiscovery) { + require.Eventuallyf(t, func() bool { + return len(disc.Host().Network().Peers()) == 2 + }, time.Minute, time.Second, "Not all peers were found") + }(disc) } wg.Add(len(discs)) @@ -553,7 +670,7 @@ func TestP2PNetworkDHTCapabilities(t *testing.T) { } // TestMultiaddrConversionToFrom ensures Multiaddr can be serialized back to an address without losing information -func TestMultiaddrConversionToFrom(t *testing.T) { +func TestP2PMultiaddrConversionToFrom(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -645,3 +762,248 @@ func TestP2PHTTPHandler(t *testing.T) { _, err = httpClient.Get("/test") require.ErrorIs(t, err, limitcaller.ErrConnectionQueueingTimeout) } + +func TestP2PRelay(t *testing.T) { + partitiontest.PartitionTest(t) + + cfg := config.GetDefaultLocal() + cfg.ForceFetchTransactions = true + log := logging.TestingLog(t) + netA, err := NewP2PNetwork(log, cfg, "", nil, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + + err = netA.Start() + require.NoError(t, err) + defer netA.Stop() + + peerInfoA := netA.service.AddrInfo() + addrsA, err := peer.AddrInfoToP2pAddrs(&peerInfoA) + require.NoError(t, err) + require.NotZero(t, addrsA[0]) + + multiAddrStr := addrsA[0].String() + phoneBookAddresses := []string{multiAddrStr} + + netB, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + err = netB.Start() + require.NoError(t, err) + defer netB.Stop() + + require.Eventually( + t, + func() bool { + return len(netA.service.ListPeersForTopic(p2p.TXTopicName)) > 0 && + len(netB.service.ListPeersForTopic(p2p.TXTopicName)) > 0 + }, + 2*time.Second, + 50*time.Millisecond, + ) + + require.Eventually(t, func() bool { + return netA.hasPeers() && netB.hasPeers() + }, 2*time.Second, 50*time.Millisecond) + + counter := newMessageCounter(t, 1) + counterDone := counter.done + netA.RegisterHandlers([]TaggedMessageHandler{{Tag: protocol.TxnTag, MessageHandler: counter}}) + + // send 5 messages from both netB to netA + // since there is no node with listening address set => no messages should be received + for i := 0; i < 5; i++ { + err := netB.Relay(context.Background(), protocol.TxnTag, []byte{1, 2, 3, byte(i)}, true, nil) + require.NoError(t, err) + } + + select { + case <-counterDone: + require.Fail(t, "No messages should have been received") + case <-time.After(1 * time.Second): + } + + // add netC with listening address set, and enable relaying on netB + // ensure all messages are received by netA + cfg.NetAddress = "127.0.0.1:0" + netC, err := NewP2PNetwork(log, cfg, "", phoneBookAddresses, genesisID, config.Devtestnet, &nopeNodeInfo{}) + require.NoError(t, err) + err = netC.Start() + require.NoError(t, err) + defer netC.Stop() + + netB.relayMessages = true + + require.Eventually( + t, + func() bool { + return len(netA.service.ListPeersForTopic(p2p.TXTopicName)) > 0 && + len(netB.service.ListPeersForTopic(p2p.TXTopicName)) > 0 && + len(netC.service.ListPeersForTopic(p2p.TXTopicName)) > 0 + }, + 2*time.Second, + 50*time.Millisecond, + ) + + require.Eventually(t, func() bool { + return netA.hasPeers() && netB.hasPeers() && netC.hasPeers() + }, 2*time.Second, 50*time.Millisecond) + + const expectedMsgs = 10 + counter = newMessageCounter(t, expectedMsgs) + counterDone = counter.done + netA.ClearHandlers() + netA.RegisterHandlers([]TaggedMessageHandler{{Tag: protocol.TxnTag, MessageHandler: counter}}) + + for i := 0; i < expectedMsgs/2; i++ { + err := netB.Relay(context.Background(), protocol.TxnTag, []byte{1, 2, 3, byte(i)}, true, nil) + require.NoError(t, err) + err = netC.Relay(context.Background(), protocol.TxnTag, []byte{11, 12, 10 + byte(i), 14}, true, nil) + require.NoError(t, err) + } + // send some duplicate messages, they should be dropped + for i := 0; i < expectedMsgs/2; i++ { + err := netB.Relay(context.Background(), protocol.TxnTag, []byte{1, 2, 3, byte(i)}, true, nil) + require.NoError(t, err) + } + + select { + case <-counterDone: + case <-time.After(2 * time.Second): + if counter.count < expectedMsgs { + require.Failf(t, "One or more messages failed to reach destination network", "%d > %d", expectedMsgs, counter.count) + } else if counter.count > expectedMsgs { + require.Failf(t, "One or more messages that were expected to be dropped, reached destination network", "%d < %d", expectedMsgs, counter.count) + } + } +} + +type mockSubPService struct { + mockService + count atomic.Int64 +} + +type mockSubscription struct { +} + +func (m *mockSubscription) Next(ctx context.Context) (*pubsub.Message, error) { return nil, nil } +func (m *mockSubscription) Cancel() {} + +func (m *mockSubPService) Subscribe(topic string, val pubsub.ValidatorEx) (p2p.SubNextCancellable, error) { + m.count.Add(1) + return &mockSubscription{}, nil +} + +// TestP2PWantTXGossip checks txTopicHandleLoop runs as expected on wantTXGossip changes +func TestP2PWantTXGossip(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + // cancelled context to trigger subscription.Next to return + ctx, cancel := context.WithCancel(context.Background()) + cancel() + mockService := &mockSubPService{} + net := &P2PNetwork{ + service: mockService, + log: logging.TestingLog(t), + ctx: ctx, + nodeInfo: &nopeNodeInfo{}, + } + net.wantTXGossip.Store(false) + + // ensure wantTXGossip from false to false is noop + net.OnNetworkAdvance() + require.Eventually(t, func() bool { net.wg.Wait(); return true }, 1*time.Second, 50*time.Millisecond) + require.Equal(t, int64(0), mockService.count.Load()) + require.False(t, net.wantTXGossip.Load()) + + // ensure wantTXGossip from true (wantTXGossip) to false (nopeNodeInfo) is noop + net.wantTXGossip.Store(true) + net.OnNetworkAdvance() + require.Eventually(t, func() bool { net.wg.Wait(); return true }, 1*time.Second, 50*time.Millisecond) + require.Equal(t, int64(0), mockService.count.Load()) + require.False(t, net.wantTXGossip.Load()) + + // check false to true change triggers subscription + net.nodeInfo = &participatingNodeInfo{} + net.OnNetworkAdvance() + require.Eventually(t, func() bool { return mockService.count.Load() == 1 }, 1*time.Second, 50*time.Millisecond) + require.True(t, net.wantTXGossip.Load()) + + // check true to true change is noop + net.OnNetworkAdvance() + require.Eventually(t, func() bool { return mockService.count.Load() == 1 }, 1*time.Second, 50*time.Millisecond) + require.True(t, net.wantTXGossip.Load()) +} + +func TestMergeP2PAddrInfoResolvedAddresses(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + m1, err := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN") + require.NoError(t, err) + m2, err := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb") + require.NoError(t, err) + m3, err := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/4001/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC") + require.NoError(t, err) + m4, err := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/4001") + require.NoError(t, err) + + var tests = []struct { + name string + primary []ma.Multiaddr + backup []ma.Multiaddr + expected int + hasInvalid bool + }{ + {"no overlap", []ma.Multiaddr{m1}, []ma.Multiaddr{m2}, 2, false}, + {"complete overlap", []ma.Multiaddr{m1}, []ma.Multiaddr{m1}, 1, false}, + {"partial overlap", []ma.Multiaddr{m1, m2}, []ma.Multiaddr{m1, m3}, 3, false}, + {"empty slices", []ma.Multiaddr{}, []ma.Multiaddr{}, 0, false}, + {"nil slices", nil, nil, 0, false}, + {"invalid p2p", []ma.Multiaddr{m1, m4}, []ma.Multiaddr{m2, m4}, 2, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r1 := mergeP2PMultiaddrResolvedAddresses(tt.primary, tt.backup) + if len(r1) != tt.expected { + t.Errorf("Expected %d addresses, got %d", tt.expected, len(r1)) + } + + var info1 []peer.AddrInfo + var info2 []peer.AddrInfo + for _, addr := range tt.primary { + info, err0 := peer.AddrInfoFromP2pAddr(addr) + if tt.hasInvalid { + if err0 == nil { + info1 = append(info1, *info) + } + } else { + require.NoError(t, err0) + info1 = append(info1, *info) + } + } + for _, addr := range tt.backup { + info, err0 := peer.AddrInfoFromP2pAddr(addr) + if tt.hasInvalid { + if err0 == nil { + info2 = append(info2, *info) + } + } else { + require.NoError(t, err0) + info2 = append(info2, *info) + } + } + if info1 == nil && tt.primary != nil { + info1 = []peer.AddrInfo{} + } + if info2 == nil && tt.backup != nil { + info1 = []peer.AddrInfo{} + } + + r2 := mergeP2PAddrInfoResolvedAddresses(info1, info2) + if len(r2) != tt.expected { + t.Errorf("Expected %d addresses, got %d", tt.expected, len(r2)) + } + }) + } +} diff --git a/node/node.go b/node/node.go index b848a643c5..33c3ca865c 100644 --- a/node/node.go +++ b/node/node.go @@ -399,6 +399,9 @@ func (node *AlgorandFullNode) Capabilities() []p2p.Capability { if node.config.StoresCatchpoints() { caps = append(caps, p2p.Catchpoints) } + if node.config.EnableGossipService && node.config.IsGossipServer() { + caps = append(caps, p2p.Gossip) + } return caps } diff --git a/node/node_test.go b/node/node_test.go index a83ea7ab39..d4e2a08ebd 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -911,7 +911,7 @@ func TestNodeHybridTopology(t *testing.T) { t.Logf("Node%d phonebook: empty", i) return []string{} case 1: - // node 1 (R) connectes to all + // node 1 (R) connects to all t.Logf("Node%d phonebook: %s, %s, %s, %s", i, ni[0].wsNetAddr(), ni[2].wsNetAddr(), ni[0].p2pMultiAddr(), ni[2].p2pMultiAddr()) return []string{ni[0].wsNetAddr(), ni[2].wsNetAddr(), ni[0].p2pMultiAddr(), ni[2].p2pMultiAddr()} case 2: @@ -952,3 +952,104 @@ func TestNodeHybridTopology(t *testing.T) { require.Fail(t, fmt.Sprintf("no block notification for wallet: %v.", wallets[0])) } } + +// TestNodeP2PRelays creates a network of 3 nodes with the following topology: +// R1 (relay, DHT) -> R2 (relay, phonebook) <- N (part node) +// Expect N to discover R1 via DHT and connect to it. +func TestNodeP2PRelays(t *testing.T) { + partitiontest.PartitionTest(t) + + const consensusTest0 = protocol.ConsensusVersion("test0") + + configurableConsensus := make(config.ConsensusProtocols) + + testParams0 := config.Consensus[protocol.ConsensusCurrentVersion] + testParams0.AgreementFilterTimeoutPeriod0 = 500 * time.Millisecond + configurableConsensus[consensusTest0] = testParams0 + + minMoneyAtStart := 1_000_000 + maxMoneyAtStart := 100_000_000_000 + gen := rand.New(rand.NewSource(2)) + + const numAccounts = 3 + acctStake := make([]basics.MicroAlgos, numAccounts) + // only node N has stake + acctStake[2] = basics.MicroAlgos{Raw: uint64(minMoneyAtStart + (gen.Int() % (maxMoneyAtStart - minMoneyAtStart)))} + + configHook := func(ni nodeInfo, cfg config.Local) (nodeInfo, config.Local) { + cfg = config.GetDefaultLocal() + cfg.BaseLoggerDebugLevel = uint32(logging.Debug) + cfg.EnableP2P = true + cfg.NetAddress = "" + cfg.EnableDHTProviders = true + + cfg.P2PPersistPeerID = true + genesisDirs, err := cfg.EnsureAndResolveGenesisDirs(ni.rootDir, ni.genesis.ID(), nil) + require.NoError(t, err) + privKey, err := p2p.GetPrivKey(cfg, genesisDirs.RootGenesisDir) + require.NoError(t, err) + ni.p2pID, err = p2p.PeerIDFromPublicKey(privKey.GetPublic()) + require.NoError(t, err) + + switch ni.idx { + case 2: + // N is not a relay + default: + cfg.NetAddress = ni.p2pNetAddr() + } + return ni, cfg + } + + phonebookHook := func(ni []nodeInfo, i int) []string { + switch i { + case 0: + // node R1 connects to R2 + t.Logf("Node%d phonebook: %s", i, ni[1].p2pMultiAddr()) + return []string{ni[1].p2pMultiAddr()} + case 1: + // node R2 connects to none one + t.Logf("Node%d phonebook: empty", i) + return []string{} + case 2: + // node N only connects to R1 + t.Logf("Node%d phonebook: %s", i, ni[1].p2pMultiAddr()) + return []string{ni[1].p2pMultiAddr()} + default: + t.Errorf("not expected number of nodes: %d", i) + t.FailNow() + } + return nil + } + + backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) + defer backlogPool.Shutdown() + + nodes, wallets := setupFullNodesEx(t, consensusTest0, backlogPool, configurableConsensus, acctStake, configHook, phonebookHook) + require.Len(t, nodes, 3) + require.Len(t, wallets, 3) + for i := 0; i < len(nodes); i++ { + defer os.Remove(wallets[i]) + defer nodes[i].Stop() + } + + startAndConnectNodes(nodes, nodelayFirstNodeStartDelay) + + require.Eventually(t, func() bool { + connectPeers(nodes) + + // since p2p open streams based on peer ID, there is no way to judge + // connectivity based on exact In/Out so count both + return len(nodes[0].net.GetPeers(network.PeersConnectedIn, network.PeersConnectedOut)) >= 1 && + len(nodes[1].net.GetPeers(network.PeersConnectedIn, network.PeersConnectedOut)) >= 2 && + len(nodes[2].net.GetPeers(network.PeersConnectedIn, network.PeersConnectedOut)) >= 1 + }, 60*time.Second, 1*time.Second) + + t.Log("Nodes connected to R2") + + // wait until N gets R1 in its phonebook + require.Eventually(t, func() bool { + // refresh N's peers in order to learn DHT data faster + nodes[2].net.RequestConnectOutgoing(false, nil) + return len(nodes[2].net.GetPeers(network.PeersPhonebookRelays)) == 2 + }, 80*time.Second, 1*time.Second) +} From ac58d8e11e1820406a18c7b7fa2293658348e389 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Fri, 22 Mar 2024 10:16:11 -0400 Subject: [PATCH 18/38] post merge fixes --- network/p2pNetwork.go | 9 --------- network/wsNetwork_test.go | 2 +- rpcs/healthService_test.go | 9 +++++---- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index ce969e1791..d022cc7ddf 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -615,15 +615,6 @@ func (n *P2PNetwork) GetPeers(options ...PeerOption) []Peer { n.log.Debugf("Archival node(s) from DHT: %v", addrs) } } - case PeersPhonebookArchivers: - // TODO: remove after merging with master - // temporary return all nodes - n.wsPeersLock.RLock() - for _, peer := range n.wsPeers { - peers = append(peers, Peer(peer)) - } - n.wsPeersLock.RUnlock() - case PeersConnectedIn: n.wsPeersLock.RLock() for _, peer := range n.wsPeers { diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index 93fc191bf1..9e4fadcde6 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -1186,7 +1186,7 @@ func TestGetPeers(t *testing.T) { phbMulti.ReplacePeerList([]string{"a", "b", "c"}, "ph", phonebook.PhoneBookEntryRelayRole) // A few for archival node roles - phbMulti.ReplacePeerList([]string{"d", "e", "f"}, "ph", PhoneBookEntryArchivalRole) + phbMulti.ReplacePeerList([]string{"d", "e", "f"}, "ph", phonebook.PhoneBookEntryArchivalRole) //addrB, _ := netB.Address() diff --git a/rpcs/healthService_test.go b/rpcs/healthService_test.go index 9d0bb215c2..f3d8139db9 100644 --- a/rpcs/healthService_test.go +++ b/rpcs/healthService_test.go @@ -17,13 +17,14 @@ package rpcs import ( - "github.com/algorand/go-algorand/network" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" "io" "net/http" "path" "testing" + + "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) { @@ -35,7 +36,7 @@ func TestHealthService_ServeHTTP(t *testing.T) { _ = MakeHealthService(nodeA) - parsedURL, err := network.ParseHostOrURL(nodeA.rootURL()) + parsedURL, err := addr.ParseHostOrURL(nodeA.rootURL()) require.NoError(t, err) client := http.Client{} From 46b5e9971529ad6c221d76005db94765f032866a Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:15:00 -0400 Subject: [PATCH 19/38] p2p: add telemetry and DHT/libp2p metrics (#5941) --- go.mod | 7 +- go.sum | 1 + network/p2p/dht/dht.go | 11 +++ network/p2p/p2p.go | 51 +++++++++- network/p2p/p2p_test.go | 91 +++++++++++++++++ network/p2pNetwork.go | 58 +++++++++-- network/p2pNetwork_test.go | 2 +- network/requestLogger_test.go | 11 ++- network/requestTracker_test.go | 11 ++- network/wsNetwork.go | 41 +++++--- network/wsNetwork_test.go | 65 ++++++------ tools/block-generator/go.mod | 1 + tools/block-generator/go.sum | 1 + util/metrics/counter.go | 9 +- util/metrics/gauge.go | 9 +- util/metrics/opencensus.go | 170 ++++++++++++++++++++++++++++++++ util/metrics/opencensus_test.go | 135 +++++++++++++++++++++++++ util/metrics/prometheus.go | 106 ++++++++++++++++++++ util/metrics/prometheus_test.go | 130 ++++++++++++++++++++++++ util/metrics/registry.go | 6 ++ 20 files changed, 846 insertions(+), 70 deletions(-) create mode 100644 util/metrics/opencensus.go create mode 100644 util/metrics/opencensus_test.go create mode 100644 util/metrics/prometheus.go create mode 100644 util/metrics/prometheus_test.go diff --git a/go.mod b/go.mod index f81756112d..4cbb3afe8a 100644 --- a/go.mod +++ b/go.mod @@ -40,9 +40,12 @@ require ( github.com/multiformats/go-multiaddr v0.12.0 github.com/multiformats/go-multiaddr-dns v0.3.1 github.com/olivere/elastic v6.2.14+incompatible + github.com/prometheus/client_golang v1.14.0 + github.com/prometheus/client_model v0.4.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.4 + go.opencensus.io v0.24.0 golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/sync v0.4.0 @@ -83,6 +86,7 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect @@ -150,8 +154,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.89.0 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect - github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect @@ -168,7 +170,6 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/otel/trace v1.16.0 // indirect diff --git a/go.sum b/go.sum index 6d6233f618..0ddca083cb 100644 --- a/go.sum +++ b/go.sum @@ -190,6 +190,7 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= diff --git a/network/p2p/dht/dht.go b/network/p2p/dht/dht.go index 1d4fa8426a..7cc290ef3b 100644 --- a/network/p2p/dht/dht.go +++ b/network/p2p/dht/dht.go @@ -23,6 +23,7 @@ import ( "time" dht "github.com/libp2p/go-libp2p-kad-dht" + dhtmetrics "github.com/libp2p/go-libp2p-kad-dht/metrics" "github.com/libp2p/go-libp2p/core/discovery" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" @@ -30,9 +31,11 @@ import ( crouting "github.com/libp2p/go-libp2p/core/routing" "github.com/libp2p/go-libp2p/p2p/discovery/backoff" "github.com/libp2p/go-libp2p/p2p/discovery/routing" + "go.opencensus.io/stats/view" "github.com/algorand/go-algorand/config" algoproto "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/metrics" ) const minBackoff = time.Second * 5 @@ -53,6 +56,14 @@ func MakeDHT(ctx context.Context, h host.Host, networkID algoproto.NetworkID, cf dht.ProtocolPrefix(dhtProtocolPrefix(networkID)), dht.BootstrapPeersFunc(bootstrapFunc), } + + if cfg.EnableMetricReporting { + if err := view.Register(dhtmetrics.DefaultViews...); err != nil { + return nil, err + } + metrics.DefaultRegistry().Register(&metrics.OpencensusDefaultMetrics) + } + return dht.New(ctx, h, dhtCfg...) } diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index e67403a3a1..9fd64e3aad 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -18,6 +18,7 @@ package p2p import ( "context" + "encoding/base32" "fmt" "runtime" "strings" @@ -27,6 +28,7 @@ import ( "github.com/algorand/go-algorand/logging" 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" @@ -86,6 +88,10 @@ type serviceImpl struct { // AlgorandWsProtocol defines a libp2p protocol name for algorand's websockets messages const AlgorandWsProtocol = "/algorand-ws/1.0.0" +// algorandGUIDProtocolPrefix defines a libp2p protocol name for algorand node telemetry GUID exchange +const algorandGUIDProtocolPrefix = "/algorand-telemetry/1.0.0/" +const algorandGUIDProtocolTemplate = algorandGUIDProtocolPrefix + "%s/%s" + const dialTimeout = 30 * time.Second // MakeHost creates a libp2p host but does not start listening. @@ -118,6 +124,13 @@ func MakeHost(cfg config.Local, datadir string, pstore *pstore.PeerStore) (host. return nil } + var disableMetrics = func(cfg *libp2p.Config) error { return nil } + if !cfg.EnableMetricReporting { + disableMetrics = libp2p.DisableMetrics() + } else { + metrics.DefaultRegistry().Register(&metrics.PrometheusDefaultMetrics) + } + host, err := libp2p.New( libp2p.Identity(privKey), libp2p.UserAgent(ua), @@ -126,6 +139,7 @@ func MakeHost(cfg config.Local, datadir string, pstore *pstore.PeerStore) (host. libp2p.Peerstore(pstore), noListenAddrs, libp2p.Security(noise.ID, noise.New), + disableMetrics, ) return &StreamChainingHost{ Host: host, @@ -178,12 +192,18 @@ func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, h ho h.SetStreamHandler(AlgorandWsProtocol, sm.streamHandler) h.SetStreamHandler(libp2phttp.ProtocolIDForMultistreamSelect, sm.streamHandlerHTTP) + // set an empty handler for telemetryID/telemetryInstance protocol in order to allow other peers to know our telemetryID + telemetryID := log.GetTelemetryGUID() + telemetryInstance := log.GetInstanceName() + telemetryProtoInfo := formatPeerTelemetryInfoProtocolName(telemetryID, telemetryInstance) + h.SetStreamHandler(protocol.ID(telemetryProtoInfo), func(s network.Stream) { s.Close() }) + ps, err := makePubSub(ctx, cfg, h) if err != nil { return nil, err } - return &serviceImpl{ + log: log, listenAddr: listenAddr, host: h, @@ -294,3 +314,32 @@ func netAddressToListenAddress(netAddress string) (string, error) { return fmt.Sprintf("/ip4/%s/tcp/%s", ip, parts[1]), nil } + +// GetPeerTelemetryInfo returns the telemetry ID of a peer by looking at its protocols +func GetPeerTelemetryInfo(peerProtocols []protocol.ID) (telemetryID string, telemetryInstance string) { + for _, protocol := range peerProtocols { + if strings.HasPrefix(string(protocol), algorandGUIDProtocolPrefix) { + telemetryInfo := string(protocol[len(algorandGUIDProtocolPrefix):]) + telemetryInfoParts := strings.Split(telemetryInfo, "/") + if len(telemetryInfoParts) == 2 { + telemetryIDBytes, err := base32.StdEncoding.DecodeString(telemetryInfoParts[0]) + if err == nil { + telemetryID = string(telemetryIDBytes) + } + telemetryInstanceBytes, err := base32.StdEncoding.DecodeString(telemetryInfoParts[1]) + if err == nil { + telemetryInstance = string(telemetryInstanceBytes) + } + return telemetryID, telemetryInstance + } + } + } + return "", "" +} + +func formatPeerTelemetryInfoProtocolName(telemetryID string, telemetryInstance string) string { + return fmt.Sprintf(algorandGUIDProtocolTemplate, + base32.StdEncoding.EncodeToString([]byte(telemetryID)), + base32.StdEncoding.EncodeToString([]byte(telemetryInstance)), + ) +} diff --git a/network/p2p/p2p_test.go b/network/p2p/p2p_test.go index 4935238a9e..fb14193a55 100644 --- a/network/p2p/p2p_test.go +++ b/network/p2p/p2p_test.go @@ -23,8 +23,10 @@ import ( "testing" "time" + "github.com/libp2p/go-libp2p" "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/stretchr/testify/require" @@ -144,3 +146,92 @@ func TestP2PStreamingHost(t *testing.T) { }, 5*time.Second, 100*time.Millisecond) } + +// TestP2PGetPeerTelemetryInfo tests the GetPeerTelemetryInfo function +func TestP2PGetPeerTelemetryInfo(t *testing.T) { + partitiontest.PartitionTest(t) + + testCases := []struct { + name string + peerProtocols []protocol.ID + expectedTelemetryID string + expectedTelemetryInstance string + }{ + { + name: "Valid Telemetry Info", + peerProtocols: []protocol.ID{protocol.ID(formatPeerTelemetryInfoProtocolName("telemetryID", "telemetryInstance"))}, + expectedTelemetryID: "telemetryID", + expectedTelemetryInstance: "telemetryInstance", + }, + { + name: "Partial Telemetry Info 1", + peerProtocols: []protocol.ID{protocol.ID(formatPeerTelemetryInfoProtocolName("telemetryID", ""))}, + expectedTelemetryID: "telemetryID", + expectedTelemetryInstance: "", + }, + { + name: "Partial Telemetry Info 2", + peerProtocols: []protocol.ID{protocol.ID(formatPeerTelemetryInfoProtocolName("", "telemetryInstance"))}, + expectedTelemetryID: "", + expectedTelemetryInstance: "telemetryInstance", + }, + { + name: "No Telemetry Info", + peerProtocols: []protocol.ID{protocol.ID("/some-other-protocol/1.0.0/otherID/otherInstance")}, + expectedTelemetryID: "", + expectedTelemetryInstance: "", + }, + { + name: "Invalid Telemetry Info Format", + peerProtocols: []protocol.ID{protocol.ID("/algorand-telemetry/1.0.0/invalidFormat")}, + expectedTelemetryID: "", + expectedTelemetryInstance: "", + }, + { + name: "Special Characters Telemetry Info Format", + peerProtocols: []protocol.ID{protocol.ID(formatPeerTelemetryInfoProtocolName("telemetry/ID", "123-//11-33"))}, + expectedTelemetryID: "telemetry/ID", + expectedTelemetryInstance: "123-//11-33", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + telemetryID, telemetryInstance := GetPeerTelemetryInfo(tc.peerProtocols) + if telemetryID != tc.expectedTelemetryID || telemetryInstance != tc.expectedTelemetryInstance { + t.Errorf("Expected telemetry ID: %s, telemetry instance: %s, but got telemetry ID: %s, telemetry instance: %s", + tc.expectedTelemetryID, tc.expectedTelemetryInstance, telemetryID, telemetryInstance) + } + }) + } +} + +func TestP2PProtocolAsMeta(t *testing.T) { + partitiontest.PartitionTest(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + h1, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + require.NoError(t, err) + defer h1.Close() + + h1TID := "telemetryID1" + h1Inst := "telemetryInstance2" + telemetryProtoInfo := formatPeerTelemetryInfoProtocolName(h1TID, h1Inst) + h1.SetStreamHandler(protocol.ID(telemetryProtoInfo), func(s network.Stream) { s.Close() }) + + h2, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + require.NoError(t, err) + defer h2.Close() + + err = h2.Connect(ctx, peer.AddrInfo{ID: h1.ID(), Addrs: h1.Addrs()}) + require.NoError(t, err) + + protos, err := h2.Peerstore().GetProtocols(h1.ID()) + require.NoError(t, err) + + tid, inst := GetPeerTelemetryInfo(protos) + require.Equal(t, h1TID, tid) + require.Equal(t, h1Inst, inst) +} diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index d022cc7ddf..17903e3492 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -27,6 +27,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/network/limitcaller" "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/network/p2p/dnsaddr" @@ -67,6 +68,7 @@ type P2PNetwork struct { wsPeersLock deadlock.RWMutex wsPeersChangeCounter atomic.Int32 wsPeersConnectivityCheckTicker *time.Ticker + peerStater peerConnectionStater relayMessages bool // True if we should relay messages from other nodes (nominally true for relays, false otherwise) wantTXGossip atomic.Bool @@ -222,6 +224,11 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo nodeInfo: node, pstore: pstore, relayMessages: relayMessages, + peerStater: peerConnectionStater{ + log: log, + peerConnectionsUpdateInterval: time.Duration(cfg.PeerConnectionsUpdateInterval) * time.Second, + lastPeerConnectionsSent: time.Now(), + }, } net.ctx, net.ctxCancel = context.WithCancel(context.Background()) @@ -419,6 +426,11 @@ func (n *P2PNetwork) meshThread() { case <-n.ctx.Done(): return } + + // send the currently connected peers information to the + // telemetry server; that would allow the telemetry server + // to construct a cross-node map of all the nodes interconnections. + n.peerStater.sendPeerConnectionsTelemetryStatus(n) } } @@ -744,6 +756,12 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea conn: &wsPeerConnP2PImpl{stream: stream}, outgoing: !incoming, } + protos, err := n.pstore.GetProtocols(p2ppeer) + if err != nil { + n.log.Warnf("Error getting protocols for peer %s: %v", p2ppeer, err) + } + wsp.TelemetryGUID, wsp.InstanceName = p2p.GetPeerTelemetryInfo(protos) + wsp.init(n.config, outgoingMessagesBufferSize) n.wsPeersLock.Lock() n.wsPeers[p2ppeer] = wsp @@ -766,14 +784,13 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea n.log.Debugf("%s stream %s protocol %s", s.Stat().Direction.String(), s.ID(), s.Protocol()) } } - // TODO: add telemetry - // n.log.EventWithDetails(telemetryspec.Network, telemetryspec.ConnectPeerEvent, - // telemetryspec.PeerEventDetails{ - // Address: addr, - // TelemetryGUID: trackedRequest.otherTelemetryGUID, - // Incoming: true, - // InstanceName: trackedRequest.otherInstanceName, - // }) + n.log.EventWithDetails(telemetryspec.Network, telemetryspec.ConnectPeerEvent, + telemetryspec.PeerEventDetails{ + Address: addr, + TelemetryGUID: wsp.TelemetryGUID, + Incoming: incoming, + InstanceName: wsp.InstanceName, + }) } // peerRemoteClose called from wsPeer to report that it has closed @@ -784,6 +801,27 @@ func (n *P2PNetwork) peerRemoteClose(peer *wsPeer, reason disconnectReason) { delete(n.wsPeersToIDs, peer) n.wsPeersLock.Unlock() n.wsPeersChangeCounter.Add(1) + + eventDetails := telemetryspec.PeerEventDetails{ + Address: peer.GetAddress(), // p2p peers store p2p addresses + TelemetryGUID: peer.TelemetryGUID, + InstanceName: peer.InstanceName, + Incoming: !peer.outgoing, + } + if peer.outgoing { + eventDetails.Endpoint = peer.GetAddress() + eventDetails.MessageDelay = peer.peerMessageDelay + } + + n.log.EventWithDetails(telemetryspec.Network, telemetryspec.DisconnectPeerEvent, + telemetryspec.DisconnectPeerEventDetails{ + PeerEventDetails: eventDetails, + Reason: string(reason), + TXCount: peer.txMessageCount.Load(), + MICount: peer.miMessageCount.Load(), + AVCount: peer.avMessageCount.Load(), + PPCount: peer.ppMessageCount.Load(), + }) } func (n *P2PNetwork) peerSnapshot(dest []*wsPeer) ([]*wsPeer, int32) { @@ -833,7 +871,7 @@ func (n *P2PNetwork) txTopicHandleLoop() { if err != pubsub.ErrSubscriptionCancelled && err != context.Canceled { n.log.Errorf("Error reading from subscription %v, peerId %s", err, n.service.ID()) } - n.log.Debugf("Canceling subscription to topic %s due Next error", p2p.TXTopicName) + n.log.Debugf("Cancelling subscription to topic %s due Subscription.Next error: %v", p2p.TXTopicName, err) sub.Cancel() return } @@ -845,7 +883,7 @@ func (n *P2PNetwork) txTopicHandleLoop() { // participation or configuration change, cancel subscription and quit if !n.wantTXGossip.Load() { - n.log.Debugf("Canceling subscription to topic %s due participation change", p2p.TXTopicName) + n.log.Debugf("Cancelling subscription to topic %s due participation change", p2p.TXTopicName) sub.Cancel() return } diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index cc08cc0cc8..a4e7975eec 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -674,7 +674,7 @@ func TestP2PMultiaddrConversionToFrom(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - a := "/ip4/192.168.1.1/tcp/8180/p2p/Qmewz5ZHN1AAGTarRbMupNPbZRfg3p5jUGoJ3JYEatJVVk" + const a = "/ip4/192.168.1.1/tcp/8180/p2p/Qmewz5ZHN1AAGTarRbMupNPbZRfg3p5jUGoJ3JYEatJVVk" ma, err := ma.NewMultiaddr(a) require.NoError(t, err) require.Equal(t, a, ma.String()) diff --git a/network/requestLogger_test.go b/network/requestLogger_test.go index af45c6cc08..0de6a41c73 100644 --- a/network/requestLogger_test.go +++ b/network/requestLogger_test.go @@ -50,11 +50,12 @@ func TestRequestLogger(t *testing.T) { dl := eventsDetailsLogger{Logger: log, eventReceived: make(chan interface{}, 1), eventIdentifier: telemetryspec.HTTPRequestEvent} log.SetLevel(logging.Level(defaultConfig.BaseLoggerDebugLevel)) netA := &WebsocketNetwork{ - log: dl, - config: defaultConfig, - phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), - GenesisID: "go-test-network-genesis", - NetworkID: config.Devtestnet, + log: dl, + config: defaultConfig, + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), + GenesisID: "go-test-network-genesis", + NetworkID: config.Devtestnet, + peerStater: peerConnectionStater{log: log}, } netA.config.EnableRequestLogger = true netA.setup() diff --git a/network/requestTracker_test.go b/network/requestTracker_test.go index f2c098b388..158cf45336 100644 --- a/network/requestTracker_test.go +++ b/network/requestTracker_test.go @@ -87,11 +87,12 @@ func TestRateLimiting(t *testing.T) { // This test is conducted locally, so we want to treat all hosts the same for counting incoming requests. testConfig.DisableLocalhostConnectionRateLimit = false wn := &WebsocketNetwork{ - log: log, - config: testConfig, - phonebook: phonebook.MakePhonebook(1, 1), - GenesisID: "go-test-network-genesis", - NetworkID: config.Devtestnet, + log: log, + config: testConfig, + phonebook: phonebook.MakePhonebook(1, 1), + GenesisID: "go-test-network-genesis", + NetworkID: config.Devtestnet, + peerStater: peerConnectionStater{log: log}, } // increase the IncomingConnectionsLimit/MaxConnectionsPerIP limits, since we don't want to test these. diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 3d5f44495e..50307f2738 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -88,7 +88,7 @@ const httpServerMaxHeaderBytes = 4096 const connectionActivityMonitorInterval = 3 * time.Minute // maxPeerInactivityDuration is the maximum allowed duration for a -// peer to remain completly idle (i.e. no inbound or outbound communication), before +// peer to remain completely idle (i.e. no inbound or outbound communication), before // we discard the connection. const maxPeerInactivityDuration = 5 * time.Minute @@ -244,8 +244,8 @@ type WebsocketNetwork struct { requestsTracker *RequestTracker requestsLogger *RequestLogger - // lastPeerConnectionsSent is the last time the peer connections were sent ( or attempted to be sent ) to the telemetry server. - lastPeerConnectionsSent time.Time + // peerStater collects and report peers connectivity telemetry + peerStater peerConnectionStater // connPerfMonitor is used on outgoing connections to measure their relative message timing connPerfMonitor *connectionPerformanceMonitor @@ -597,7 +597,6 @@ func (wn *WebsocketNetwork) setup() { wn.upgrader.ReadBufferSize = 4096 wn.upgrader.WriteBufferSize = 4096 wn.upgrader.EnableCompression = false - wn.lastPeerConnectionsSent = time.Now() wn.router = mux.NewRouter() if wn.config.EnableGossipService { wn.router.Handle(GossipNetworkPath, wn) @@ -1614,7 +1613,7 @@ func (wn *WebsocketNetwork) meshThread() { // send the currently connected peers information to the // telemetry server; that would allow the telemetry server // to construct a cross-node map of all the nodes interconnections. - wn.sendPeerConnectionsTelemetryStatus() + wn.peerStater.sendPeerConnectionsTelemetryStatus(wn) } } @@ -1776,27 +1775,38 @@ func (wn *WebsocketNetwork) OnNetworkAdvance() { } } +type peerConnectionStater struct { + log logging.Logger + + peerConnectionsUpdateInterval time.Duration + lastPeerConnectionsSent time.Time +} + +type peerSnapshoter interface { + peerSnapshot(peers []*wsPeer) ([]*wsPeer, int32) +} + // sendPeerConnectionsTelemetryStatus sends a snapshot of the currently connected peers // to the telemetry server. Internally, it's using a timer to ensure that it would only // send the information once every hour ( configurable via PeerConnectionsUpdateInterval ) -func (wn *WebsocketNetwork) sendPeerConnectionsTelemetryStatus() { - if !wn.log.GetTelemetryEnabled() { +func (pcs *peerConnectionStater) sendPeerConnectionsTelemetryStatus(snapshoter peerSnapshoter) { + if !pcs.log.GetTelemetryEnabled() { return } now := time.Now() - if wn.lastPeerConnectionsSent.Add(time.Duration(wn.config.PeerConnectionsUpdateInterval)*time.Second).After(now) || wn.config.PeerConnectionsUpdateInterval <= 0 { + if pcs.lastPeerConnectionsSent.Add(pcs.peerConnectionsUpdateInterval).After(now) || pcs.peerConnectionsUpdateInterval <= 0 { // it's not yet time to send the update. return } - wn.lastPeerConnectionsSent = now + pcs.lastPeerConnectionsSent = now var peers []*wsPeer - peers, _ = wn.peerSnapshot(peers) - connectionDetails := wn.getPeerConnectionTelemetryDetails(now, peers) - wn.log.EventWithDetails(telemetryspec.Network, telemetryspec.PeerConnectionsEvent, connectionDetails) + peers, _ = snapshoter.peerSnapshot(peers) + connectionDetails := getPeerConnectionTelemetryDetails(now, peers) + pcs.log.EventWithDetails(telemetryspec.Network, telemetryspec.PeerConnectionsEvent, connectionDetails) } -func (wn *WebsocketNetwork) getPeerConnectionTelemetryDetails(now time.Time, peers []*wsPeer) telemetryspec.PeersConnectionDetails { +func getPeerConnectionTelemetryDetails(now time.Time, peers []*wsPeer) telemetryspec.PeersConnectionDetails { var connectionDetails telemetryspec.PeersConnectionDetails for _, peer := range peers { connDetail := telemetryspec.PeerConnectionDetails{ @@ -2305,6 +2315,11 @@ func NewWebsocketNetwork(log logging.Logger, config config.Local, phonebookAddre peerID: peerID, peerIDSigner: idSigner, resolveSRVRecords: tools_network.ReadFromSRV, + peerStater: peerConnectionStater{ + log: log, + peerConnectionsUpdateInterval: time.Duration(config.PeerConnectionsUpdateInterval) * time.Second, + lastPeerConnectionsSent: time.Now(), + }, } wn.setup() diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index 9e4fadcde6..db9be82975 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -129,11 +129,12 @@ func makeTestWebsocketNodeWithConfig(t testing.TB, conf config.Local, opts ...te log := logging.TestingLog(t) log.SetLevel(logging.Warn) wn := &WebsocketNetwork{ - log: log, - config: conf, - phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), - GenesisID: genesisID, - NetworkID: config.Devtestnet, + log: log, + config: conf, + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), + GenesisID: genesisID, + NetworkID: config.Devtestnet, + peerStater: peerConnectionStater{log: log}, } // apply options to newly-created WebsocketNetwork, if provided for _, opt := range opts { @@ -1055,11 +1056,12 @@ func makeTestFilterWebsocketNode(t *testing.T, nodename string) *WebsocketNetwor dc.OutgoingMessageFilterBucketCount = 3 dc.OutgoingMessageFilterBucketSize = 128 wn := &WebsocketNetwork{ - log: logging.TestingLog(t).With("node", nodename), - config: dc, - phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), - GenesisID: genesisID, - NetworkID: config.Devtestnet, + log: logging.TestingLog(t).With("node", nodename), + config: dc, + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), + GenesisID: genesisID, + NetworkID: config.Devtestnet, + peerStater: peerConnectionStater{log: logging.TestingLog(t).With("node", nodename)}, } require.True(t, wn.config.EnableIncomingMessageFilter) wn.setup() @@ -2476,7 +2478,7 @@ func TestWebsocketNetwork_checkServerResponseVariables(t *testing.T) { noVersionHeader := http.Header{} noVersionHeader.Set(NodeRandomHeader, wn.RandomID+"tag") noVersionHeader.Set(GenesisHeader, wn.GenesisID) - responseVariableOk, matchingVersion = wn.checkServerResponseVariables(noVersionHeader, "addressX") + responseVariableOk, _ = wn.checkServerResponseVariables(noVersionHeader, "addressX") require.Equal(t, false, responseVariableOk) noRandomHeader := http.Header{} @@ -2565,11 +2567,12 @@ func TestSlowPeerDisconnection(t *testing.T) { log := logging.TestingLog(t) log.SetLevel(logging.Info) wn := &WebsocketNetwork{ - log: log, - config: defaultConfig, - phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), - GenesisID: genesisID, - NetworkID: config.Devtestnet, + log: log, + config: defaultConfig, + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), + GenesisID: genesisID, + NetworkID: config.Devtestnet, + peerStater: peerConnectionStater{log: log}, } wn.setup() wn.broadcaster.slowWritingPeerMonitorInterval = time.Millisecond * 50 @@ -2640,11 +2643,12 @@ func TestForceMessageRelaying(t *testing.T) { log := logging.TestingLog(t) log.SetLevel(logging.Level(defaultConfig.BaseLoggerDebugLevel)) wn := &WebsocketNetwork{ - log: log, - config: defaultConfig, - phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), - GenesisID: genesisID, - NetworkID: config.Devtestnet, + log: log, + config: defaultConfig, + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), + GenesisID: genesisID, + NetworkID: config.Devtestnet, + peerStater: peerConnectionStater{log: log}, } wn.setup() wn.eventualReadyDelay = time.Second @@ -2734,11 +2738,12 @@ func TestCheckProtocolVersionMatch(t *testing.T) { log := logging.TestingLog(t) log.SetLevel(logging.Level(defaultConfig.BaseLoggerDebugLevel)) wn := &WebsocketNetwork{ - log: log, - config: defaultConfig, - phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), - GenesisID: genesisID, - NetworkID: config.Devtestnet, + log: log, + config: defaultConfig, + phonebook: phonebook.MakePhonebook(1, 1*time.Millisecond), + GenesisID: genesisID, + NetworkID: config.Devtestnet, + peerStater: peerConnectionStater{log: log}, } wn.setup() wn.supportedProtocolVersions = []string{"2", "1"} @@ -3778,9 +3783,9 @@ func TestWebsocketNetworkTelemetryTCP(t *testing.T) { // get RTT from both ends and assert nonzero var peersA, peersB []*wsPeer peersA, _ = netA.peerSnapshot(peersA) - detailsA := netA.getPeerConnectionTelemetryDetails(time.Now(), peersA) + detailsA := getPeerConnectionTelemetryDetails(time.Now(), peersA) peersB, _ = netB.peerSnapshot(peersB) - detailsB := netB.getPeerConnectionTelemetryDetails(time.Now(), peersB) + detailsB := getPeerConnectionTelemetryDetails(time.Now(), peersB) require.Len(t, detailsA.IncomingPeers, 1) assert.NotZero(t, detailsA.IncomingPeers[0].TCP.RTT) require.Len(t, detailsB.OutgoingPeers, 1) @@ -3801,8 +3806,8 @@ func TestWebsocketNetworkTelemetryTCP(t *testing.T) { defer closeFunc2() // use stale peers snapshot from closed networks to get telemetry // *net.OpError "use of closed network connection" err results in 0 rtt values - detailsA = netA.getPeerConnectionTelemetryDetails(time.Now(), peersA) - detailsB = netB.getPeerConnectionTelemetryDetails(time.Now(), peersB) + detailsA = getPeerConnectionTelemetryDetails(time.Now(), peersA) + detailsB = getPeerConnectionTelemetryDetails(time.Now(), peersB) require.Len(t, detailsA.IncomingPeers, 1) assert.Zero(t, detailsA.IncomingPeers[0].TCP.RTT) require.Len(t, detailsB.OutgoingPeers, 1) diff --git a/tools/block-generator/go.mod b/tools/block-generator/go.mod index 0ef60b4bdd..03efe41222 100644 --- a/tools/block-generator/go.mod +++ b/tools/block-generator/go.mod @@ -51,6 +51,7 @@ require ( github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/gopacket v1.1.19 // indirect diff --git a/tools/block-generator/go.sum b/tools/block-generator/go.sum index dc7ca7e0f7..336ba11052 100644 --- a/tools/block-generator/go.sum +++ b/tools/block-generator/go.sum @@ -166,6 +166,7 @@ github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= diff --git a/util/metrics/counter.go b/util/metrics/counter.go index 38852386d8..e9b437a4a2 100644 --- a/util/metrics/counter.go +++ b/util/metrics/counter.go @@ -28,6 +28,14 @@ type Counter struct { // MakeCounter create a new counter with the provided name and description. func MakeCounter(metric MetricName) *Counter { + c := makeCounter(metric) + c.Register(nil) + return c +} + +// makeCounter create a new counter with the provided name and description +// but does not register it with the default registry. +func makeCounter(metric MetricName) *Counter { c := &Counter{c: couge{ values: make([]*cougeValues, 0), description: metric.Description, @@ -35,7 +43,6 @@ func MakeCounter(metric MetricName) *Counter { labels: make(map[string]int), valuesIndices: make(map[int]int), }} - c.Register(nil) return c } diff --git a/util/metrics/gauge.go b/util/metrics/gauge.go index bbc143a14f..edf144e48f 100644 --- a/util/metrics/gauge.go +++ b/util/metrics/gauge.go @@ -27,6 +27,14 @@ type Gauge struct { // MakeGauge create a new gauge with the provided name and description. func MakeGauge(metric MetricName) *Gauge { + c := makeGauge(metric) + c.Register(nil) + return c +} + +// makeGauge create a new gauge with the provided name and description +// but does not register it with the default registry. +func makeGauge(metric MetricName) *Gauge { c := &Gauge{g: couge{ values: make([]*cougeValues, 0), description: metric.Description, @@ -34,7 +42,6 @@ func MakeGauge(metric MetricName) *Gauge { labels: make(map[string]int), valuesIndices: make(map[int]int), }} - c.Register(nil) return c } diff --git a/util/metrics/opencensus.go b/util/metrics/opencensus.go new file mode 100644 index 0000000000..6d38436e6a --- /dev/null +++ b/util/metrics/opencensus.go @@ -0,0 +1,170 @@ +// Copyright (C) 2019-2024 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 . + +// Functions for opencensus stats aggs conversion to our internal data type +// suitable for further reporting + +package metrics + +import ( + "context" + "strings" + + "go.opencensus.io/metric/metricdata" + "go.opencensus.io/metric/metricexport" + "golang.org/x/exp/slices" +) + +type defaultOpencensusGatherer struct { + names []string +} + +// WriteMetric return opencensus data converted to algorand format +func (og *defaultOpencensusGatherer) WriteMetric(buf *strings.Builder, parentLabels string) { + metrics := collectOpenCensusMetrics(og.names) + for _, metric := range metrics { + metric.WriteMetric(buf, parentLabels) + } +} + +// AddMetric return opencensus data converted to algorand format +func (og *defaultOpencensusGatherer) AddMetric(values map[string]float64) { + metrics := collectOpenCensusMetrics(og.names) + for _, metric := range metrics { + metric.AddMetric(values) + } +} + +type statExporter struct { + names map[string]struct{} + metrics []Metric +} + +func collectOpenCensusMetrics(names []string) []Metric { + exporter := &statExporter{} + if len(names) > 0 { + exporter.names = make(map[string]struct{}, len(names)) + for _, name := range names { + exporter.names[name] = struct{}{} + } + } + reader := metricexport.NewReader() + reader.ReadAndExport(exporter) + + return exporter.metrics +} + +// statCounter stores single int64 value per stat with labels +type statCounter struct { + name string + description string + labels []map[string]string + values []int64 +} + +// WriteMetric outputs Prometheus metrics for all labels/values in statCounter +func (st *statCounter) WriteMetric(buf *strings.Builder, parentLabels string) { + counter := makeCounter(MetricName{st.name, st.description}) + for i := 0; i < len(st.labels); i++ { + counter.AddUint64(uint64(st.values[i]), st.labels[i]) + } + counter.WriteMetric(buf, parentLabels) +} + +// AddMetric outputs all statCounter's labels/values into a map +func (st *statCounter) AddMetric(values map[string]float64) { + counter := makeCounter(MetricName{st.name, st.description}) + for i := 0; i < len(st.labels); i++ { + counter.AddUint64(uint64(st.values[i]), st.labels[i]) + } + counter.AddMetric(values) +} + +// statCounter stores single float64 sun value per stat with labels +type statDistribution struct { + name string + description string + labels []map[string]string + values []float64 +} + +// WriteMetric outputs Prometheus metrics for all labels/values in statCounter +func (st *statDistribution) WriteMetric(buf *strings.Builder, parentLabels string) { + gauge := makeGauge(MetricName{st.name, st.description}) + for i := 0; i < len(st.labels); i++ { + gauge.SetLabels(uint64(st.values[i]), st.labels[i]) + } + gauge.WriteMetric(buf, parentLabels) +} + +// AddMetric outputs all statCounter's labels/values into a map +func (st *statDistribution) AddMetric(values map[string]float64) { + gauge := makeGauge(MetricName{st.name, st.description}) + for i := 0; i < len(st.labels); i++ { + gauge.SetLabels(uint64(st.values[i]), st.labels[i]) + } + gauge.AddMetric(values) +} + +func (s *statExporter) ExportMetrics(ctx context.Context, data []*metricdata.Metric) error { + labeler := func(lk []metricdata.LabelKey, lv []metricdata.LabelValue, ignores ...string) map[string]string { + // default labeler concatenates labels + labels := make(map[string]string, len(lk)) + for i := range lk { + if lv[i].Present && (len(ignores) == 0 || len(ignores) > 0 && !slices.Contains(ignores, lk[i].Key)) { + labels[lk[i].Key] = lv[i].Value + } + } + return labels + } + + for _, m := range data { + if _, ok := s.names[m.Descriptor.Name]; len(s.names) > 0 && !ok { + continue + } + if m.Descriptor.Type == metricdata.TypeCumulativeInt64 { + counter := statCounter{ + name: m.Descriptor.Name, + description: m.Descriptor.Description, + } + for _, d := range m.TimeSeries { + // ignore a known useless instance_id label + labels := labeler(m.Descriptor.LabelKeys, d.LabelValues, "instance_id") + counter.labels = append(counter.labels, labels) + counter.values = append(counter.values, d.Points[0].Value.(int64)) + } + + s.metrics = append(s.metrics, &counter) + } else if m.Descriptor.Type == metricdata.TypeCumulativeDistribution { + // TODO: the metrics below cannot be integer gauge, and Sum statistic does not make any sense. + // libp2p.io/dht/kad/outbound_request_latency + // libp2p.io/dht/kad/inbound_request_latency + // Ignore? + dist := statDistribution{ + name: m.Descriptor.Name, + description: m.Descriptor.Description, + } + // check if we are processing a known DHT metric + for _, d := range m.TimeSeries { + label := labeler(m.Descriptor.LabelKeys, d.LabelValues, "instance_id") + dist.labels = append(dist.labels, label) + dist.values = append(dist.values, d.Points[0].Value.(*metricdata.Distribution).Sum) + } + s.metrics = append(s.metrics, &dist) + } + } + return nil +} diff --git a/util/metrics/opencensus_test.go b/util/metrics/opencensus_test.go new file mode 100644 index 0000000000..7a20d75334 --- /dev/null +++ b/util/metrics/opencensus_test.go @@ -0,0 +1,135 @@ +// Copyright (C) 2019-2024 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 metrics + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "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 +func TestDHTOpenCensusMetrics(t *testing.T) { + partitiontest.PartitionTest(t) + + defaultBytesDistribution := view.Distribution(1024, 2048, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824, 4294967296) + + keyMessageType, _ := tag.NewKey("message_type") + keyPeerID, _ := tag.NewKey("peer_id") + keyInstanceID, _ := tag.NewKey("instance_id") + + sentMessages := stats.Int64("my_sent_messages", "Total number of messages sent per RPC", stats.UnitDimensionless) + receivedBytes := stats.Int64("my_received_bytes", "Total received bytes per RPC", stats.UnitBytes) + + receivedBytesView := &view.View{ + Measure: receivedBytes, + TagKeys: []tag.Key{keyMessageType, keyPeerID, keyInstanceID}, + Aggregation: defaultBytesDistribution, + } + sentMessagesView := &view.View{ + Measure: sentMessages, + TagKeys: []tag.Key{keyMessageType, keyPeerID, keyInstanceID}, + Aggregation: view.Count(), + } + + err := view.Register(receivedBytesView, sentMessagesView) + require.NoError(t, err) + + ctx := context.Background() + tags1 := []tag.Mutator{ + tag.Upsert(keyMessageType, "UNKNOWN"), + tag.Upsert(keyPeerID, "1234"), + tag.Upsert(keyInstanceID, fmt.Sprintf("%p", t)), + } + ctx1, _ := tag.New(ctx, tags1...) + + stats.Record(ctx1, + sentMessages.M(1), + receivedBytes.M(int64(100)), + ) + + tags2 := []tag.Mutator{ + tag.Upsert(keyMessageType, "ADD_PROVIDER"), + tag.Upsert(keyPeerID, "abcd"), + tag.Upsert(keyInstanceID, fmt.Sprintf("%p", t)), + } + ctx2, _ := tag.New(ctx, tags2...) + + stats.Record(ctx2, + sentMessages.M(1), + receivedBytes.M(int64(123)), + ) + + // first check some metrics are collected when no names provided + // cannot assert on specific values because network tests might run in parallel with this package + // and produce some metric under specific configuration + require.Eventually(t, func() bool { + // stats are written by a background goroutine, give it a chance to finish + metrics := collectOpenCensusMetrics(nil) + return len(metrics) >= 2 + }, 10*time.Second, 20*time.Millisecond) + + // now assert on specific names and values + metrics := collectOpenCensusMetrics([]string{"my_sent_messages", "my_received_bytes"}) + require.Len(t, metrics, 2) + for _, m := range metrics { + var buf strings.Builder + m.WriteMetric(&buf, "") + promValue := buf.String() + if strings.Contains(promValue, "my_sent_messages") { + require.Contains(t, promValue, "my_sent_messages counter\n") + require.Contains(t, promValue, `peer_id="abcd"`) + require.Contains(t, promValue, `peer_id="1234"`) + require.Contains(t, promValue, `message_type="ADD_PROVIDER"`) + require.Contains(t, promValue, `message_type="UNKNOWN"`) + require.Contains(t, promValue, "} 1\n") + } else if strings.Contains(promValue, "my_received_bytes") { + require.Contains(t, promValue, "my_received_bytes gauge\n") + require.Contains(t, promValue, `peer_id="1234"`) + require.Contains(t, promValue, `peer_id="abcd"`) + require.Contains(t, promValue, `message_type="ADD_PROVIDER"`) + require.Contains(t, promValue, `message_type="UNKNOWN"`) + require.Contains(t, promValue, "} 123\n") + require.Contains(t, promValue, "} 100\n") + } else { + require.Fail(t, "not expected metric", promValue) + } + + values := make(map[string]float64) + m.AddMetric(values) + for k, v := range values { + require.True(t, strings.Contains(k, "message_type__ADD_PROVIDER") || strings.Contains(k, "message_type__UNKNOWN")) + require.True(t, strings.Contains(k, "peer_id__1234") || strings.Contains(k, "peer_id__abcd")) + if strings.Contains(k, "my_sent_messages") { + require.Equal(t, v, float64(1)) + } else if strings.Contains(k, "my_received_bytes") { + require.True(t, v == 100 || v == 123) + } else { + require.Fail(t, "not expected metric key", k) + } + } + } +} diff --git a/util/metrics/prometheus.go b/util/metrics/prometheus.go new file mode 100644 index 0000000000..edce668b93 --- /dev/null +++ b/util/metrics/prometheus.go @@ -0,0 +1,106 @@ +// Copyright (C) 2019-2024 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 . + +// Functions for Prometheus metrics conversion to our internal data type +// suitable for further reporting + +package metrics + +import ( + "strings" + + "github.com/prometheus/client_golang/prometheus" + iopc "github.com/prometheus/client_model/go" +) + +type defaultPrometheusGatherer struct { + names []string +} + +// WriteMetric return prometheus converted to algorand format. +// Supports only counter and gauge types and ignores go_ metrics. +func (pg *defaultPrometheusGatherer) WriteMetric(buf *strings.Builder, parentLabels string) { + metrics := collectOpenCensusMetrics(pg.names) + for _, metric := range metrics { + metric.WriteMetric(buf, parentLabels) + } +} + +// AddMetric return prometheus data converted to algorand format. +// Supports only counter and gauge types and ignores go_ metrics. +func (pg *defaultPrometheusGatherer) AddMetric(values map[string]float64) { + metrics := collectPrometheusMetrics(pg.names) + for _, metric := range metrics { + metric.AddMetric(values) + } +} + +func collectPrometheusMetrics(names []string) []Metric { + var result []Metric + var namesMap map[string]struct{} + if len(names) > 0 { + namesMap = make(map[string]struct{}, len(names)) + for _, name := range names { + namesMap[name] = struct{}{} + } + } + + convertLabels := func(m *iopc.Metric) map[string]string { + var labels map[string]string + if lbls := m.GetLabel(); len(lbls) > 0 { + labels = make(map[string]string, len(lbls)) + for _, lbl := range lbls { + labels[lbl.GetName()] = lbl.GetValue() + } + } + return labels + } + metrics, _ := prometheus.DefaultGatherer.Gather() + for _, metric := range metrics { + if strings.HasPrefix(metric.GetName(), "go_") { + continue + } + if _, ok := namesMap[metric.GetName()]; len(namesMap) > 0 && ok || len(namesMap) == 0 { + if metric.GetType() == iopc.MetricType_COUNTER && metric.GetMetric() != nil { + counter := makeCounter(MetricName{metric.GetName(), metric.GetHelp()}) + ma := metric.GetMetric() + for _, m := range ma { + if m.GetCounter() == nil { + continue + } + val := uint64(m.GetCounter().GetValue()) + labels := convertLabels(m) + counter.AddUint64(val, labels) + } + result = append(result, counter) + } else if metric.GetType() == iopc.MetricType_GAUGE && metric.GetMetric() != nil { + gauge := makeGauge(MetricName{metric.GetName(), metric.GetHelp()}) + + ma := metric.GetMetric() + for _, m := range ma { + if m.GetGauge() == nil { + continue + } + val := uint64(m.GetGauge().GetValue()) + labels := convertLabels(m) + gauge.SetLabels(val, labels) + } + result = append(result, gauge) + } + } + } + return result +} diff --git a/util/metrics/prometheus_test.go b/util/metrics/prometheus_test.go new file mode 100644 index 0000000000..18313ee220 --- /dev/null +++ b/util/metrics/prometheus_test.go @@ -0,0 +1,130 @@ +// Copyright (C) 2019-2024 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 . + +// Functions for Prometheus metrics conversion to our internal data type +// suitable for further reporting + +package metrics + +import ( + "strings" + "testing" + "time" + + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" +) + +func TestPrometheusMetrics(t *testing.T) { + partitiontest.PartitionTest(t) + + const metricNamespace = "test_metric" + + // gauge vec with labels + gaugeLabels := prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: metricNamespace, + Name: "streams", + Help: "Number of Streams", + }, []string{"dir", "scope", "protocol"}) + + // gauge without labels + gauge := prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: metricNamespace, + Name: "protocols_count", + Help: "Protocols Count", + }, + ) + + // counter with labels + counterLabels := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: metricNamespace, + Name: "identify_total", + Help: "Identify", + }, + []string{"dir"}, + ) + + // counter without labels + counter := prometheus.NewCounter( + prometheus.CounterOpts{ + Namespace: metricNamespace, + Name: "counter_total", + Help: "Counter", + }, + ) + + prometheus.DefaultRegisterer.MustRegister(gaugeLabels) + prometheus.DefaultRegisterer.MustRegister(gauge) + prometheus.DefaultRegisterer.MustRegister(counterLabels) + prometheus.DefaultRegisterer.MustRegister(counter) + + // set some values + tags := []string{"outbound", "protocol", "/test/proto"} + gaugeLabels.WithLabelValues(tags...).Set(float64(1)) + + gauge.Set(float64(2)) + + tags = []string{"inbound"} + counterLabels.WithLabelValues(tags...).Add(float64(3)) + + counter.Add(float64(4)) + + // wait they collected and ready for gathering + require.Eventually(t, func() bool { + metrics := collectPrometheusMetrics(nil) + return len(metrics) >= 4 + }, 5*time.Second, 100*time.Millisecond) + + metrics := collectPrometheusMetrics([]string{ + metricNamespace + "_streams", + metricNamespace + "_protocols_count", + metricNamespace + "_identify_total", + metricNamespace + "_counter_total"}) + require.Len(t, metrics, 4) + + for _, m := range metrics { + buf := strings.Builder{} + m.WriteMetric(&buf, "") + promValue := buf.String() + if strings.Contains(promValue, metricNamespace+"_streams") { + require.Contains(t, promValue, metricNamespace+"_streams gauge\n") + require.Contains(t, promValue, metricNamespace+"_streams{") + // map/labels order is not guaranteed + require.Contains(t, promValue, "dir=\"outbound\"") + require.Contains(t, promValue, "protocol=\"/test/proto\"") + require.Contains(t, promValue, "scope=\"protocol\"") + require.Contains(t, promValue, "} 1\n") + } else if strings.Contains(promValue, metricNamespace+"_protocols_count") { + require.Contains(t, promValue, metricNamespace+"_protocols_count gauge\n") + require.Contains(t, promValue, metricNamespace+"_protocols_count 2\n") + } else if strings.Contains(promValue, metricNamespace+"_identify_total") { + require.Contains(t, promValue, metricNamespace+"_identify_total counter\n") + require.Contains(t, promValue, metricNamespace+"_identify_total{dir=\"inbound\"} 3\n") + } else if strings.Contains(promValue, metricNamespace+"_counter_total") { + require.Contains(t, promValue, metricNamespace+"_counter_total counter\n") + require.Contains(t, promValue, metricNamespace+"_counter_total 4\n") + } else { + require.Fail(t, "not expected metric", promValue) + } + + values := make(map[string]float64) + m.AddMetric(values) + require.Len(t, values, 1) + } +} diff --git a/util/metrics/registry.go b/util/metrics/registry.go index 43078bb4c2..d525bc1833 100644 --- a/util/metrics/registry.go +++ b/util/metrics/registry.go @@ -37,6 +37,12 @@ func DefaultRegistry() *Registry { return defaultRegistry } +// PrometheusDefaultMetrics is the default prometheus gatherer implementing the Metric interface +var PrometheusDefaultMetrics = defaultPrometheusGatherer{} + +// OpencensusDefaultMetrics is the default prometheus gatherer implementing the Metric interface +var OpencensusDefaultMetrics = defaultOpencensusGatherer{} + func init() { defaultRegistry = MakeRegistry() } From d38f35c6c2b6ac50ddc2670807f9db24c0b59de2 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:27:52 -0400 Subject: [PATCH 20/38] p2p: fix wantTXGossip modifications (#5982) --- network/p2pNetwork.go | 11 +++++++++- network/p2pNetwork_test.go | 42 ++++++++++++++++++++++++++++++++++++-- network/wsNetwork_test.go | 5 +---- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 17903e3492..dd830ac5d7 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -197,6 +197,15 @@ type gossipSubPeer struct { func (p gossipSubPeer) GetNetwork() GossipNode { return p.net } +func (p gossipSubPeer) OnClose(f func()) { + net := p.GetNetwork().(*P2PNetwork) + net.wsPeersLock.Lock() + defer net.wsPeersLock.Unlock() + if wsp, ok := net.wsPeers[p.peerID]; ok { + wsp.OnClose(f) + } +} + // NewP2PNetwork returns an instance of GossipNode that uses the p2p.Service func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID, node NodeInfo) (*P2PNetwork, error) { const readBufferLen = 2048 @@ -668,7 +677,7 @@ func (n *P2PNetwork) GetHTTPClient(address string) (*http.Client, error) { func (n *P2PNetwork) OnNetworkAdvance() { if n.nodeInfo != nil { old := n.wantTXGossip.Load() - new := n.nodeInfo.IsParticipating() + new := n.relayMessages || n.config.ForceFetchTransactions || n.nodeInfo.IsParticipating() if old != new { n.wantTXGossip.Store(new) if new { diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index a4e7975eec..4e929723f6 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -907,9 +907,9 @@ func TestP2PWantTXGossip(t *testing.T) { ctx: ctx, nodeInfo: &nopeNodeInfo{}, } - net.wantTXGossip.Store(false) // ensure wantTXGossip from false to false is noop + net.wantTXGossip.Store(false) net.OnNetworkAdvance() require.Eventually(t, func() bool { net.wg.Wait(); return true }, 1*time.Second, 50*time.Millisecond) require.Equal(t, int64(0), mockService.count.Load()) @@ -923,14 +923,52 @@ func TestP2PWantTXGossip(t *testing.T) { require.False(t, net.wantTXGossip.Load()) // check false to true change triggers subscription + net.wantTXGossip.Store(false) net.nodeInfo = &participatingNodeInfo{} net.OnNetworkAdvance() require.Eventually(t, func() bool { return mockService.count.Load() == 1 }, 1*time.Second, 50*time.Millisecond) require.True(t, net.wantTXGossip.Load()) + // check IsParticipating changes wantTXGossip + net.wantTXGossip.Store(true) + net.nodeInfo = &nopeNodeInfo{} + net.config.ForceFetchTransactions = false + net.relayMessages = false + net.OnNetworkAdvance() + require.Eventually(t, func() bool { net.wg.Wait(); return true }, 1*time.Second, 50*time.Millisecond) + require.False(t, net.wantTXGossip.Load()) + + // check ForceFetchTransactions and relayMessages also take effect + net.wantTXGossip.Store(false) + net.nodeInfo = &nopeNodeInfo{} + net.config.ForceFetchTransactions = true + net.relayMessages = false + net.OnNetworkAdvance() + require.Eventually(t, func() bool { return mockService.count.Load() == 2 }, 1*time.Second, 50*time.Millisecond) + require.True(t, net.wantTXGossip.Load()) + + net.wantTXGossip.Store(false) + net.nodeInfo = &nopeNodeInfo{} + net.config.ForceFetchTransactions = false + net.relayMessages = true + net.OnNetworkAdvance() + require.Eventually(t, func() bool { return mockService.count.Load() == 3 }, 1*time.Second, 50*time.Millisecond) + require.True(t, net.wantTXGossip.Load()) + + // ensure empty nodeInfo prevents changing the value + net.wantTXGossip.Store(false) + net.nodeInfo = nil + net.config.ForceFetchTransactions = true + net.relayMessages = true + net.OnNetworkAdvance() + require.Eventually(t, func() bool { net.wg.Wait(); return true }, 1*time.Second, 50*time.Millisecond) + require.False(t, net.wantTXGossip.Load()) + // check true to true change is noop + net.wantTXGossip.Store(true) + net.nodeInfo = &participatingNodeInfo{} net.OnNetworkAdvance() - require.Eventually(t, func() bool { return mockService.count.Load() == 1 }, 1*time.Second, 50*time.Millisecond) + require.Eventually(t, func() bool { return mockService.count.Load() == 3 }, 1*time.Second, 50*time.Millisecond) require.True(t, net.wantTXGossip.Load()) } diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go index db9be82975..038a9d6e2d 100644 --- a/network/wsNetwork_test.go +++ b/network/wsNetwork_test.go @@ -40,7 +40,6 @@ import ( "time" "github.com/algorand/go-algorand/internal/rapidgen" - "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/network/phonebook" "pgregory.net/rapid" @@ -3293,14 +3292,12 @@ func TestWebsocketNetworkTXMessageOfInterestNPN(t *testing.T) { } type participatingNodeInfo struct { + nopeNodeInfo } func (nnni *participatingNodeInfo) IsParticipating() bool { return true } -func (nnni *participatingNodeInfo) Capabilities() []p2p.Capability { - return nil -} func TestWebsocketNetworkTXMessageOfInterestPN(t *testing.T) { // Tests that A->B follows MOI From 411837f27fc5c44ba4972ebe802a737b08b6a609 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Fri, 10 May 2024 16:07:43 -0400 Subject: [PATCH 21/38] p2p: refactor gossipsub to validation and handling (#5976) * Added MessageProcessor similar to MessageHandler but working in two stages: validate and handle * Split processIncomingTxn txhandler method into two validate + handle * This allowed to use gossipsub's validate and handing framework as intended. --- components/mocks/mockNetwork.go | 8 ++ data/txHandler.go | 173 +++++++++++++++++++++++++++----- network/gossipNode.go | 53 +++++++++- network/hybridNetwork.go | 12 +++ network/multiplexer.go | 106 +++++++++++++++---- network/p2pNetwork.go | 24 +++-- network/p2pNetwork_test.go | 121 ++++++++++++++++------ network/wsNetwork.go | 8 ++ 8 files changed, 425 insertions(+), 80 deletions(-) diff --git a/components/mocks/mockNetwork.go b/components/mocks/mockNetwork.go index 7e1ab29126..c0b7724e07 100644 --- a/components/mocks/mockNetwork.go +++ b/components/mocks/mockNetwork.go @@ -91,6 +91,14 @@ func (network *MockNetwork) RegisterHandlers(dispatch []network.TaggedMessageHan func (network *MockNetwork) ClearHandlers() { } +// RegisterProcessors - empty implementation. +func (network *MockNetwork) RegisterProcessors(dispatch []network.TaggedMessageProcessor) { +} + +// ClearProcessors - empty implementation +func (network *MockNetwork) ClearProcessors() { +} + // RegisterHTTPHandler - empty implementation func (network *MockNetwork) RegisterHTTPHandler(path string, handler http.Handler) { } diff --git a/data/txHandler.go b/data/txHandler.go index 3d20e95acd..895b07269d 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -243,6 +243,21 @@ func (handler *TxHandler) Start() { handler.net.RegisterHandlers([]network.TaggedMessageHandler{ {Tag: protocol.TxnTag, MessageHandler: network.HandlerFunc(handler.processIncomingTxn)}, }) + + handler.net.RegisterProcessors([]network.TaggedMessageProcessor{ + { + Tag: protocol.TxnTag, + // create anonymous struct to hold the two functions and satisfy the network.MessageProcessor interface + MessageProcessor: struct { + network.ProcessorValidateFunc + network.ProcessorHandleFunc + }{ + network.ProcessorValidateFunc(handler.validateIncomingTxMessage), + network.ProcessorHandleFunc(handler.processIncomingTxMessage), + }, + }, + }) + handler.backlogWg.Add(2) go handler.backlogWorker() go handler.backlogGaugeThread() @@ -530,7 +545,7 @@ func (handler *TxHandler) deleteFromCaches(msgKey *crypto.Digest, canonicalKey * // dedupCanonical checks if the transaction group has been seen before after reencoding to canonical representation. // returns a key used for insertion if the group was not found. -func (handler *TxHandler) dedupCanonical(ntx int, unverifiedTxGroup []transactions.SignedTxn, consumed int) (key *crypto.Digest, isDup bool) { +func (handler *TxHandler) dedupCanonical(unverifiedTxGroup []transactions.SignedTxn, consumed int) (key *crypto.Digest, isDup bool) { // consider situations where someone want to censor transactions A // 1. Txn A is not part of a group => txn A with a valid signature is OK // Censorship attempts are: @@ -547,6 +562,7 @@ func (handler *TxHandler) dedupCanonical(ntx int, unverifiedTxGroup []transactio // - using individual txn from a group: {A, Z} could be poisoned by {A, B}, where B is invalid var d crypto.Digest + ntx := len(unverifiedTxGroup) if ntx == 1 { // a single transaction => cache/dedup canonical txn with its signature enc := unverifiedTxGroup[0].MarshalMsg(nil) @@ -574,49 +590,52 @@ func (handler *TxHandler) dedupCanonical(ntx int, unverifiedTxGroup []transactio return &d, false } -// processIncomingTxn decodes a transaction group from incoming message and enqueues into the back log for processing. -// The function also performs some input data pre-validation; -// - txn groups are cut to MaxTxGroupSize size -// - message are checked for duplicates -// - transactions are checked for duplicates - -func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) network.OutgoingMessage { +// incomingMsgDupErlCheck runs the duplicate and rate limiting checks on a raw incoming messages. +// Returns: +// - the key used for insertion if the message was not found in the cache +// - the capacity guard returned by the elastic rate limiter +// - a boolean indicating if the message was a duplicate or the sender is rate limited +func (handler *TxHandler) incomingMsgDupErlCheck(data []byte, sender network.DisconnectablePeer) (*crypto.Digest, *util.ErlCapacityGuard, bool) { var msgKey *crypto.Digest + var capguard *util.ErlCapacityGuard var isDup bool if handler.msgCache != nil { // check for duplicate messages // this helps against relaying duplicates - if msgKey, isDup = handler.msgCache.CheckAndPut(rawmsg.Data); isDup { + if msgKey, isDup = handler.msgCache.CheckAndPut(data); isDup { transactionMessagesDupRawMsg.Inc(nil) - return network.OutgoingMessage{Action: network.Ignore} + return msgKey, capguard, true } } - unverifiedTxGroup := make([]transactions.SignedTxn, 1) - dec := protocol.NewMsgpDecoderBytes(rawmsg.Data) - ntx := 0 - consumed := 0 - var err error - var capguard *util.ErlCapacityGuard if handler.erl != nil { congestedERL := float64(cap(handler.backlogQueue))*handler.backlogCongestionThreshold < float64(len(handler.backlogQueue)) // consume a capacity unit // if the elastic rate limiter cannot vend a capacity, the error it returns // is sufficient to indicate that we should enable Congestion Control, because // an issue in vending capacity indicates the underlying resource (TXBacklog) is full - capguard, err = handler.erl.ConsumeCapacity(rawmsg.Sender.(util.ErlClient)) + capguard, err = handler.erl.ConsumeCapacity(sender.(util.ErlClient)) if err != nil { handler.erl.EnableCongestionControl() // if there is no capacity, it is the same as if we failed to put the item onto the backlog, so report such transactionMessagesDroppedFromBacklog.Inc(nil) - return network.OutgoingMessage{Action: network.Ignore} + return msgKey, capguard, true } // if the backlog Queue has 50% of its buffer back, turn congestion control off if !congestedERL { handler.erl.DisableCongestionControl() } } + return msgKey, capguard, false +} + +// decodeMsg decodes TX message buffer into transactions.SignedTxn, +// and returns number of bytes consumed from the buffer and a boolean indicating if the message was invalid. +func decodeMsg(data []byte) (unverifiedTxGroup []transactions.SignedTxn, consumed int, invalid bool) { + unverifiedTxGroup = make([]transactions.SignedTxn, 1) + dec := protocol.NewMsgpDecoderBytes(data) + ntx := 0 for { if len(unverifiedTxGroup) == ntx { @@ -630,7 +649,7 @@ func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) net break } logging.Base().Warnf("Received a non-decodable txn: %v", err) - return network.OutgoingMessage{Action: network.Disconnect} + return nil, 0, true } consumed = dec.Consumed() ntx++ @@ -639,13 +658,13 @@ func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) net if dec.Remaining() > 0 { // if something else left in the buffer - this is an error, drop transactionMessageTxGroupExcessive.Inc(nil) - return network.OutgoingMessage{Action: network.Disconnect} + return nil, 0, true } } } if ntx == 0 { logging.Base().Warnf("Received empty tx group") - return network.OutgoingMessage{Action: network.Disconnect} + return nil, 0, true } unverifiedTxGroup = unverifiedTxGroup[:ntx] @@ -654,22 +673,57 @@ func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) net transactionMessageTxGroupFull.Inc(nil) } + return unverifiedTxGroup, consumed, false +} + +// incomingTxGroupDupRateLimit checks +// - if the incoming transaction group has been seen before after reencoding to canonical representation, and +// - if the sender is rate limited by the per-application rate limiter. +func (handler *TxHandler) incomingTxGroupDupRateLimit(unverifiedTxGroup []transactions.SignedTxn, encodedExpectedSize int, sender network.DisconnectablePeer) (*crypto.Digest, bool) { var canonicalKey *crypto.Digest if handler.txCanonicalCache != nil { - if canonicalKey, isDup = handler.dedupCanonical(ntx, unverifiedTxGroup, consumed); isDup { + var isDup bool + if canonicalKey, isDup = handler.dedupCanonical(unverifiedTxGroup, encodedExpectedSize); isDup { transactionMessagesDupCanonical.Inc(nil) - return network.OutgoingMessage{Action: network.Ignore} + return canonicalKey, true } } // rate limit per application in a group. Limiting any app in a group drops the entire message. if handler.appLimiter != nil { congestedARL := len(handler.backlogQueue) > handler.appLimiterBacklogThreshold - if congestedARL && handler.appLimiter.shouldDrop(unverifiedTxGroup, rawmsg.Sender.(network.IPAddressable).RoutingAddr()) { + if congestedARL && handler.appLimiter.shouldDrop(unverifiedTxGroup, sender.(network.IPAddressable).RoutingAddr()) { transactionMessagesAppLimiterDrop.Inc(nil) - return network.OutgoingMessage{Action: network.Ignore} + return canonicalKey, true } } + return canonicalKey, false +} + +// processIncomingTxn decodes a transaction group from incoming message and enqueues into the back log for processing. +// The function also performs some input data pre-validation; +// - txn groups are cut to MaxTxGroupSize size +// - message are checked for duplicates +// - transactions are checked for duplicates +func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) network.OutgoingMessage { + msgKey, capguard, shouldDrop := handler.incomingMsgDupErlCheck(rawmsg.Data, rawmsg.Sender) + if shouldDrop { + // this TX message was found in the duplicate cache, or ERL rate-limited it + return network.OutgoingMessage{Action: network.Ignore} + } + + unverifiedTxGroup, consumed, invalid := decodeMsg(rawmsg.Data) + if invalid { + // invalid encoding or exceeding txgroup, disconnect from this peer + return network.OutgoingMessage{Action: network.Disconnect} + } + + canonicalKey, drop := handler.incomingTxGroupDupRateLimit(unverifiedTxGroup, consumed, rawmsg.Sender) + if drop { + // this re-serialized txgroup was detected as a duplicate by the canonical message cache, + // or it was rate-limited by the per-app rate limiter + return network.OutgoingMessage{Action: network.Ignore} + } select { case handler.backlogQueue <- &txBacklogMsg{ @@ -696,6 +750,75 @@ func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) net return network.OutgoingMessage{Action: network.Ignore} } +type validatedIncomingTxMessage struct { + rawmsg network.IncomingMessage + unverifiedTxGroup []transactions.SignedTxn + msgKey *crypto.Digest + canonicalKey *crypto.Digest + capguard *util.ErlCapacityGuard +} + +// validateIncomingTxMessage is the validator for the MessageProcessor implementation used by P2PNetwork. +func (handler *TxHandler) validateIncomingTxMessage(rawmsg network.IncomingMessage) network.ValidatedMessage { + msgKey, capguard, shouldDrop := handler.incomingMsgDupErlCheck(rawmsg.Data, rawmsg.Sender) + if shouldDrop { + // this TX message was found in the duplicate cache, or ERL rate-limited it + return network.ValidatedMessage{Action: network.Ignore, ValidatorData: nil} + } + + unverifiedTxGroup, consumed, invalid := decodeMsg(rawmsg.Data) + if invalid { + // invalid encoding or exceeding txgroup, disconnect from this peer + return network.ValidatedMessage{Action: network.Disconnect, ValidatorData: nil} + } + + canonicalKey, drop := handler.incomingTxGroupDupRateLimit(unverifiedTxGroup, consumed, rawmsg.Sender) + if drop { + // this re-serialized txgroup was detected as a duplicate by the canonical message cache, + // or it was rate-limited by the per-app rate limiter + return network.ValidatedMessage{Action: network.Ignore, ValidatorData: nil} + } + + return network.ValidatedMessage{ + Action: network.Accept, + Tag: rawmsg.Tag, + ValidatorData: &validatedIncomingTxMessage{ + rawmsg: rawmsg, + unverifiedTxGroup: unverifiedTxGroup, + msgKey: msgKey, + canonicalKey: canonicalKey, + capguard: capguard, + }, + } +} + +// processIncomingTxMessage is the handler for the MessageProcessor implementation used by P2PNetwork. +func (handler *TxHandler) processIncomingTxMessage(validatedMessage network.ValidatedMessage) network.OutgoingMessage { + msg := validatedMessage.ValidatorData.(*validatedIncomingTxMessage) + select { + case handler.backlogQueue <- &txBacklogMsg{ + rawmsg: &msg.rawmsg, + unverifiedTxGroup: msg.unverifiedTxGroup, + rawmsgDataHash: msg.msgKey, + unverifiedTxGroupHash: msg.canonicalKey, + capguard: msg.capguard, + }: + default: + // if we failed here we want to increase the corresponding metric. It might suggest that we + // want to increase the queue size. + transactionMessagesDroppedFromBacklog.Inc(nil) + + // additionally, remove the txn from duplicate caches to ensure it can be re-submitted + if handler.txCanonicalCache != nil && msg.canonicalKey != nil { + handler.txCanonicalCache.Delete(msg.canonicalKey) + } + if handler.msgCache != nil && msg.msgKey != nil { + handler.msgCache.DeleteByKey(msg.msgKey) + } + } + return network.OutgoingMessage{Action: network.Ignore} +} + var errBackLogFullLocal = errors.New("backlog full") // LocalTransaction is a special shortcut handler for local transactions and intended to be used diff --git a/network/gossipNode.go b/network/gossipNode.go index fb0a415876..eeeca95167 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -88,6 +88,12 @@ type GossipNode interface { // ClearHandlers deregisters all the existing message handlers. ClearHandlers() + // RegisterProcessors adds to the set of given message processors. + RegisterProcessors(dispatch []TaggedMessageProcessor) + + // ClearProcessors deregisters all the existing message processors. + ClearProcessors() + // GetHTTPClient returns a http.Client with a suitable for the network Transport // that would also limit the number of outgoing connections. GetHTTPClient(address string) (*http.Client, error) @@ -162,6 +168,14 @@ type OutgoingMessage struct { OnRelease func() } +// ValidatedMessage is a message that has been validated and is ready to be processed. +// Think as an intermediate one between IncomingMessage and OutgoingMessage +type ValidatedMessage struct { + Action ForwardingPolicy + Tag Tag + ValidatorData interface{} +} + // ForwardingPolicy is an enum indicating to whom we should send a message // //msgp:ignore ForwardingPolicy @@ -179,6 +193,9 @@ const ( // Respond - reply to the sender Respond + + // Accept - accept for further processing after successful validation + Accept ) // MessageHandler takes a IncomingMessage (e.g., vote, transaction), processes it, and returns what (if anything) @@ -189,20 +206,52 @@ type MessageHandler interface { Handle(message IncomingMessage) OutgoingMessage } -// HandlerFunc represents an implemenation of the MessageHandler interface +// HandlerFunc represents an implementation of the MessageHandler interface type HandlerFunc func(message IncomingMessage) OutgoingMessage -// Handle implements MessageHandler.Handle, calling the handler with the IncomingKessage and returning the OutgoingMessage +// Handle implements MessageHandler.Handle, calling the handler with the IncomingMessage and returning the OutgoingMessage func (f HandlerFunc) Handle(message IncomingMessage) OutgoingMessage { return f(message) } +// MessageProcessor takes a IncomingMessage (e.g., vote, transaction), processes it, and returns what (if anything) +// to send to the network in response. +// This is an extension of the MessageHandler that works in two stages: validate ->[result]-> handle. +type MessageProcessor interface { + Validate(message IncomingMessage) ValidatedMessage + Handle(message ValidatedMessage) OutgoingMessage +} + +// ProcessorValidateFunc represents an implementation of the MessageProcessor interface +type ProcessorValidateFunc func(message IncomingMessage) ValidatedMessage + +// ProcessorHandleFunc represents an implementation of the MessageProcessor interface +type ProcessorHandleFunc func(message ValidatedMessage) OutgoingMessage + +// Validate implements MessageProcessor.Validate, calling the validator with the IncomingMessage and returning the action +// and validation extra data that can be use as the handler input. +func (f ProcessorValidateFunc) Validate(message IncomingMessage) ValidatedMessage { + return f(message) +} + +// Handle implements MessageProcessor.Handle calling the handler with the ValidatedMessage and returning the OutgoingMessage +func (f ProcessorHandleFunc) Handle(message ValidatedMessage) OutgoingMessage { + return f(message) +} + // TaggedMessageHandler receives one type of broadcast messages type TaggedMessageHandler struct { Tag MessageHandler } +// TaggedMessageProcessor receives one type of broadcast messages +// and performs two stage processing: validating and handling +type TaggedMessageProcessor struct { + Tag + MessageProcessor +} + // Propagate is a convenience function to save typing in the common case of a message handler telling us to propagate an incoming message // "return network.Propagate(msg)" instead of "return network.OutgoingMsg{network.Broadcast, msg.Tag, msg.Data}" func Propagate(msg IncomingMessage) OutgoingMessage { diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go index 7abb2ab569..6041d95f9a 100644 --- a/network/hybridNetwork.go +++ b/network/hybridNetwork.go @@ -180,6 +180,18 @@ func (n *HybridP2PNetwork) ClearHandlers() { n.wsNetwork.ClearHandlers() } +// RegisterProcessors adds to the set of given message handlers. +func (n *HybridP2PNetwork) RegisterProcessors(dispatch []TaggedMessageProcessor) { + n.p2pNetwork.RegisterProcessors(dispatch) + n.wsNetwork.RegisterProcessors(dispatch) +} + +// ClearProcessors deregisters all the existing message handlers. +func (n *HybridP2PNetwork) ClearProcessors() { + n.p2pNetwork.ClearProcessors() + n.wsNetwork.ClearProcessors() +} + // GetHTTPClient returns a http.Client with a suitable for the network Transport // that would also limit the number of outgoing connections. func (n *HybridP2PNetwork) GetHTTPClient(address string) (*http.Client, error) { diff --git a/network/multiplexer.go b/network/multiplexer.go index 0e97d63f28..2d69259c9d 100644 --- a/network/multiplexer.go +++ b/network/multiplexer.go @@ -24,32 +24,55 @@ import ( // Multiplexer is a message handler that sorts incoming messages by Tag and passes // them along to the relevant message handler for that type of message. type Multiplexer struct { - msgHandlers atomic.Value // stores map[Tag]MessageHandler, an immutable map. + msgHandlers atomic.Value // stores map[Tag]MessageHandler, an immutable map. + msgProcessors atomic.Value // stores map[Tag]MessageProcessor, an immutable map. } // MakeMultiplexer creates an empty Multiplexer func MakeMultiplexer() *Multiplexer { m := &Multiplexer{} - m.ClearHandlers([]Tag{}) // allocate the map + m.ClearHandlers(nil) // allocate the map + m.ClearProcessors(nil) // allocate the map return m } -// getHandlersMap retrieves the handlers map. -func (m *Multiplexer) getHandlersMap() map[Tag]MessageHandler { - handlersVal := m.msgHandlers.Load() - if handlers, valid := handlersVal.(map[Tag]MessageHandler); valid { +// getMap retrieves a typed map from an atomic.Value. +func getMap[T any](source *atomic.Value) map[Tag]T { + mp := source.Load() + if handlers, valid := mp.(map[Tag]T); valid { return handlers } return nil } -// Retrives the handler for the given message Tag from the handlers array while taking a read lock. -func (m *Multiplexer) getHandler(tag Tag) (MessageHandler, bool) { - if handlers := m.getHandlersMap(); handlers != nil { +// getHandlersMap retrieves the handlers map. +func (m *Multiplexer) getHandlersMap() map[Tag]MessageHandler { + return getMap[MessageHandler](&m.msgHandlers) +} + +// getProcessorsMap retrieves the processors map. +func (m *Multiplexer) getProcessorsMap() map[Tag]MessageProcessor { + return getMap[MessageProcessor](&m.msgHandlers) +} + +// Retrieves the handler for the given message Tag from the given value while. +func getHandler[T any](source *atomic.Value, tag Tag) (T, bool) { + if handlers := getMap[T](source); handlers != nil { handler, ok := handlers[tag] return handler, ok } - return nil, false + var empty T + return empty, false +} + +// Retrieves the handler for the given message Tag from the handlers array. +func (m *Multiplexer) getHandler(tag Tag) (MessageHandler, bool) { + return getHandler[MessageHandler](&m.msgHandlers, tag) +} + +// Retrieves the processor for the given message Tag from the processors array. +func (m *Multiplexer) getProcessor(tag Tag) (MessageProcessor, bool) { + return getHandler[MessageProcessor](&m.msgProcessors, tag) } // Handle is the "input" side of the multiplexer. It dispatches the message to the previously defined handler. @@ -63,6 +86,28 @@ func (m *Multiplexer) Handle(msg IncomingMessage) OutgoingMessage { return OutgoingMessage{} } +// Validate is an alternative "input" side of the multiplexer. It dispatches the message to the previously defined validator. +func (m *Multiplexer) Validate(msg IncomingMessage) ValidatedMessage { + handler, ok := m.getProcessor(msg.Tag) + + if ok { + outmsg := handler.Validate(msg) + return outmsg + } + return ValidatedMessage{} +} + +// Process is the second step of message handling after validation. It dispatches the message to the previously defined processor. +func (m *Multiplexer) Process(msg ValidatedMessage) OutgoingMessage { + handler, ok := m.getProcessor(msg.Tag) + + if ok { + outmsg := handler.Handle(msg) + return outmsg + } + return OutgoingMessage{} +} + // RegisterHandlers registers the set of given message handlers. func (m *Multiplexer) RegisterHandlers(dispatch []TaggedMessageHandler) { mp := make(map[Tag]MessageHandler) @@ -80,10 +125,27 @@ func (m *Multiplexer) RegisterHandlers(dispatch []TaggedMessageHandler) { m.msgHandlers.Store(mp) } -// ClearHandlers deregisters all the existing message handlers other than the one provided in the excludeTags list -func (m *Multiplexer) ClearHandlers(excludeTags []Tag) { +// RegisterProcessors registers the set of given message handlers. +func (m *Multiplexer) RegisterProcessors(dispatch []TaggedMessageProcessor) { + mp := make(map[Tag]MessageProcessor) + if existingMap := m.getProcessorsMap(); existingMap != nil { + for k, v := range existingMap { + mp[k] = v + } + } + for _, v := range dispatch { + if _, has := mp[v.Tag]; has { + panic(fmt.Sprintf("Already registered a handler for tag %v", v.Tag)) + } + mp[v.Tag] = v.MessageProcessor + } + m.msgProcessors.Store(mp) +} + +// ClearProcessors deregisters all the existing message handlers other than the one provided in the excludeTags list +func clear[T any](target *atomic.Value, excludeTags []Tag) { if len(excludeTags) == 0 { - m.msgHandlers.Store(make(map[Tag]MessageHandler)) + target.Store(make(map[Tag]T)) return } @@ -93,13 +155,23 @@ func (m *Multiplexer) ClearHandlers(excludeTags []Tag) { excludeTagsMap[tag] = true } - currentHandlersMap := m.getHandlersMap() - newMap := make(map[Tag]MessageHandler, len(excludeTagsMap)) - for tag, handler := range currentHandlersMap { + currentMap := getMap[T](target) + newMap := make(map[Tag]T, len(excludeTagsMap)) + for tag, handler := range currentMap { if excludeTagsMap[tag] { newMap[tag] = handler } } - m.msgHandlers.Store(newMap) + target.Store(newMap) +} + +// ClearHandlers deregisters all the existing message handlers other than the one provided in the excludeTags list +func (m *Multiplexer) ClearHandlers(excludeTags []Tag) { + clear[MessageHandler](&m.msgHandlers, excludeTags) +} + +// ClearProcessors deregisters all the existing message handlers other than the one provided in the excludeTags list +func (m *Multiplexer) ClearProcessors(excludeTags []Tag) { + clear[MessageProcessor](&m.msgProcessors, excludeTags) } diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index dd830ac5d7..5eaf6ec36f 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -659,6 +659,16 @@ func (n *P2PNetwork) ClearHandlers() { n.handler.ClearHandlers([]Tag{}) } +// RegisterProcessors adds to the set of given message handlers. +func (n *P2PNetwork) RegisterProcessors(dispatch []TaggedMessageProcessor) { + n.handler.RegisterProcessors(dispatch) +} + +// ClearProcessors deregisters all the existing message handlers. +func (n *P2PNetwork) ClearProcessors() { + n.handler.ClearProcessors([]Tag{}) +} + // GetHTTPClient returns a http.Client with a suitable for the network Transport // that would also limit the number of outgoing connections. func (n *P2PNetwork) GetHTTPClient(address string) (*http.Client, error) { @@ -884,11 +894,12 @@ func (n *P2PNetwork) txTopicHandleLoop() { sub.Cancel() return } + // if there is a self-sent the message no need to process it. + if msg.ReceivedFrom == n.service.ID() { + continue + } - // discard TX message. - // from gossipsub's point of view, it's just waiting to hear back from the validator, - // and txHandler does all its work in the validator, so we don't need to do anything here - _ = msg + _ = n.handler.Process(msg.ValidatorData.(ValidatedMessage)) // participation or configuration change, cancel subscription and quit if !n.wantTXGossip.Load() { @@ -923,14 +934,15 @@ func (n *P2PNetwork) txTopicValidator(ctx context.Context, peerID peer.ID, msg * peerStats.txReceived.Add(1) n.peerStatsMu.Unlock() - outmsg := n.handler.Handle(inmsg) + outmsg := n.handler.Validate(inmsg) // there was a decision made in the handler about this message switch outmsg.Action { case Ignore: return pubsub.ValidationIgnore case Disconnect: return pubsub.ValidationReject - case Broadcast: // TxHandler.processIncomingTxn does not currently return this Action + case Accept: + msg.ValidatorData = outmsg return pubsub.ValidationAccept default: n.log.Warnf("handler returned invalid action %d", outmsg.Action) diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 4e929723f6..7cc590ff02 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -18,6 +18,7 @@ package network import ( "context" + "errors" "fmt" "io" "net/http" @@ -98,15 +99,26 @@ func TestP2PSubmitTX(t *testing.T) { // now we should be connected in a line: B <-> A <-> C where both B and C are connected to A but not each other // Since we aren't using the transaction handler in this test, we need to register a pass-through handler - passThroughHandler := []TaggedMessageHandler{ - {Tag: protocol.TxnTag, MessageHandler: HandlerFunc(func(msg IncomingMessage) OutgoingMessage { - return OutgoingMessage{Action: Broadcast} - })}, + passThroughHandler := []TaggedMessageProcessor{ + { + Tag: protocol.TxnTag, + MessageProcessor: struct { + ProcessorValidateFunc + ProcessorHandleFunc + }{ + ProcessorValidateFunc(func(msg IncomingMessage) ValidatedMessage { + return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatorData: nil} + }), + ProcessorHandleFunc(func(msg ValidatedMessage) OutgoingMessage { + return OutgoingMessage{Action: Ignore} + }), + }, + }, } - netA.RegisterHandlers(passThroughHandler) - netB.RegisterHandlers(passThroughHandler) - netC.RegisterHandlers(passThroughHandler) + netA.RegisterProcessors(passThroughHandler) + netB.RegisterProcessors(passThroughHandler) + netC.RegisterProcessors(passThroughHandler) // send messages from B and confirm that they get received by C (via A) for i := 0; i < 10; i++ { @@ -178,14 +190,26 @@ func TestP2PSubmitTXNoGossip(t *testing.T) { time.Sleep(time.Second) // give time for peers to connect. // ensure netC cannot receive messages - passThroughHandler := []TaggedMessageHandler{ - {Tag: protocol.TxnTag, MessageHandler: HandlerFunc(func(msg IncomingMessage) OutgoingMessage { - return OutgoingMessage{Action: Broadcast} - })}, + + passThroughHandler := []TaggedMessageProcessor{ + { + Tag: protocol.TxnTag, + MessageProcessor: struct { + ProcessorValidateFunc + ProcessorHandleFunc + }{ + ProcessorValidateFunc(func(msg IncomingMessage) ValidatedMessage { + return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatorData: nil} + }), + ProcessorHandleFunc(func(msg ValidatedMessage) OutgoingMessage { + return OutgoingMessage{Action: Ignore} + }), + }, + }, } - netB.RegisterHandlers(passThroughHandler) - netC.RegisterHandlers(passThroughHandler) + netB.RegisterProcessors(passThroughHandler) + netC.RegisterProcessors(passThroughHandler) for i := 0; i < 10; i++ { err = netA.Broadcast(context.Background(), protocol.TxnTag, []byte(fmt.Sprintf("test %d", i)), false, nil) require.NoError(t, err) @@ -207,7 +231,7 @@ func TestP2PSubmitTXNoGossip(t *testing.T) { 50*time.Millisecond, ) - // check netB did not receive the messages + // check netC did not receive the messages netC.peerStatsMu.Lock() _, ok := netC.peerStats[netA.service.ID()] netC.peerStatsMu.Unlock() @@ -804,9 +828,33 @@ func TestP2PRelay(t *testing.T) { return netA.hasPeers() && netB.hasPeers() }, 2*time.Second, 50*time.Millisecond) - counter := newMessageCounter(t, 1) - counterDone := counter.done - netA.RegisterHandlers([]TaggedMessageHandler{{Tag: protocol.TxnTag, MessageHandler: counter}}) + makeCounterHandler := func(numExpected int) ([]TaggedMessageProcessor, *int, chan struct{}) { + numActual := 0 + counterDone := make(chan struct{}) + counterHandler := []TaggedMessageProcessor{ + { + Tag: protocol.TxnTag, + MessageProcessor: struct { + ProcessorValidateFunc + ProcessorHandleFunc + }{ + ProcessorValidateFunc(func(msg IncomingMessage) ValidatedMessage { + return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatorData: nil} + }), + ProcessorHandleFunc(func(msg ValidatedMessage) OutgoingMessage { + numActual++ + if numActual >= numExpected { + close(counterDone) + } + return OutgoingMessage{Action: Ignore} + }), + }, + }, + } + return counterHandler, &numActual, counterDone + } + counterHandler, _, counterDone := makeCounterHandler(1) + netA.RegisterProcessors(counterHandler) // send 5 messages from both netB to netA // since there is no node with listening address set => no messages should be received @@ -848,10 +896,9 @@ func TestP2PRelay(t *testing.T) { }, 2*time.Second, 50*time.Millisecond) const expectedMsgs = 10 - counter = newMessageCounter(t, expectedMsgs) - counterDone = counter.done - netA.ClearHandlers() - netA.RegisterHandlers([]TaggedMessageHandler{{Tag: protocol.TxnTag, MessageHandler: counter}}) + counterHandler, count, counterDone := makeCounterHandler(expectedMsgs) + netA.ClearProcessors() + netA.RegisterProcessors(counterHandler) for i := 0; i < expectedMsgs/2; i++ { err := netB.Relay(context.Background(), protocol.TxnTag, []byte{1, 2, 3, byte(i)}, true, nil) @@ -868,28 +915,41 @@ func TestP2PRelay(t *testing.T) { select { case <-counterDone: case <-time.After(2 * time.Second): - if counter.count < expectedMsgs { - require.Failf(t, "One or more messages failed to reach destination network", "%d > %d", expectedMsgs, counter.count) - } else if counter.count > expectedMsgs { - require.Failf(t, "One or more messages that were expected to be dropped, reached destination network", "%d < %d", expectedMsgs, counter.count) + if *count < expectedMsgs { + require.Failf(t, "One or more messages failed to reach destination network", "%d > %d", expectedMsgs, *count) + } else if *count > expectedMsgs { + require.Failf(t, "One or more messages that were expected to be dropped, reached destination network", "%d < %d", expectedMsgs, *count) } } } type mockSubPService struct { mockService - count atomic.Int64 + count atomic.Int64 + otherPeerID peer.ID + shouldNextFail bool } type mockSubscription struct { + peerID peer.ID + shouldNextFail bool } -func (m *mockSubscription) Next(ctx context.Context) (*pubsub.Message, error) { return nil, nil } -func (m *mockSubscription) Cancel() {} +func (m *mockSubscription) Next(ctx context.Context) (*pubsub.Message, error) { + if m.shouldNextFail { + return nil, errors.New("mockSubscription error") + } + return &pubsub.Message{ReceivedFrom: m.peerID}, nil +} +func (m *mockSubscription) Cancel() {} func (m *mockSubPService) Subscribe(topic string, val pubsub.ValidatorEx) (p2p.SubNextCancellable, error) { m.count.Add(1) - return &mockSubscription{}, nil + otherPeerID := m.otherPeerID + if otherPeerID == "" { + otherPeerID = "mockSubPServicePeerID" + } + return &mockSubscription{peerID: otherPeerID, shouldNextFail: m.shouldNextFail}, nil } // TestP2PWantTXGossip checks txTopicHandleLoop runs as expected on wantTXGossip changes @@ -900,7 +960,8 @@ func TestP2PWantTXGossip(t *testing.T) { // cancelled context to trigger subscription.Next to return ctx, cancel := context.WithCancel(context.Background()) cancel() - mockService := &mockSubPService{} + peerID := peer.ID("myPeerID") + mockService := &mockSubPService{mockService: mockService{id: peerID}, shouldNextFail: true} net := &P2PNetwork{ service: mockService, log: logging.TestingLog(t), diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 50307f2738..13e0fe71f5 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -853,6 +853,14 @@ func (wn *WebsocketNetwork) ClearHandlers() { wn.handler.ClearHandlers([]Tag{protocol.PingTag, protocol.PingReplyTag, protocol.NetPrioResponseTag}) } +// RegisterProcessors registers the set of given message handlers. +func (wn *WebsocketNetwork) RegisterProcessors(dispatch []TaggedMessageProcessor) { +} + +// ClearProcessors deregisters all the existing message handlers. +func (wn *WebsocketNetwork) ClearProcessors() { +} + func (wn *WebsocketNetwork) setHeaders(header http.Header) { localTelemetryGUID := wn.log.GetTelemetryGUID() localInstanceName := wn.log.GetInstanceName() From 8f9ad90fa4e008c4518721ad9c13e22fd8a303ca Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Fri, 7 Jun 2024 15:27:10 -0400 Subject: [PATCH 22/38] post-merge fixes --- data/txHandler.go | 45 +++++++++++++++------- go.mod | 16 +++----- go.sum | 67 ++++++++++++++++++++++++++++++--- node/node.go | 2 +- tools/block-generator/go.mod | 21 +++++++++-- tools/block-generator/go.sum | 72 +++++++++++++++++++++++++++++++----- 6 files changed, 179 insertions(+), 44 deletions(-) diff --git a/data/txHandler.go b/data/txHandler.go index c6953b7909..bb515db2f7 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -609,19 +609,6 @@ func (handler *TxHandler) incomingMsgDupErlCheck(data []byte, sender network.Dis } var err error - var capguard *util.ErlCapacityGuard - accepted := false - defer func() { - // if we failed to put the item onto the backlog, we should release the capacity if any - if !accepted { - if capguard != nil { - if capErr := capguard.Release(); capErr != nil { - logging.Base().Warnf("Failed to release capacity to ElasticRateLimiter: %v", capErr) - } - } - } - }() - if handler.erl != nil { congestedERL := float64(cap(handler.backlogQueue))*handler.backlogCongestionThreshold < float64(len(handler.backlogQueue)) // consume a capacity unit @@ -720,6 +707,19 @@ func (handler *TxHandler) incomingTxGroupDupRateLimit(unverifiedTxGroup []transa // - transactions are checked for duplicates func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) network.OutgoingMessage { msgKey, capguard, shouldDrop := handler.incomingMsgDupErlCheck(rawmsg.Data, rawmsg.Sender) + + accepted := false + defer func() { + // if we failed to put the item onto the backlog, we should release the capacity if any + if !accepted { + if capguard != nil { + if capErr := capguard.Release(); capErr != nil { + logging.Base().Warnf("processIncomingTxn: failed to release capacity to ElasticRateLimiter: %v", capErr) + } + } + } + }() + if shouldDrop { // this TX message was found in the duplicate cache, or ERL rate-limited it return network.OutgoingMessage{Action: network.Ignore} @@ -775,6 +775,19 @@ type validatedIncomingTxMessage struct { // validateIncomingTxMessage is the validator for the MessageProcessor implementation used by P2PNetwork. func (handler *TxHandler) validateIncomingTxMessage(rawmsg network.IncomingMessage) network.ValidatedMessage { msgKey, capguard, shouldDrop := handler.incomingMsgDupErlCheck(rawmsg.Data, rawmsg.Sender) + + accepted := false + defer func() { + // if we failed to put the item onto the backlog, we should release the capacity if any + if !accepted { + if capguard != nil { + if capErr := capguard.Release(); capErr != nil { + logging.Base().Warnf("validateIncomingTxMessage: failed to release capacity to ElasticRateLimiter: %v", capErr) + } + } + } + }() + if shouldDrop { // this TX message was found in the duplicate cache, or ERL rate-limited it return network.ValidatedMessage{Action: network.Ignore, ValidatorData: nil} @@ -793,6 +806,7 @@ func (handler *TxHandler) validateIncomingTxMessage(rawmsg network.IncomingMessa return network.ValidatedMessage{Action: network.Ignore, ValidatorData: nil} } + accepted = true return network.ValidatedMessage{ Action: network.Accept, Tag: rawmsg.Tag, @@ -829,6 +843,11 @@ func (handler *TxHandler) processIncomingTxMessage(validatedMessage network.Vali if handler.msgCache != nil && msg.msgKey != nil { handler.msgCache.DeleteByKey(msg.msgKey) } + if msg.capguard != nil { + if capErr := msg.capguard.Release(); capErr != nil { + logging.Base().Warnf("processIncomingTxMessage: failed to release capacity to ElasticRateLimiter: %v", capErr) + } + } } return network.OutgoingMessage{Action: network.Ignore} } diff --git a/go.mod b/go.mod index 85be321982..bfddb8e3db 100644 --- a/go.mod +++ b/go.mod @@ -38,12 +38,11 @@ require ( github.com/libp2p/go-yamux/v4 v4.0.1 github.com/mattn/go-sqlite3 v1.14.16 github.com/miekg/dns v1.1.58 - github.com/muesli/termenv v0.15.2 github.com/multiformats/go-multiaddr v0.12.3 github.com/multiformats/go-multiaddr-dns v0.3.1 github.com/olivere/elastic v6.2.14+incompatible - github.com/prometheus/client_golang v1.14.0 - github.com/prometheus/client_model v0.4.0 + github.com/prometheus/client_golang v1.18.0 + github.com/prometheus/client_model v0.6.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.4 @@ -79,7 +78,7 @@ require ( github.com/flynn/noise v1.1.0 // indirect github.com/fortytw2/leaktest v1.3.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.19.5 // indirect @@ -88,7 +87,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect github.com/google/uuid v1.4.0 // indirect @@ -117,6 +115,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect @@ -125,13 +124,10 @@ require ( github.com/libp2p/go-nat v0.2.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect @@ -153,12 +149,10 @@ require ( github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.18.0 // indirect - github.com/prometheus/client_model v0.6.0 // indirect + github.com/polydawn/refmt v0.89.0 // indirect github.com/prometheus/common v0.47.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-20 v0.2.3 // indirect github.com/quic-go/quic-go v0.42.0 // indirect github.com/quic-go/webtransport-go v0.6.0 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect diff --git a/go.sum b/go.sum index 13e49d4db1..1a52e04711 100644 --- a/go.sum +++ b/go.sum @@ -125,6 +125,7 @@ github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0 github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= @@ -141,6 +142,7 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= @@ -153,8 +155,11 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -185,6 +190,9 @@ github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -197,7 +205,9 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -208,7 +218,9 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -220,13 +232,16 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -240,14 +255,14 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= -github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -258,6 +273,15 @@ github.com/ipfs/boxo v0.10.0 h1:tdDAxq8jrsbRkYoF+5Rcqyeb91hgWe2hp7iLu7ORZLY= github.com/ipfs/boxo v0.10.0/go.mod h1:Fg+BnfxZ0RPzR0nOodzdIq3A7KgoWAOWsEIImrIQdBM= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= +github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= +github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g= @@ -282,6 +306,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -327,14 +352,22 @@ github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= github.com/libp2p/go-libp2p v0.33.2 h1:vCdwnFxoGOXMKmaGHlDSnL4bM3fQeW8pgIa9DECnb40= github.com/libp2p/go-libp2p v0.33.2/go.mod h1:zTeppLuCvUIkT118pFVzA8xzP/p2dJYOMApCkFh0Yww= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-kad-dht v0.24.3 h1:VjxtDVWaaf4UFjGBf+yl2JCiGaHx7+ctAUa9oJCR3QE= +github.com/libp2p/go-libp2p-kad-dht v0.24.3/go.mod h1:BShPzRbK6+fN3hk8a0WGAYKpb8m4k+DtchkqouGTrSg= +github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0= +github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0= github.com/libp2p/go-libp2p-pubsub v0.10.0 h1:wS0S5FlISavMaAbxyQn3dxMOe2eegMfswM471RuHJwA= github.com/libp2p/go-libp2p-pubsub v0.10.0/go.mod h1:1OxbaT/pFRO5h+Dpze8hdHQ63R0ke55XTs6b6NwLLkw= +github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= +github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= @@ -345,8 +378,6 @@ github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9t github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= -github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -426,8 +457,6 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= -github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= -github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= @@ -451,6 +480,8 @@ github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= @@ -488,6 +519,7 @@ github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFD github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -593,6 +625,15 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -605,6 +646,7 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -619,6 +661,7 @@ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -637,6 +680,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -747,6 +791,9 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -775,6 +822,7 @@ google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= @@ -782,13 +830,18 @@ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -796,6 +849,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= @@ -821,6 +875,7 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= pgregory.net/rapid v0.6.2 h1:ErW5sL+UKtfBfUTsWHDCoeB+eZKLKMxrSd1VJY6W4bw= diff --git a/node/node.go b/node/node.go index 53254eb466..206d2fa087 100644 --- a/node/node.go +++ b/node/node.go @@ -398,7 +398,7 @@ func (node *AlgorandFullNode) Start() error { // Capabilities returns the node's capabilities for advertising to other nodes. func (node *AlgorandFullNode) Capabilities() []p2p.Capability { var caps []p2p.Capability - if node.IsArchival() { + if node.config.Archival { caps = append(caps, p2p.Archival) } if node.config.StoresCatchpoints() { diff --git a/tools/block-generator/go.mod b/tools/block-generator/go.mod index bc9da4d3cb..9baf9fafef 100644 --- a/tools/block-generator/go.mod +++ b/tools/block-generator/go.mod @@ -48,17 +48,21 @@ require ( github.com/elastic/gosigar v0.14.2 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect github.com/google/uuid v1.4.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -79,20 +83,22 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect github.com/libp2p/go-libp2p v0.33.2 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.24.3 // indirect + github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect github.com/libp2p/go-libp2p-pubsub v0.10.0 // indirect + github.com/libp2p/go-libp2p-record v0.2.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-nat v0.2.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/libp2p/go-reuseport v0.4.0 // indirect github.com/libp2p/go-yamux/v4 v4.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/miekg/dns v1.1.58 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect @@ -109,15 +115,16 @@ require ( github.com/multiformats/go-multicodec v0.9.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multistream v0.5.0 // indirect - github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/olivere/elastic v6.2.14+incompatible // indirect github.com/onsi/ginkgo/v2 v2.15.0 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/polydawn/refmt v0.89.0 // indirect github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/common v0.47.0 // indirect @@ -130,6 +137,11 @@ require ( github.com/sirupsen/logrus v1.8.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.16.0 // indirect go.uber.org/dig v1.17.1 // indirect go.uber.org/fx v1.20.1 // indirect go.uber.org/mock v0.4.0 // indirect @@ -143,6 +155,7 @@ require ( golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.18.0 // indirect + gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/sohlich/elogrus.v3 v3.0.0-20180410122755-1fa29e2f2009 // indirect lukechampine.com/blake3 v1.2.1 // indirect diff --git a/tools/block-generator/go.sum b/tools/block-generator/go.sum index 29082436c4..5ed96a265b 100644 --- a/tools/block-generator/go.sum +++ b/tools/block-generator/go.sum @@ -118,6 +118,7 @@ github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0 github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= @@ -132,6 +133,7 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= @@ -142,8 +144,11 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -164,6 +169,9 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -176,7 +184,9 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -187,7 +197,9 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -198,13 +210,16 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -218,14 +233,14 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= -github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -235,6 +250,15 @@ github.com/ipfs/boxo v0.10.0 h1:tdDAxq8jrsbRkYoF+5Rcqyeb91hgWe2hp7iLu7ORZLY= github.com/ipfs/boxo v0.10.0/go.mod h1:Fg+BnfxZ0RPzR0nOodzdIq3A7KgoWAOWsEIImrIQdBM= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= +github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= +github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g= @@ -257,6 +281,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= @@ -295,14 +320,22 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= github.com/libp2p/go-libp2p v0.33.2 h1:vCdwnFxoGOXMKmaGHlDSnL4bM3fQeW8pgIa9DECnb40= github.com/libp2p/go-libp2p v0.33.2/go.mod h1:zTeppLuCvUIkT118pFVzA8xzP/p2dJYOMApCkFh0Yww= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-kad-dht v0.24.3 h1:VjxtDVWaaf4UFjGBf+yl2JCiGaHx7+ctAUa9oJCR3QE= +github.com/libp2p/go-libp2p-kad-dht v0.24.3/go.mod h1:BShPzRbK6+fN3hk8a0WGAYKpb8m4k+DtchkqouGTrSg= +github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0= +github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0= github.com/libp2p/go-libp2p-pubsub v0.10.0 h1:wS0S5FlISavMaAbxyQn3dxMOe2eegMfswM471RuHJwA= github.com/libp2p/go-libp2p-pubsub v0.10.0/go.mod h1:1OxbaT/pFRO5h+Dpze8hdHQ63R0ke55XTs6b6NwLLkw= +github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= +github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= @@ -313,8 +346,6 @@ github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9t github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= -github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -331,8 +362,6 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -386,8 +415,6 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= -github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= -github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= @@ -411,6 +438,8 @@ github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= @@ -448,6 +477,7 @@ github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFD github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -505,9 +535,9 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -548,6 +578,15 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -560,6 +599,7 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -574,6 +614,7 @@ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -592,6 +633,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -696,6 +738,9 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -724,6 +769,7 @@ google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= @@ -731,13 +777,18 @@ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -745,6 +796,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= @@ -759,6 +811,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -768,6 +821,7 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= pgregory.net/rapid v0.6.2 h1:ErW5sL+UKtfBfUTsWHDCoeB+eZKLKMxrSd1VJY6W4bw= From 052792ded21d0baf909a229434e44a4bcf1a5d21 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:50:20 -0400 Subject: [PATCH 23/38] p2p: test scenarios support (#5962) --- config/config.go | 1 + config/localTemplate.go | 12 +- config/local_defaults.go | 2 +- go.mod | 4 +- installer/config.json.example | 2 +- logging/log.go | 7 + netdeploy/remote/nodeConfig.go | 1 + netdeploy/remote/nodecfg/nodeConfigurator.go | 35 +++ netdeploy/remote/nodecfg/nodeDir.go | 47 +++ network/p2p/http.go | 2 + network/p2p/logger.go | 118 ++++++++ network/p2p/p2p.go | 9 +- network/p2p/pubsub.go | 2 + network/p2p/streams.go | 8 +- network/p2pNetwork.go | 84 ++++-- network/p2pNetwork_test.go | 20 ++ network/websocketProxy_test.go | 2 +- network/wsPeer.go | 5 +- network/wsPeer_test.go | 12 +- node/node.go | 5 +- node/node_test.go | 8 +- test/heapwatch/agreement-log.py | 187 ++++++++++++ test/heapwatch/block_history_plot.py | 30 +- test/heapwatch/client_ram_report.py | 4 + test/heapwatch/metrics_aggs.py | 175 +++++++++++ test/heapwatch/metrics_delta.py | 75 ++--- test/heapwatch/metrics_lib.py | 272 ++++++++++++++++++ test/heapwatch/metrics_viz.py | 191 +++++------- test/heapwatch/requirements.txt | 3 + test/heapwatch/topology-extract-p2p.py | 104 +++++++ test/heapwatch/topology-extract-ws.py | 115 ++++++++ test/heapwatch/topology-viz.py | 75 +++++ test/testdata/configs/config-v34.json | 2 +- .../hello-world-small-p2p/genesis.json | 30 ++ .../recipes/hello-world-small-p2p/net.json | 107 +++++++ .../recipes/hello-world-small-p2p/recipe.json | 7 + .../hello-world-small-p2p/topology.json | 20 ++ .../recipes/hello-world-tiny-p2p/genesis.json | 30 ++ .../hosttemplates.json | 0 .../recipes/hello-world-tiny-p2p/net.json | 101 +++++++ .../recipes/hello-world-tiny-p2p/recipe.json | 7 + .../hello-world-tiny-p2p/topology.json | 20 ++ .../recipes/hello-world/genesis.json | 2 +- .../recipes/scenario1s-p2p/Makefile | 23 ++ .../recipes/scenario1s-p2p/README.md | 16 ++ .../scenario1s-p2p/copy-node-configs.py | 55 ++++ .../recipes/scenario1s-p2p/recipe.json | 7 + .../recipes/scenario1s/Makefile | 4 +- util/metrics/opencensus.go | 6 +- util/metrics/opencensus_test.go | 12 + util/metrics/prometheus.go | 2 +- util/metrics/prometheus_test.go | 18 ++ 52 files changed, 1828 insertions(+), 258 deletions(-) create mode 100644 network/p2p/logger.go create mode 100644 test/heapwatch/agreement-log.py create mode 100644 test/heapwatch/metrics_aggs.py create mode 100644 test/heapwatch/metrics_lib.py create mode 100644 test/heapwatch/topology-extract-p2p.py create mode 100644 test/heapwatch/topology-extract-ws.py create mode 100644 test/heapwatch/topology-viz.py create mode 100644 test/testdata/deployednettemplates/recipes/hello-world-small-p2p/genesis.json create mode 100644 test/testdata/deployednettemplates/recipes/hello-world-small-p2p/net.json create mode 100644 test/testdata/deployednettemplates/recipes/hello-world-small-p2p/recipe.json create mode 100644 test/testdata/deployednettemplates/recipes/hello-world-small-p2p/topology.json create mode 100644 test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/genesis.json rename test/testdata/deployednettemplates/recipes/{hello-world => hello-world-tiny-p2p}/hosttemplates.json (100%) create mode 100644 test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/net.json create mode 100644 test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/recipe.json create mode 100644 test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/topology.json create mode 100644 test/testdata/deployednettemplates/recipes/scenario1s-p2p/Makefile create mode 100644 test/testdata/deployednettemplates/recipes/scenario1s-p2p/README.md create mode 100644 test/testdata/deployednettemplates/recipes/scenario1s-p2p/copy-node-configs.py create mode 100644 test/testdata/deployednettemplates/recipes/scenario1s-p2p/recipe.json diff --git a/config/config.go b/config/config.go index a8beb05800..2d5d0bdbfe 100644 --- a/config/config.go +++ b/config/config.go @@ -268,6 +268,7 @@ const ( dnssecSRV = 1 << iota dnssecRelayAddr dnssecTelemetryAddr + dnssecTXT ) const ( diff --git a/config/localTemplate.go b/config/localTemplate.go index 1ebdab3870..e748d5eb43 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -362,8 +362,9 @@ type Local struct { // 0x01 (dnssecSRV) - validate SRV response // 0x02 (dnssecRelayAddr) - validate relays' names to addresses resolution // 0x04 (dnssecTelemetryAddr) - validate telemetry and metrics names to addresses resolution + // 0x08 (dnssecTXT) - validate TXT response // ... - DNSSecurityFlags uint32 `version[6]:"1"` + DNSSecurityFlags uint32 `version[6]:"1" version[34]:"9"` // EnablePingHandler controls whether the gossip node would respond to ping messages with a pong message. EnablePingHandler bool `version[6]:"true"` @@ -688,11 +689,16 @@ func (cfg Local) DNSSecurityRelayAddrEnforced() bool { return cfg.DNSSecurityFlags&dnssecRelayAddr != 0 } -// DNSSecurityTelemeryAddrEnforced returns true if relay name to ip addr resolution enforced -func (cfg Local) DNSSecurityTelemeryAddrEnforced() bool { +// DNSSecurityTelemetryAddrEnforced returns true if relay name to ip addr resolution enforced +func (cfg Local) DNSSecurityTelemetryAddrEnforced() bool { return cfg.DNSSecurityFlags&dnssecTelemetryAddr != 0 } +// DNSSecurityTXTEnforced returns true if TXT response verification enforced +func (cfg Local) DNSSecurityTXTEnforced() bool { + return cfg.DNSSecurityFlags&dnssecTXT != 0 +} + // CatchupVerifyCertificate returns true if certificate verification is needed func (cfg Local) CatchupVerifyCertificate() bool { return cfg.CatchupBlockValidateMode&catchupValidationModeCertificate == 0 diff --git a/config/local_defaults.go b/config/local_defaults.go index ca9b8f8509..232d4ae5df 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -51,7 +51,7 @@ var defaultLocal = Local{ ConnectionsRateLimitingWindowSeconds: 1, CrashDBDir: "", DNSBootstrapID: ".algorand.network?backup=.algorand.net&dedup=.algorand-.(network|net)", - DNSSecurityFlags: 1, + DNSSecurityFlags: 9, DeadlockDetection: 0, DeadlockDetectionThreshold: 30, DisableAPIAuth: false, diff --git a/go.mod b/go.mod index bfddb8e3db..e4d8c5b8d5 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/google/go-querystring v1.0.0 github.com/gorilla/mux v1.8.0 github.com/ipfs/go-log v1.0.5 + github.com/ipfs/go-log/v2 v2.5.1 github.com/jmoiron/sqlx v1.2.0 github.com/karalabe/usb v0.0.2 github.com/labstack/echo/v4 v4.9.1 @@ -47,6 +48,7 @@ require ( github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.4 go.opencensus.io v0.24.0 + go.uber.org/zap v1.27.0 golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a golang.org/x/sync v0.6.0 @@ -101,7 +103,6 @@ require ( github.com/ipfs/boxo v0.10.0 // indirect github.com/ipfs/go-cid v0.4.1 // indirect github.com/ipfs/go-datastore v0.6.0 // indirect - github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/ipld/go-ipld-prime v0.20.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect @@ -171,7 +172,6 @@ require ( go.uber.org/fx v1.20.1 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.15.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/term v0.18.0 // indirect diff --git a/installer/config.json.example b/installer/config.json.example index d695679067..b8701ccab3 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -30,7 +30,7 @@ "ConnectionsRateLimitingWindowSeconds": 1, "CrashDBDir": "", "DNSBootstrapID": ".algorand.network?backup=.algorand.net&dedup=.algorand-.(network|net)", - "DNSSecurityFlags": 1, + "DNSSecurityFlags": 9, "DeadlockDetection": 0, "DeadlockDetectionThreshold": 30, "DisableAPIAuth": false, diff --git a/logging/log.go b/logging/log.go index 48f83c1b9e..47a14ec722 100644 --- a/logging/log.go +++ b/logging/log.go @@ -148,6 +148,9 @@ type Logger interface { // source adds file, line and function fields to the event source() *logrus.Entry + // Entry returns the logrus raw entry + Entry() *logrus.Entry + // Adds a hook to the logger AddHook(hook logrus.Hook) @@ -319,6 +322,10 @@ func (l logger) SetJSONFormatter() { l.entry.Logger.Formatter = &logrus.JSONFormatter{TimestampFormat: "2006-01-02T15:04:05.000000Z07:00"} } +func (l logger) Entry() *logrus.Entry { + return l.entry +} + func (l logger) source() *logrus.Entry { event := l.entry diff --git a/netdeploy/remote/nodeConfig.go b/netdeploy/remote/nodeConfig.go index 2c6e0e423f..4880d76eb9 100644 --- a/netdeploy/remote/nodeConfig.go +++ b/netdeploy/remote/nodeConfig.go @@ -34,6 +34,7 @@ type NodeConfig struct { DashboardEndpoint string `json:",omitempty"` DeadlockOverride int `json:",omitempty"` // -1 = Disable deadlock detection, 0 = Use Default for build, 1 = Enable ConfigJSONOverride string `json:",omitempty"` // Raw json to merge into config.json after other modifications are complete + P2PBootstrap bool // True if this node should be a p2p bootstrap node and registered in DNS // NodeNameMatchRegex is tested against Name in generated configs and if matched the rest of the configs in this record are applied as a template NodeNameMatchRegex string `json:",omitempty"` diff --git a/netdeploy/remote/nodecfg/nodeConfigurator.go b/netdeploy/remote/nodecfg/nodeConfigurator.go index 5ab43d5ff7..842570bfc8 100644 --- a/netdeploy/remote/nodecfg/nodeConfigurator.go +++ b/netdeploy/remote/nodecfg/nodeConfigurator.go @@ -42,6 +42,7 @@ type nodeConfigurator struct { bootstrappedTrackerDir string relayEndpoints []srvEntry metricsEndpoints []srvEntry + p2pBootstrapEndpoints []txtEntry } type srvEntry struct { @@ -49,6 +50,11 @@ type srvEntry struct { port string } +type txtEntry struct { + netAddress string + peerID string +} + // ApplyConfigurationToHost attempts to apply the provided configuration to the local host, // based on the configuration specified for the provided hostName, with node // directories being created / updated under the specified rootNodeDir @@ -248,6 +254,31 @@ func (nc *nodeConfigurator) registerDNSRecords() (err error) { return } } + + dnsaddrsFrom := fmt.Sprintf("_dnsaddr.%s.algodev.network", nc.genesisData.Network) + for _, entry := range nc.p2pBootstrapEndpoints { + port, parseErr := strconv.ParseInt(strings.Split(entry.netAddress, ":")[1], 10, 64) + if parseErr != nil { + return parseErr + } + var addrType string + if isIP { + addrType = "ip4" + } else { + addrType = "dnsaddr" + } + addrInfoString := fmt.Sprintf("/%s/%s/tcp/%d/p2p/%s", addrType, nc.dnsName, port, entry.peerID) + to := fmt.Sprintf("dnsaddr=%s", addrInfoString) + + fmt.Fprintf(os.Stdout, "...... Adding P2P TXT Record '%s' -> '%s' .\n", dnsaddrsFrom, to) + const priority = 1 + const proxied = false + dnsErr := cloudflareDNS.CreateDNSRecord(context.Background(), "TXT", dnsaddrsFrom, to, cloudflare.AutomaticTTL, priority, proxied) + if dnsErr != nil { + return dnsErr + } + } + return } @@ -281,3 +312,7 @@ func (nc *nodeConfigurator) addRelaySrv(srvRecord string, port string) { func (nc *nodeConfigurator) registerMetricsSrv(srvRecord string, port string) { nc.metricsEndpoints = append(nc.metricsEndpoints, srvEntry{srvRecord, port}) } + +func (nc *nodeConfigurator) addP2PBootstrap(netAddress string, peerID string) { + nc.p2pBootstrapEndpoints = append(nc.p2pBootstrapEndpoints, txtEntry{netAddress, peerID}) +} diff --git a/netdeploy/remote/nodecfg/nodeDir.go b/netdeploy/remote/nodecfg/nodeDir.go index 9bd13343c1..26b1c9b8d1 100644 --- a/netdeploy/remote/nodecfg/nodeDir.go +++ b/netdeploy/remote/nodecfg/nodeDir.go @@ -18,6 +18,7 @@ package nodecfg import ( "encoding/json" + "errors" "fmt" "net/url" "os" @@ -27,6 +28,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/netdeploy/remote" + "github.com/algorand/go-algorand/network/p2p" "github.com/algorand/go-algorand/shared/algoh" "github.com/algorand/go-algorand/util/tokens" ) @@ -101,6 +103,12 @@ func (nd *nodeDir) configure() (err error) { fmt.Fprintf(os.Stdout, "Error during configureNetAddress: %s\n", err) return } + + if err = nd.configureP2PDNSBootstrap(nd.P2PBootstrap); err != nil { + fmt.Fprintf(os.Stdout, "Error during configureP2PDNSBootstrap: %s\n", err) + return + } + fmt.Println("Done configuring node directory.") return } @@ -156,6 +164,45 @@ func (nd *nodeDir) configureNetAddress() (err error) { return } +func (nd *nodeDir) configureP2PDNSBootstrap(p2pBootstrap bool) error { + if !p2pBootstrap { + return nil + } + fmt.Fprintf(os.Stdout, " - Configuring P2P DNS Bootstrap: %s\n", nd.Name) + if err := nd.ensureConfig(); err != nil { + return err + } + // ensure p2p config params set are what is expected: + // - EnableP2P or EnableP2PHybridMode + // - NetAddress or P2PListenAddress is set + // - EnableGossipService + if !nd.config.EnableP2P && !nd.config.EnableP2PHybridMode { + return errors.New("p2p bootstrap requires EnableP2P or EnableP2PHybridMode to be set") + } + if nd.NetAddress == "" && nd.config.P2PListenAddress == "" { + return errors.New("p2p bootstrap requires NetAddress or P2PListenAddress to be set") + } + if !nd.config.EnableGossipService { + return errors.New("p2p bootstrap requires EnableGossipService to be set") + } + + netAddress := nd.NetAddress + if nd.config.P2PListenAddress != "" { + netAddress = nd.config.P2PListenAddress + } + + key, err := p2p.GetPrivKey(config.Local{P2PPersistPeerID: true}, nd.dataDir) + if err != nil { + return err + } + peerID, err := p2p.PeerIDFromPublicKey(key.GetPublic()) + if err != nil { + return err + } + nd.configurator.addP2PBootstrap(netAddress, peerID.String()) + return nil +} + func (nd *nodeDir) configureAPIEndpoint(address string) (err error) { if err = nd.ensureConfig(); err != nil { return diff --git a/network/p2p/http.go b/network/p2p/http.go index d12b51034d..eea40c127e 100644 --- a/network/p2p/http.go +++ b/network/p2p/http.go @@ -21,6 +21,7 @@ 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" @@ -62,6 +63,7 @@ func MakeHTTPClient(addrInfo *peer.AddrInfo) (*http.Client, error) { if err != nil { return nil, err } + logging.Base().Debugf("MakeHTTPClient made a new P2P host %s for %s", clientStreamHost.ID(), addrInfo.String()) client := libp2phttp.Host{StreamHost: clientStreamHost} diff --git a/network/p2p/logger.go b/network/p2p/logger.go new file mode 100644 index 0000000000..8ac9dc7b97 --- /dev/null +++ b/network/p2p/logger.go @@ -0,0 +1,118 @@ +// Copyright (C) 2019-2024 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 . + +// This package implement a zap.Core in order to wrap lip2p logger into algod's logger. + +package p2p + +import ( + "runtime" + + p2plogging "github.com/ipfs/go-log/v2" + "github.com/sirupsen/logrus" + "go.uber.org/zap/zapcore" + + "github.com/algorand/go-algorand/logging" +) + +// var levelsMap = map[logging.Level]zapcore.Level{ +// logging.Debug: zapcore.DebugLevel, +// logging.Info: zapcore.InfoLevel, +// logging.Warn: zapcore.WarnLevel, +// logging.Error: zapcore.ErrorLevel, +// logging.Fatal: zapcore.FatalLevel, +// logging.Panic: zapcore.PanicLevel, +// } + +var levelsMap = map[zapcore.Level]logging.Level{ + zapcore.DebugLevel: logging.Debug, + zapcore.InfoLevel: logging.Info, + zapcore.WarnLevel: logging.Warn, + zapcore.ErrorLevel: logging.Error, + zapcore.FatalLevel: logging.Fatal, + zapcore.PanicLevel: logging.Panic, +} + +// loggingCore implements zapcore.Core +type loggingCore struct { + log logging.Logger + level logging.Level + fields []zapcore.Field + zapcore.Core +} + +// EnableP2PLogging enables libp2p logging into the provided logger with the provided level. +func EnableP2PLogging(log logging.Logger, l logging.Level) { + core := loggingCore{ + log: log, + level: l, + } + for p2pLevel, logLevel := range levelsMap { + if logLevel == l { + p2plogging.SetAllLoggers(p2plogging.LogLevel(p2pLevel)) + break + } + } + p2plogging.SetPrimaryCore(&core) +} + +func (c *loggingCore) Enabled(l zapcore.Level) bool { + return c.log.IsLevelEnabled(c.level) +} + +func (c *loggingCore) With(fields []zapcore.Field) zapcore.Core { + return &loggingCore{ + log: c.log, + level: c.level, + fields: append(c.fields, fields...), + } +} + +func (c *loggingCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if c.Enabled(e.Level) { + return ce.AddCore(e, c) + } + return ce +} + +func (c *loggingCore) Write(e zapcore.Entry, fields []zapcore.Field) error { + allFields := append(c.fields, fields...) + loggingFields := make(logging.Fields, len(allFields)) + + for _, f := range allFields { + if len(f.String) > 0 { + loggingFields[f.Key] = f.String + } else if f.Interface != nil { + loggingFields[f.Key] = f.Interface + } else { + loggingFields[f.Key] = f.Integer + } + } + event := c.log.WithFields(loggingFields).With("libp2p", e.LoggerName) + event = event.WithFields(logrus.Fields{ + "file": e.Caller.File, + "line": e.Caller.Line, + }) + if function := runtime.FuncForPC(e.Caller.PC); function != nil { + event = event.With("function", function.Name()) + } + event.Entry().Log(logrus.Level(levelsMap[e.Level]), e.Message) + return nil +} + +func (c *loggingCore) Sync() error { + return nil +} diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 9fd64e3aad..9ee9d3fa43 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -245,10 +245,17 @@ func (s *serviceImpl) IDSigner() *PeerIDChallengeSigner { func (s *serviceImpl) DialPeersUntilTargetCount(targetConnCount int) { ps := s.host.Peerstore().(*pstore.PeerStore) peerIDs := ps.GetAddresses(targetConnCount, phonebook.PhoneBookEntryRelayRole) + conns := s.host.Network().Conns() + var numOutgoingConns int + for _, conn := range conns { + if conn.Stat().Direction == network.DirOutbound { + numOutgoingConns++ + } + } for _, peerInfo := range peerIDs { peerInfo := peerInfo.(*peer.AddrInfo) // if we are at our target count stop trying to connect - if len(s.host.Network().Conns()) == targetConnCount { + if numOutgoingConns >= targetConnCount { return } // if we are already connected to this peer, skip it diff --git a/network/p2p/pubsub.go b/network/p2p/pubsub.go index 5d1c144632..8e67ac6725 100644 --- a/network/p2p/pubsub.go +++ b/network/p2p/pubsub.go @@ -93,7 +93,9 @@ func makePubSub(ctx context.Context, cfg config.Local, host host.Host) (*pubsub. pubsub.WithSubscriptionFilter(pubsub.WrapLimitSubscriptionFilter(pubsub.NewAllowlistSubscriptionFilter(TXTopicName), 100)), // pubsub.WithEventTracer(jsonTracer), pubsub.WithValidateQueueSize(256), + pubsub.WithMessageSignaturePolicy(pubsub.StrictNoSign), // pubsub.WithValidateThrottle(cfg.TxBacklogSize), + pubsub.WithValidateWorkers(20), // match to number wsNetwork workers } return pubsub.NewGossipSub(ctx, host, options...) diff --git a/network/p2p/streams.go b/network/p2p/streams.go index 0961141a0c..d16633adfd 100644 --- a/network/p2p/streams.go +++ b/network/p2p/streams.go @@ -75,7 +75,7 @@ func (n *streamManager) streamHandler(stream network.Stream) { n.streams[stream.Conn().RemotePeer()] = stream // streamHandler is supposed to be called for accepted streams, so we expect incoming here - incoming := stream.Stat().Direction == network.DirInbound + incoming := stream.Conn().Stat().Direction == network.DirInbound if !incoming { if stream.Stat().Direction == network.DirUnknown { n.log.Warnf("Unknown direction for a steam %s to/from %s", stream.ID(), remotePeer) @@ -93,7 +93,7 @@ func (n *streamManager) streamHandler(stream network.Stream) { // no old stream n.streams[stream.Conn().RemotePeer()] = stream // streamHandler is supposed to be called for accepted streams, so we expect incoming here - incoming := stream.Stat().Direction == network.DirInbound + incoming := stream.Conn().Stat().Direction == network.DirInbound if !incoming { if stream.Stat().Direction == network.DirUnknown { n.log.Warnf("streamHandler: unknown direction for a steam %s to/from %s", stream.ID(), remotePeer) @@ -142,7 +142,7 @@ func (n *streamManager) Connected(net network.Network, conn network.Conn) { stream, err := n.host.NewStream(n.ctx, remotePeer, AlgorandWsProtocol) if err != nil { - n.log.Infof("Failed to open stream to %s: %v", remotePeer, err) + n.log.Infof("Failed to open stream to %s (%s): %v", remotePeer, conn.RemoteMultiaddr().String(), err) return } n.streams[remotePeer] = stream @@ -153,7 +153,7 @@ func (n *streamManager) Connected(net network.Network, conn network.Conn) { n.streamsLock.Unlock() // a new stream created above, expected direction is outbound - incoming := stream.Stat().Direction == network.DirInbound + incoming := stream.Conn().Stat().Direction == network.DirInbound if incoming { n.log.Warnf("Unexpected incoming stream in streamHandler for connection %s (%s): %s vs %s stream", stream.Conn().ID(), remotePeer, stream.Conn().Stat().Direction, stream.Stat().Direction.String()) } else { diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 5eaf6ec36f..22da1915f3 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -18,6 +18,7 @@ package network import ( "context" + "math/rand" "net" "net/http" "strings" @@ -39,6 +40,7 @@ import ( pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/discovery/backoff" "github.com/multiformats/go-multiaddr" manet "github.com/multiformats/go-multiaddr/net" ) @@ -191,8 +193,9 @@ type p2pPeerStats struct { } type gossipSubPeer struct { - peerID peer.ID - net GossipNode + peerID peer.ID + net GossipNode + routingAddr [8]byte } func (p gossipSubPeer) GetNetwork() GossipNode { return p.net } @@ -206,6 +209,10 @@ func (p gossipSubPeer) OnClose(f func()) { } } +func (p gossipSubPeer) RoutingAddr() []byte { + return p.routingAddr[:] +} + // NewP2PNetwork returns an instance of GossipNode that uses the p2p.Service func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID, node NodeInfo) (*P2PNetwork, error) { const readBufferLen = 2048 @@ -255,6 +262,8 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo broadcastQueueBulk: make(chan broadcastRequest, 100), } + p2p.EnableP2PLogging(log, logging.Level(cfg.BaseLoggerDebugLevel)) + h, la, err := p2p.MakeHost(cfg, datadir, pstore) if err != nil { return nil, err @@ -270,7 +279,7 @@ func NewP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebo cfg: cfg, networkID: networkID, phonebookPeers: addrInfo, - resolveController: dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecuritySRVEnforced(), ""), + resolveController: dnsaddr.NewMultiaddrDNSResolveController(cfg.DNSSecurityTXTEnforced(), ""), log: net.log, } net.bootstrapperStart = bootstrapper.start @@ -393,12 +402,12 @@ func (n *P2PNetwork) innerStop() { } // meshThreadInner fetches nodes from DHT and attempts to connect to them -func (n *P2PNetwork) meshThreadInner() { +func (n *P2PNetwork) meshThreadInner() int { defer n.service.DialPeersUntilTargetCount(n.config.GossipFanout) // fetch peers from DNS var dnsPeers, dhtPeers []peer.AddrInfo - dnsPeers = dnsLookupBootstrapPeers(n.log, n.config, n.networkID, dnsaddr.NewMultiaddrDNSResolveController(n.config.DNSSecuritySRVEnforced(), "")) + dnsPeers = dnsLookupBootstrapPeers(n.log, n.config, n.networkID, dnsaddr.NewMultiaddrDNSResolveController(n.config.DNSSecurityTXTEnforced(), "")) // discover peers from DHT if n.capabilitiesDiscovery != nil { @@ -406,7 +415,6 @@ func (n *P2PNetwork) meshThreadInner() { dhtPeers, err = n.capabilitiesDiscovery.PeersForCapability(p2p.Gossip, n.config.GossipFanout) if err != nil { n.log.Warnf("Error getting relay nodes from capabilities discovery: %v", err) - return } n.log.Debugf("Discovered %d gossip peers from DHT", len(dhtPeers)) } @@ -416,21 +424,36 @@ func (n *P2PNetwork) meshThreadInner() { for i := range peers { replace = append(replace, &peers[i]) } - n.pstore.ReplacePeerList(replace, string(n.networkID), phonebook.PhoneBookEntryRelayRole) + if len(peers) > 0 { + n.pstore.ReplacePeerList(replace, string(n.networkID), phonebook.PhoneBookEntryRelayRole) + } + return len(peers) } func (n *P2PNetwork) meshThread() { defer n.wg.Done() + timer := time.NewTicker(1) // start immediately and reset after + + // Add exponential backoff with jitter to the mesh thread to handle new networks startup + // when no DNS or DHT peers are available. + // The parameters produce approximate the following delays (although they are random but the sequence give the idea): + // 2 2.4 4.6 9 20 19.5 28 24 14 14 35 60 60 + ebf := backoff.NewExponentialDecorrelatedJitter(2*time.Second, meshThreadInterval, 3.0, rand.NewSource(rand.Int63())) + eb := ebf() + defer timer.Stop() - var resetTimer bool for { select { case <-timer.C: - n.meshThreadInner() - if !resetTimer { + numPeers := n.meshThreadInner() + if numPeers > 0 { + // found something, reset timer to the default value timer.Reset(meshThreadInterval) - resetTimer = true + eb.Reset() + } else { + // no peers found, backoff + timer.Reset(eb.Delay()) } case <-n.ctx.Done(): return @@ -717,9 +740,9 @@ func (n *P2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn Deadl // wsStreamHandler is a callback that the p2p package calls when a new peer connects and establishes a // stream for the websocket protocol. -func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, stream network.Stream, incoming bool) { +func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2pPeer peer.ID, stream network.Stream, incoming bool) { if stream.Protocol() != p2p.AlgorandWsProtocol { - n.log.Warnf("unknown protocol %s", stream.Protocol()) + n.log.Warnf("unknown protocol %s from peer%s", stream.Protocol(), p2pPeer) return } @@ -727,7 +750,7 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea var initMsg [1]byte rn, err := stream.Read(initMsg[:]) if rn == 0 || err != nil { - n.log.Warnf("wsStreamHandler: error reading initial message: %s", err) + n.log.Warnf("wsStreamHandler: error reading initial message: %s, peer %s (%s)", err, p2pPeer, stream.Conn().RemoteMultiaddr().String()) return } } else { @@ -743,7 +766,7 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea if numOutgoingPeers >= n.config.GossipFanout { // this appears to be some auxiliary connection made by libp2p itself like DHT connection. // skip this connection since there are already enough peers - n.log.Debugf("skipping outgoing connection to peer %s: num outgoing %d > fanout %d ", p2ppeer, numOutgoingPeers, n.config.GossipFanout) + n.log.Debugf("skipping outgoing connection to peer %s: num outgoing %d > fanout %d ", p2pPeer, numOutgoingPeers, n.config.GossipFanout) stream.Close() return } @@ -759,11 +782,11 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea ma := stream.Conn().RemoteMultiaddr() addr := ma.String() if addr == "" { - n.log.Warnf("Could not get address for peer %s", p2ppeer) + n.log.Warnf("Could not get address for peer %s", p2pPeer) } // create a wsPeer for this stream and added it to the peers map. - addrInfo := &peer.AddrInfo{ID: p2ppeer, Addrs: []multiaddr.Multiaddr{ma}} + addrInfo := &peer.AddrInfo{ID: p2pPeer, Addrs: []multiaddr.Multiaddr{ma}} maxIdleConnsPerHost := int(n.config.ConnectionsRateLimitingCount) client, err := p2p.MakeHTTPClientWithRateLimit(addrInfo, n.pstore, limitcaller.DefaultQueueingTimeout, maxIdleConnsPerHost) if err != nil { @@ -775,16 +798,16 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea conn: &wsPeerConnP2PImpl{stream: stream}, outgoing: !incoming, } - protos, err := n.pstore.GetProtocols(p2ppeer) + protos, err := n.pstore.GetProtocols(p2pPeer) if err != nil { - n.log.Warnf("Error getting protocols for peer %s: %v", p2ppeer, err) + n.log.Warnf("Error getting protocols for peer %s: %v", p2pPeer, err) } wsp.TelemetryGUID, wsp.InstanceName = p2p.GetPeerTelemetryInfo(protos) wsp.init(n.config, outgoingMessagesBufferSize) n.wsPeersLock.Lock() - n.wsPeers[p2ppeer] = wsp - n.wsPeersToIDs[wsp] = p2ppeer + n.wsPeers[p2pPeer] = wsp + n.wsPeersToIDs[wsp] = p2pPeer n.wsPeersLock.Unlock() n.wsPeersChangeCounter.Add(1) @@ -794,8 +817,11 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2ppeer peer.ID, strea event = "ConnectedIn" msg = "Accepted incoming connection from peer %s" } - localAddr, _ := n.Address() - n.log.With("event", event).With("remote", addr).With("local", localAddr).Infof(msg, p2ppeer.String()) + localAddr, has := n.Address() + if !has { + n.log.Warn("Could not get local address") + } + n.log.With("event", event).With("remote", addr).With("local", localAddr).Infof(msg, p2pPeer.String()) if n.log.GetLevel() >= logging.Debug { n.log.Debugf("streams for %s conn %s ", stream.Conn().Stat().Direction.String(), stream.Conn().ID()) @@ -912,8 +938,18 @@ func (n *P2PNetwork) txTopicHandleLoop() { // txTopicValidator calls txHandler to validate and process incoming transactions. func (n *P2PNetwork) txTopicValidator(ctx context.Context, peerID peer.ID, msg *pubsub.Message) pubsub.ValidationResult { + var routingAddr [8]byte + n.wsPeersLock.Lock() + if wsp, ok := n.wsPeers[peerID]; ok { + copy(routingAddr[:], wsp.RoutingAddr()) + } else { + // well, otherwise use last 8 bytes of peerID + copy(routingAddr[:], peerID[len(peerID)-8:]) + } + n.wsPeersLock.Unlock() + inmsg := IncomingMessage{ - Sender: gossipSubPeer{peerID: msg.ReceivedFrom, net: n}, + Sender: gossipSubPeer{peerID: msg.ReceivedFrom, net: n, routingAddr: routingAddr}, Tag: protocol.TxnTag, Data: msg.Data, Net: n, diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 7cc590ff02..0206871a54 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -36,6 +36,7 @@ import ( "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" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/crypto" @@ -1106,3 +1107,22 @@ func TestMergeP2PAddrInfoResolvedAddresses(t *testing.T) { }) } } + +// TestP2PGossipSubPeerCasts checks that gossipSubPeer implements the ErlClient and IPAddressable interfaces +// needed by TxHandler +func TestP2PGossipSubPeerCasts(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + var g interface{} = gossipSubPeer{} + _, ok := g.(util.ErlClient) + require.True(t, ok) + + _, ok = g.(IPAddressable) + require.True(t, ok) + + // check that gossipSubPeer is hashable as ERL wants + var m map[util.ErlClient]struct{} + require.Equal(t, m[gossipSubPeer{}], struct{}{}) + require.Equal(t, m[g.(util.ErlClient)], struct{}{}) +} diff --git a/network/websocketProxy_test.go b/network/websocketProxy_test.go index cefe4b687a..96628acb69 100644 --- a/network/websocketProxy_test.go +++ b/network/websocketProxy_test.go @@ -317,7 +317,7 @@ func TestWebsocketProxyWsNet(t *testing.T) { peerB := netA.peers[0] require.NotEmpty(t, peerB.originAddress) require.Equal(t, fakeXForwardedFor, peerB.originAddress) - require.NotEqual(t, peerB.RoutingAddr(), peerB.IPAddr()) + require.NotEqual(t, peerB.RoutingAddr(), peerB.ipAddr()) fakeXForwardedForParsed := net.ParseIP(fakeXForwardedFor) require.NotEqual(t, fakeXForwardedForParsed, peerB.RoutingAddr()) } diff --git a/network/wsPeer.go b/network/wsPeer.go index 7606168687..2b302f071f 100644 --- a/network/wsPeer.go +++ b/network/wsPeer.go @@ -329,7 +329,6 @@ type HTTPPeer interface { // IPAddressable is addressable with either IPv4 or IPv6 address type IPAddressable interface { - IPAddr() []byte RoutingAddr() []byte } @@ -385,7 +384,7 @@ func (wp *wsPeer) Version() string { return wp.version } -func (wp *wsPeer) IPAddr() []byte { +func (wp *wsPeer) ipAddr() []byte { remote := wp.conn.RemoteAddr() if remote == nil { return nil @@ -420,7 +419,7 @@ func (wp *wsPeer) RoutingAddr() []byte { if wp.wsPeerCore.originAddress != "" { ip = net.ParseIP(wp.wsPeerCore.originAddress) } else { - ip = wp.IPAddr() + ip = wp.ipAddr() } if len(ip) != net.IPv6len { diff --git a/network/wsPeer_test.go b/network/wsPeer_test.go index d1f32302a0..973c027b16 100644 --- a/network/wsPeer_test.go +++ b/network/wsPeer_test.go @@ -288,32 +288,32 @@ func TestWsPeerIPAddr(t *testing.T) { } // some raw IPv4 address conn.addr.IP = []byte{127, 0, 0, 1} - require.Equal(t, []byte{127, 0, 0, 1}, peer.IPAddr()) + require.Equal(t, []byte{127, 0, 0, 1}, peer.ipAddr()) require.Equal(t, []byte{127, 0, 0, 1}, peer.RoutingAddr()) // IPv4 constructed from net.IPv4 conn.addr.IP = net.IPv4(127, 0, 0, 2) - require.Equal(t, []byte{127, 0, 0, 2}, peer.IPAddr()) + require.Equal(t, []byte{127, 0, 0, 2}, peer.ipAddr()) require.Equal(t, []byte{127, 0, 0, 2}, peer.RoutingAddr()) // some IPv6 address conn.addr.IP = net.IPv6linklocalallrouters - require.Equal(t, []byte(net.IPv6linklocalallrouters), peer.IPAddr()) + require.Equal(t, []byte(net.IPv6linklocalallrouters), peer.ipAddr()) require.Equal(t, []byte(net.IPv6linklocalallrouters[0:8]), peer.RoutingAddr()) // embedded IPv4 into IPv6 conn.addr.IP = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 3} require.Equal(t, 16, len(conn.addr.IP)) - require.Equal(t, []byte{127, 0, 0, 3}, peer.IPAddr()) + require.Equal(t, []byte{127, 0, 0, 3}, peer.ipAddr()) require.Equal(t, []byte{127, 0, 0, 3}, peer.RoutingAddr()) conn.addr.IP = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 4} require.Equal(t, 16, len(conn.addr.IP)) - require.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 4}, peer.IPAddr()) + require.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 4}, peer.ipAddr()) require.Equal(t, []byte{127, 0, 0, 4}, peer.RoutingAddr()) // check incoming peer with originAddress set conn.addr.IP = []byte{127, 0, 0, 1} peer.wsPeerCore.originAddress = "127.0.0.2" - require.Equal(t, []byte{127, 0, 0, 1}, peer.IPAddr()) + require.Equal(t, []byte{127, 0, 0, 1}, peer.ipAddr()) require.Equal(t, []byte{127, 0, 0, 2}, peer.RoutingAddr()) } diff --git a/node/node.go b/node/node.go index 206d2fa087..bfb32ba3ff 100644 --- a/node/node.go +++ b/node/node.go @@ -199,14 +199,13 @@ func MakeFull(log logging.Logger, rootDir string, cfg config.Local, phonebookAdd // tie network, block fetcher, and agreement services together var p2pNode network.GossipNode if cfg.EnableP2PHybridMode { - p2pNode, err = network.NewHybridP2PNetwork(node.log, node.config, node.genesisDirs.RootGenesisDir, phonebookAddresses, genesis.ID(), genesis.Network, node) + p2pNode, err = network.NewHybridP2PNetwork(node.log, node.config, rootDir, phonebookAddresses, genesis.ID(), genesis.Network, node) if err != nil { log.Errorf("could not create hybrid p2p node: %v", err) return nil, err } } else if cfg.EnableP2P { - // TODO: pass more appropriate genesisDir (hot/cold). Presently this is just used to store a peerID key. - p2pNode, err = network.NewP2PNetwork(node.log, node.config, node.genesisDirs.RootGenesisDir, phonebookAddresses, genesis.ID(), genesis.Network, node) + p2pNode, err = network.NewP2PNetwork(node.log, node.config, rootDir, phonebookAddresses, genesis.ID(), genesis.Network, node) if err != nil { log.Errorf("could not create p2p node: %v", err) return nil, err diff --git a/node/node_test.go b/node/node_test.go index d4e2a08ebd..361f3478f7 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -891,9 +891,7 @@ func TestNodeHybridTopology(t *testing.T) { cfg.EnableP2PHybridMode = true cfg.EnableDHTProviders = true cfg.P2PPersistPeerID = true - genesisDirs, err := cfg.EnsureAndResolveGenesisDirs(ni.rootDir, ni.genesis.ID(), nil) - require.NoError(t, err) - privKey, err := p2p.GetPrivKey(cfg, genesisDirs.RootGenesisDir) + privKey, err := p2p.GetPrivKey(cfg, ni.rootDir) require.NoError(t, err) ni.p2pID, err = p2p.PeerIDFromPublicKey(privKey.GetPublic()) require.NoError(t, err) @@ -984,9 +982,7 @@ func TestNodeP2PRelays(t *testing.T) { cfg.EnableDHTProviders = true cfg.P2PPersistPeerID = true - genesisDirs, err := cfg.EnsureAndResolveGenesisDirs(ni.rootDir, ni.genesis.ID(), nil) - require.NoError(t, err) - privKey, err := p2p.GetPrivKey(cfg, genesisDirs.RootGenesisDir) + privKey, err := p2p.GetPrivKey(cfg, ni.rootDir) require.NoError(t, err) ni.p2pID, err = p2p.PeerIDFromPublicKey(privKey.GetPublic()) require.NoError(t, err) diff --git a/test/heapwatch/agreement-log.py b/test/heapwatch/agreement-log.py new file mode 100644 index 0000000000..4109b37a71 --- /dev/null +++ b/test/heapwatch/agreement-log.py @@ -0,0 +1,187 @@ +""" +Agreement logs parser, takes either separate node.log files from a directory and guessing names from the file names, +or parses the e2e test failure log file watching for node names as " libgoalFixture.go:376: Relay0/node.log:" strings. + +This tool similar a bit to carpenter but takes multiple log files at once. +To force colors when outputting to a file, set FORCE_COLOR=1 in the environment. +""" + +import argparse +from datetime import datetime, timedelta +import glob +import json +import logging +import os +import time + +from termcolor import COLORS, colored + +logger = logging.getLogger(__name__) + +filtered_events = frozenset(['Persisted']) + +def process_json_line(line: str, node_name: str, by_node: dict, events: list): + """Handles a single line of json log file, returns parsed event or None if it's not an agreement event. + + line is a single line of json log file. + node_name is a name of the node that produced this line. + by_node is dict with unique nodes meta information. + events is a list of all parsed events. It is appended in this function to keep the caller code clean. + """ + try: + evt = json.loads(line) + except json.JSONDecodeError: + logger.error('failed to parse json: %s', line) + return None + if evt.get('Context') == 'Agreement' and evt.get('Type'): + if evt['Type'] in filtered_events: + return None + dt = datetime.strptime(evt['time'], '%Y-%m-%dT%H:%M:%S.%f%z') + sender = evt.get('Sender') + sender = sender[:12] if sender else '' + h = evt.get('Hash') + h = h[:8] if h else '' + w = evt.get('Weight', '-') if not evt['Type'].startswith('Proposal') else ' ' + wt = evt.get('WeightTotal', '-') if not evt['Type'].startswith('Proposal') else ' ' + if evt['Type'] in ('StepTimeout', 'VoteAttest', 'BlockAssembled', 'BlockPipelined'): + w, wt = ' ', ' ' + result = { + 'time': dt, + 'type': evt.get('Type'), + 'round': evt.get('Round', '-'), + 'period': evt.get('Period', '-'), + 'step': evt.get('Step', '-'), + 'object_round': evt.get('ObjectRound', '-'), + 'object_period': evt.get('ObjectPeriod', '-'), + 'object_step': evt.get('ObjectStep', '-'), + 'hash': h, + 'sender': sender, + 'weight': w, + 'weight_total': wt, + 'node': node_name, + } + events.append(result) + metadata = by_node.get(node_name) + if not metadata: + metadata = { + 'type': evt.get('Type'), + 'time': dt + } + by_node[node_name] = metadata + else: + if evt.get('Type') == 'RoundConcluded': + rt = dt - metadata['time'] + result['round_time_ms'] = rt / timedelta(milliseconds=1) + elif evt.get('Type') == 'RoundStart': + metadata['time'] = dt + metadata['type'] = 'RoundStart' + by_node[node_name] = metadata + + return result + return None + +def main(): + os.environ['TZ'] = 'UTC' + time.tzset() + + ap = argparse.ArgumentParser() + ap.add_argument('test_log_or_dir', help='Dir with log files or a single log file from e2e tests') + ap.add_argument('-e', '--end-round', type=int, help=f'Round to end at') + args = ap.parse_args() + + by_node = {} + events = [] + if os.path.isdir(args.test_log_or_dir): + logger.info('processing directory %s', args.test_log_or_dir) + log_files = sorted(glob.glob(os.path.join(args.test_log_or_dir, '*-node.log'))) + if not log_files: + logger.error('no log files found in %s', args.test_log_or_dir) + return 1 + for filename in os.listdir(args.test_log_or_dir): + if filename.endswith("-node.log"): + with open(os.path.join(args.test_log_or_dir, filename), 'r') as file: + node_name = filename[:len(filename) - len('-node.log')] + node_name = node_name.replace('relay', 'R') + node_name = node_name.replace('nonParticipatingNode', 'NPN') + node_name = node_name.replace('node', 'N') + for line in file: + event = process_json_line(line, node_name, by_node, events) + if event and args.end_round and \ + isinstance(event['round'], int) and event['round'] >= args.end_round: + break + + else: + logger.info('processing file %s', args.test_log_or_dir) + with open(args.test_log_or_dir, 'r') as file: + line0 = None + while not line0: + line0 = file.readline() + line0 = line0.strip() + + if line0[0] == '{': + # regular json line + node_name = 'node' + process_json_line(line, node_name, by_node, events) + for line in file: + line = line.strip() + event = process_json_line(line, node_name, by_node, events) + if event and args.end_round and \ + isinstance(event['round'], int) and event['round'] >= args.end_round: + break + else: + # looks like e2e test output with lines line this: + """ + libgoalFixture.go:374: ===================... + libgoalFixture.go:376: Relay0/node.log: + libgoalFixture.go:379: {"file":"server.go"... + """ + node_name = None + if line0.endswith('node.log:'): + node_name = line0.split(' ')[1].split('/')[0] + logger.info('found node name: %s', node_name) + for line in file: + line = line.strip() + if line.endswith('node.log:'): + node_name = line.split(' ')[1].split('/')[0] + logger.info('found node name: %s', node_name) + if node_name: + for line in file: + json_start = line.find('{') + if json_start == -1: + # end of continuous json block + node_name = None + break + line = line[json_start:] + event = process_json_line(line, node_name, by_node, events) + if event and args.end_round and \ + isinstance(event['round'], int) and event['round'] >= args.end_round: + break + + log = sorted(events, key=lambda x: x['time']) + + # num_nodes = len(by_node) + colors = list(COLORS) + colors = colors[colors.index('light_grey'):] + if len(colors) < len(by_node): + colors = colors * (len(by_node) // len(colors) + 1) + node_color = {k: v for k, v in zip(by_node.keys(), colors)} + + fmt = '%15s (%s,%s,%s) (%s,%s,%s) %4s|%-4s %-8s %-18s %8s %12s %5s' + print(fmt % ('TS', 'R', 'P', 'S', 'r', 'p', 's', 'W', 'WT', 'NODE', 'EVENT TYPE', 'HASH', 'SENDER', 'RT ms')) + for e in log: + color = node_color[e['node']] + text = colored(fmt % ( + e['time'].strftime('%H:%M:%S.%f'), + e['round'], e['period'], e['step'], + e['object_round'], e['object_period'], e['object_step'], + e['weight'], e['weight_total'], + e['node'][:8], + e['type'], e['hash'], e['sender'], + int(e['round_time_ms']) if 'round_time_ms' in e else ''), + color, + ) + print(text) + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + main() diff --git a/test/heapwatch/block_history_plot.py b/test/heapwatch/block_history_plot.py index 7de45e21b0..d8c86b454f 100644 --- a/test/heapwatch/block_history_plot.py +++ b/test/heapwatch/block_history_plot.py @@ -138,19 +138,21 @@ def process(path, args): min(tpsv[start:end]), max(tpsv[start:end]), )) print('long round times: {}'.format(' '.join(list(map(str,filter(lambda x: x >= 9,dtv[start:end])))))) - fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2,2) - ax1.set_title('round time (seconds)') + fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(2,3, figsize=(10, 5)) + ax1.set_title('round time histogram (sec)') ax1.hist(list(filter(lambda x: x < 9,dtv[start:end])),bins=20) - if args.rtime: - ax2.set_title('round time') - ax2.plot(dtv) - else: - ax2.set_title('TPS') - ax2.hist(tpsv[start:end],bins=20) + ax4.set_title('round time') + ax4.plot(dtv[start:end]) + + ax2.set_title('txn/block histogram') + ax2.hist(txnv[start:end],bins=20) + + ax5.set_title('txn/block') + ax5.plot(txnv[start:end]) - ax3.set_title('txn/block') - ax3.hist(txnv[start:end],bins=20) + ax3.set_title('TPS') + ax3.hist(tpsv[start:end],bins=20) # 10 round moving average TPS tpsv10 = [] @@ -165,12 +167,12 @@ def process(path, args): dtxn = tca-tc0 tpsv10.append(dtxn/dt) if args.tps1: - ax4.set_title('TPS') - ax4.plot(tpsv[start:end]) + ax6.set_title('TPS') + ax6.plot(tpsv[start:end]) print('fullish block sizes: {}'.format(list(filter(lambda x: x > 100, txnv)))) else: - ax4.set_title('TPS(10 round window)') - ax4.plot(tpsv10) + ax6.set_title('TPS(10 round window)') + ax6.plot(tpsv10) fig.tight_layout() plt.savefig(path + '_hist.svg', format='svg') plt.savefig(path + '_hist.png', format='png') diff --git a/test/heapwatch/client_ram_report.py b/test/heapwatch/client_ram_report.py index 97a1171630..f16fbeaa3f 100644 --- a/test/heapwatch/client_ram_report.py +++ b/test/heapwatch/client_ram_report.py @@ -202,6 +202,10 @@ def main(): heap_totals = get_heap_inuse_totals(args.dir) heap_details = get_heap_metrics(args.dir) + if not heap_totals and not heap_details: + print('no data found', file=sys.stderr) + return 0 + if args.csv: if args.csv == '-': csvf = sys.stdout diff --git a/test/heapwatch/metrics_aggs.py b/test/heapwatch/metrics_aggs.py new file mode 100644 index 0000000000..0189634be5 --- /dev/null +++ b/test/heapwatch/metrics_aggs.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# Copyright (C) 2019-2024 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 . +# +### +# +# Process and aggregate /metrics data captured by heapWatch.py +# Useful for metrics with labels and bandwidth analysis. +# +import argparse +import glob +import logging +import os +import time +import sys + +import dash +from dash import dcc, html +import plotly.graph_objs as go +from plotly.subplots import make_subplots + + +from metrics_lib import MetricType, parse_metrics, gather_metrics_files_by_nick + +logger = logging.getLogger(__name__) + + +def main(): + os.environ['TZ'] = 'UTC' + time.tzset() + default_img_filename = 'metrics_aggs.png' + default_html_filename = 'metrics_aggs.html' + + ap = argparse.ArgumentParser() + ap.add_argument('metrics_names', nargs='+', default=None, help='metric name(s) to track') + ap.add_argument('-d', '--dir', type=str, default=None, help='dir path to find /*.metrics in') + ap.add_argument('-l', '--list-nodes', default=False, action='store_true', help='list available node names with metrics') + ap.add_argument('-t', '--tags', action='append', default=[], help='tag/label pairs in a=b format to aggregate by, may be repeated. Empty means aggregation by metric name') + ap.add_argument('--nick-re', action='append', default=[], help='regexp to filter node names, may be repeated') + ap.add_argument('--nick-lre', action='append', default=[], help='label:regexp to filter node names, may be repeated') + ap.add_argument('-s', '--save', type=str, choices=['png', 'html'], help=f'save plot to \'{default_img_filename}\' or \'{default_html_filename}\' file instead of showing it') + ap.add_argument('--verbose', default=False, action='store_true') + + args = ap.parse_args() + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + tags = {} + if args.tags: + for tag in args.tags: + if '=' not in tag: + raise (f'Invalid tag: {tag}') + k, v = tag.split('=', 1) + tags[k] = v + tag_keys = set(tags.keys()) + + metrics_files = sorted(glob.glob(os.path.join(args.dir, '*.metrics'))) + metrics_files.extend(glob.glob(os.path.join(args.dir, 'terraform-inventory.host'))) + filesByNick = gather_metrics_files_by_nick(metrics_files, args.nick_re, args.nick_lre) + + if args.list_nodes: + print('Available nodes:', ', '.join(sorted(filesByNick.keys()))) + return 0 + + app = dash.Dash(__name__) + app.layout = html.Div( + html.Div([ + html.H4('Algod Metrics'), + html.Div(id='text'), + dcc.Graph(id='graph'), + ]) + ) + metrics_names = set(args.metrics_names) + nrows = len(metrics_names) + + fig = make_subplots( + rows=nrows, cols=1, + vertical_spacing=0.03, shared_xaxes=True, + subplot_titles=[f'{name}' for name in sorted(metrics_names)], + ) + + fig['layout']['margin'] = { + 'l': 30, 'r': 10, 'b': 10, 't': 20 + } + fig['layout']['height'] = 500 * nrows + + + for nick, files_by_date in filesByNick.items(): + active_metrics = {} + data = {'time': []} + raw_series = {} + raw_times = {} + idx = 0 + for dt, metrics_file in files_by_date.items(): + data['time'].append(dt) + with open(metrics_file, 'rt') as f: + metrics = parse_metrics(f, nick, metrics_names) + for metric_name, metrics_seq in metrics.items(): + active_metric_names = [] + raw_value = 0 + for metric in metrics_seq: + if metric.type != MetricType.COUNTER: + raise RuntimeError('Only COUNT metrics are supported') + if tags is None or tags is not None and metric.has_tags(tag_keys, tags): + raw_value += metric.value + full_name = metric.string(set(tag_keys).union({'n'})) + + if full_name is None: + continue + + if full_name not in data: + # handle gaps in data, sometimes metric file might miss a value + # but the chart requires matching x and y series (time and metric value) + # data is what does into the chart, and raw_series is used to calculate + data[full_name] = [0] * len(files_by_date) + raw_series[full_name] = [] + raw_times[full_name] = [] + + metric_value = raw_value + if len(raw_series[full_name]) > 0 and len(raw_times[full_name]) > 0: + metric_value = (metric_value - raw_series[full_name][-1]) / (dt - raw_times[full_name][-1]).total_seconds() + else: + metric_value = 0 + + data[full_name][idx] = metric_value + raw_series[full_name].append(raw_value) + raw_times[full_name].append(dt) + + active_metric_names.append(full_name) + + active_metric_names.sort() + active_metrics[full_name] = active_metric_names + idx += 1 + + for i, metric_pair in enumerate(sorted(active_metrics.items())): + metric_name, metric_fullnames = metric_pair + for metric_fullname in metric_fullnames: + fig.append_trace(go.Scatter( + x=data['time'], + y=data[metric_fullname], + name=metric_fullname, + mode='lines+markers', + line=dict(width=1), + ), i+1, 1) + + if args.save: + if args.save == 'html': + target_path = os.path.join(args.dir, default_html_filename) + fig.write_html(target_path) + else: + target_path = os.path.join(args.dir, default_img_filename) + fig.write_image(target_path) + print(f'Saved plot to {target_path}') + else: + fig.show() + + return 0 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/test/heapwatch/metrics_delta.py b/test/heapwatch/metrics_delta.py index 50b1e9e2e3..2d64ee097a 100644 --- a/test/heapwatch/metrics_delta.py +++ b/test/heapwatch/metrics_delta.py @@ -22,7 +22,6 @@ # Generate text report on bandwidth in and out of relays/PN/NPN import argparse -import configparser import contextlib import csv import glob @@ -36,42 +35,10 @@ import sys import time -logger = logging.getLogger(__name__) +from metrics_lib import num, hunum, terraform_inventory_ip_not_names, \ + metric_line_re, test_metric_line_re -def num(x): - if '.' in x: - return float(x) - return int(x) - -def hunum(x): - if x >= 10000000000: - return '{:.1f}G'.format(x / 1000000000.0) - if x >= 1000000000: - return '{:.2f}G'.format(x / 1000000000.0) - if x >= 10000000: - return '{:.1f}M'.format(x / 1000000.0) - if x >= 1000000: - return '{:.2f}M'.format(x / 1000000.0) - if x >= 10000: - return '{:.1f}k'.format(x / 1000.0) - if x >= 1000: - return '{:.2f}k'.format(x / 1000.0) - return '{:.2f}x'.format(x) - -metric_line_re = re.compile(r'(\S+\{[^}]*\})\s+(.*)') - -def test_metric_line_re(): - testlines = ( - ('algod_network_connections_dropped_total{reason="write err"} 1', 1), - #('algod_network_sent_bytes_MS 274992', 274992), # handled by split - ) - for line, n in testlines: - try: - m = metric_line_re.match(line) - assert int(m.group(2)) == n - except: - logger.error('failed on line %r', line, exc_info=True) - raise +logger = logging.getLogger(__name__) def parse_metrics(fin): out = dict() @@ -86,10 +53,15 @@ def parse_metrics(fin): continue m = metric_line_re.match(line) if m: - out[m.group(1)] = num(m.group(2)) + key = m.group(1) + val = m.group(2) else: ab = line.split() - out[ab[0]] = num(ab[1]) + key = ab[0] + val = ab[1] + if key.endswith('{}'): + key = key[:-2] + out[key] = num(val) except: print(f'An exception occurred in parse_metrics: {sys.exc_info()}') pass @@ -371,21 +343,6 @@ def process_nick_re(nre, filesByNick, nick_to_tfname, rsum, args, grsum): 'npn': (.7,.7,0), } -def terraform_inventory_ip_not_names(tf_inventory_path): - """return ip to nickname mapping""" - tf_inventory = configparser.ConfigParser(allow_no_value=True) - tf_inventory.read(tf_inventory_path) - ip_to_name = {} - for k, sub in tf_inventory.items(): - if k.startswith('name_'): - for ip in sub: - if ip in ip_to_name: - logger.warning('ip %r already named %r, also got %r', ip, ip_to_name[ip], k) - ip_to_name[ip] = k - #logger.debug('names: %r', sorted(ip_to_name.values())) - #logger.debug('ip to name %r', ip_to_name) - return ip_to_name - def main(): os.environ['TZ'] = 'UTC' time.tzset() @@ -541,7 +498,7 @@ def __init__(self): self.txPLists = {} self.txPSums = {} self.times = [] - # algod_tx_pool_count{} + # algod_tx_pool_count self.txPool = [] # objectBytes = [(curtime, algod_go_memory_classes_heap_objects_bytes), ...] self.objectBytes = [] @@ -601,13 +558,13 @@ def process_files(self, args, nick=None, metrics_files=None, bisource=None): bi = bisource.get(curtime) if bi is None: logger.warning('%s no blockinfo', path) - self.txPool.append(cur.get('algod_tx_pool_count{}')) + self.txPool.append(cur.get('algod_tx_pool_count')) objectBytes = cur.get('algod_go_memory_classes_heap_objects_bytes') if objectBytes: self.objectBytes.append((curtime, objectBytes)) #logger.debug('%s: %r', path, cur) - verifyGood = cur.get('algod_agreement_proposal_verify_good{}') - verifyMs = cur.get('algod_agreement_proposal_verify_ms{}') + verifyGood = cur.get('algod_agreement_proposal_verify_good') + verifyMs = cur.get('algod_agreement_proposal_verify_ms') if verifyGood and verifyMs: # last writer wins self.verifyMillis = verifyMs / verifyGood @@ -626,8 +583,8 @@ def process_files(self, args, nick=None, metrics_files=None, bisource=None): rounds = (bi.get('block',{}).get('rnd', 0) - prevbi.get('block',{}).get('rnd', 0)) if rounds != 0: blocktime = dt/rounds - txBytes = d.get('algod_network_sent_bytes_total{}',0) - rxBytes = d.get('algod_network_received_bytes_total{}',0) + txBytes = d.get('algod_network_sent_bytes_total',0) + rxBytes = d.get('algod_network_received_bytes_total',0) txBytesPerSec = txBytes / dt rxBytesPerSec = rxBytes / dt # TODO: gather algod_network_sent_bytes_* and algod_network_received_bytes_* diff --git a/test/heapwatch/metrics_lib.py b/test/heapwatch/metrics_lib.py new file mode 100644 index 0000000000..fbda555b90 --- /dev/null +++ b/test/heapwatch/metrics_lib.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +# Copyright (C) 2019-2024 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 . +# +### +# +# Common functions for processing /metrics data captured by heapWatch.py +# +import configparser +from datetime import datetime +from enum import Enum +import logging +import os +import re +import sys +from typing import Dict, Iterable, List, Optional, Tuple, Union +from urllib.parse import urlparse + + +from client_ram_report import dapp + + +logger = logging.getLogger(__name__) +metric_line_re = re.compile(r'(\S+\{[^}]*\})\s+(.*)') + +def num(x): + if '.' in x: + return float(x) + return int(x) + +def hunum(x): + if x >= 10000000000: + return '{:.1f}G'.format(x / 1000000000.0) + if x >= 1000000000: + return '{:.2f}G'.format(x / 1000000000.0) + if x >= 10000000: + return '{:.1f}M'.format(x / 1000000.0) + if x >= 1000000: + return '{:.2f}M'.format(x / 1000000.0) + if x >= 10000: + return '{:.1f}k'.format(x / 1000.0) + if x >= 1000: + return '{:.2f}k'.format(x / 1000.0) + return '{:.2f}x'.format(x) + + +def test_metric_line_re(): + testlines = ( + ('algod_network_connections_dropped_total{reason="write err"} 1', 1), + #('algod_network_sent_bytes_MS 274992', 274992), # handled by split + ) + for line, n in testlines: + try: + m = metric_line_re.match(line) + assert int(m.group(2)) == n + except: + print('failed on line %r', line) + raise + +def terraform_inventory_ip_not_names(tf_inventory_path): + """return ip to nickname mapping""" + tf_inventory = configparser.ConfigParser(allow_no_value=True) + tf_inventory.read(tf_inventory_path) + ip_to_name = {} + for k, sub in tf_inventory.items(): + if k.startswith('name_'): + for ip in sub: + if ip in ip_to_name: + logger.warning('ip %r already named %r, also got %r', ip, ip_to_name[ip], k) + ip_to_name[ip] = k + #logger.debug('names: %r', sorted(ip_to_name.values())) + #logger.debug('ip to name %r', ip_to_name) + return ip_to_name + +metrics_fname_re = re.compile(r'(.*?)\.(\d+_\d+)\.metrics') + +def gather_metrics_files_by_nick( + metrics_files: Iterable[str], nick_res: List[str], nick_lres: List[str] +) -> Dict[str, Dict[datetime, str]]: + """return {"node nickname": {datetime: path, ...}, ...}} + after resolving ip addresses into nodes nick names and applying nick_re and nick_lre filters. + """ + filesByNick = {} + tf_inventory_path = None + for path in metrics_files: + fname = os.path.basename(path) + if fname == 'terraform-inventory.host': + tf_inventory_path = path + continue + m = metrics_fname_re.match(fname) + if not m: + continue + nick = m.group(1) + timestamp = m.group(2) + timestamp = datetime.strptime(timestamp, '%Y%m%d_%H%M%S') + dapp(filesByNick, nick, timestamp, path) + + if tf_inventory_path: + # remap ip addresses to node names + ip_to_name = terraform_inventory_ip_not_names(tf_inventory_path) + filesByNick2 = {} + for nick in filesByNick.keys(): + parsed = urlparse('//' + nick) + name: str = ip_to_name.get(parsed.hostname) + val = filesByNick[nick] + filesByNick2[name] = val + + filesByNick = filesByNick2 + filesByNick2 = {} + + for nick in filesByNick.keys(): + if nick_res or not nick_res and not nick_lres: + # filter by regexp or apply default renaming + for nick_re in nick_res: + if re.match(nick_re, nick): + break + else: + if nick_res: + # regex is given but not matched, continue to the next node + continue + + # apply default renaming + name = nick + idx = name.find('_') + if idx != -1: + name = name[idx+1:] + val = filesByNick[nick] + filesByNick2[name] = val + + elif nick_lres: + # filter by label:regexp + label = None + for nick_lre in nick_lres: + label, nick_re = nick_lre.split(':') + if re.match(nick_re, nick): + break + else: + if nick_lres: + # regex is given but not matched, continue to the next node + continue + + val = filesByNick[nick] + filesByNick2[label] = val + else: + raise RuntimeError('unexpected options combination') + + if filesByNick2: + filesByNick = filesByNick2 + + return filesByNick + +class MetricType(Enum): + GAUGE = 0 + COUNTER = 1 + +class Metric: + """Metric with tags""" + def __init__(self, metric_name: str, type: MetricType, value: Union[int, float]): + full_name = metric_name.strip() + self.name = full_name + self.value = value + self.type = type + self.tags: Dict[str, str] = {} + self.tag_keys: set = set() + + det_idx = self.name.find('{') + if det_idx != -1: + self.name = self.name[:det_idx] + # ensure that the last character is '}' + idx = full_name.index('}') + if idx != len(full_name) - 1: + raise ValueError(f'Invalid metric name: {full_name}') + raw_tags = full_name[full_name.find('{')+1:full_name.find('}')] + tags = raw_tags.split(',') + for tag in tags: + key, value = tag.split('=') + if value[0] == '"' and value[-1] == '"': + value = value[1:-1] + self.tags[key] = value + self.tag_keys.add(key) + + def short_name(self): + return self.name + + def __str__(self): + return self.string() + + def string(self, tags: Optional[set[str]]=None): + result = self.name + if self.tags: + if not tags: + tags = self.tags + result += '{' + ','.join([f'{k}={v}' for k, v in sorted(self.tags.items()) if k in tags]) + '}' + return result + + def add_tag(self, key: str, value: str): + self.tags[key] = value + self.tag_keys.add(key) + + def has_tags(self, tag_keys: set, tags: Dict[str, str]): + """return True if all tags are present in the metric tags + tag_keys are not strictly needed but used as an optimization + """ + if self.tag_keys.intersection(tag_keys) != tag_keys: + return False + for k, v in tags.items(): + if self.tags.get(k) != v: + return False + return True + +def parse_metrics( + fin: Iterable[str], nick: str, metrics_names: set=None, diff: bool=None +) -> Dict[str, List[Metric]]: + """Parse metrics file and return dicts of metric names (no tags) and list of Metric objects + each containing the metric name, value and tags. + """ + out = {} + try: + last_type = None + for line in fin: + if not line: + continue + line = line.strip() + if not line: + continue + if line[0] == '#': + if line.startswith('# TYPE'): + tpe = line.split()[-1] + if tpe == 'gauge': + last_type = MetricType.GAUGE + elif tpe == 'counter': + last_type = MetricType.COUNTER + continue + m = metric_line_re.match(line) + if m: + name = m.group(1) + value = num(m.group(2)) + else: + ab = line.split() + name = ab[0] + value = num(ab[1]) + + metric = Metric(name, last_type, value) + metric.add_tag('n', nick) + if not metrics_names or metric.name in metrics_names: + if metric.name not in out: + out[metric.name] = [metric] + else: + out[metric.name].append(metric) + except: + print(f'An exception occurred in parse_metrics: {sys.exc_info()}') + pass + if diff and metrics_names and len(metrics_names) == 2 and len(out) == 2: + m = list(out.keys()) + name = f'{m[0]}_-_{m[1]}' + metric = Metric(name, MetricType.GAUGE, out[m[0]].value - out[m[1]].value) + out = [{name: metric}] + + return out diff --git a/test/heapwatch/metrics_viz.py b/test/heapwatch/metrics_viz.py index 584fc0ae59..741aa2dd73 100644 --- a/test/heapwatch/metrics_viz.py +++ b/test/heapwatch/metrics_viz.py @@ -11,13 +11,11 @@ """ import argparse -from datetime import datetime import glob import logging import os import re import time -from typing import Dict, Iterable, Tuple import sys import dash @@ -25,95 +23,24 @@ import plotly.graph_objs as go from plotly.subplots import make_subplots -from metrics_delta import metric_line_re, num, terraform_inventory_ip_not_names -from client_ram_report import dapp +from metrics_lib import MetricType, parse_metrics, gather_metrics_files_by_nick logger = logging.getLogger(__name__) -metrics_fname_re = re.compile(r'(.*?)\.(\d+_\d+)\.metrics') - -def gather_metrics_files_by_nick(metrics_files: Iterable[str]) -> Dict[str, Dict[datetime, str]]: - """return {"node nickname": {datetime: path, ...}, ...}}""" - filesByNick = {} - tf_inventory_path = None - for path in metrics_files: - fname = os.path.basename(path) - if fname == 'terraform-inventory.host': - tf_inventory_path = path - continue - m = metrics_fname_re.match(fname) - if not m: - continue - nick = m.group(1) - timestamp = m.group(2) - timestamp = datetime.strptime(timestamp, '%Y%m%d_%H%M%S') - dapp(filesByNick, nick, timestamp, path) - return tf_inventory_path, filesByNick - - -TYPE_GAUGE = 0 -TYPE_COUNTER = 1 - -def parse_metrics(fin: Iterable[str], nick: str, metrics_names: set=None, diff: bool=None) -> Tuple[Dict[str, float], Dict[str, int]]: - """Parse metrics file and return dicts of values and types""" - out = {} - types = {} - try: - last_type = None - for line in fin: - if not line: - continue - line = line.strip() - if not line: - continue - if line[0] == '#': - if line.startswith('# TYPE'): - tpe = line.split()[-1] - if tpe == 'gauge': - last_type = TYPE_GAUGE - elif tpe == 'counter': - last_type = TYPE_COUNTER - continue - m = metric_line_re.match(line) - if m: - name = m.group(1) - value = num(m.group(2)) - else: - ab = line.split() - name = ab[0] - value = num(ab[1]) - - det_idx = name.find('{') - if det_idx != -1: - name = name[:det_idx] - fullname = f'{name}{{n={nick}}}' - if not metrics_names or name in metrics_names: - out[fullname] = value - types[fullname] = last_type - except: - print(f'An exception occurred in parse_metrics: {sys.exc_info()}') - pass - if diff and metrics_names and len(metrics_names) == 2 and len(out) == 2: - m = list(out.keys()) - name = f'{m[0]}_-_{m[1]}' - new_out = {name: out[m[0]] - out[m[1]]} - new_types = {name: TYPE_GAUGE} - out = new_out - types = new_types - - return out, types - def main(): os.environ['TZ'] = 'UTC' time.tzset() - default_output_file = 'metrics_viz.png' + default_img_filename = 'metrics_viz.png' + default_html_filename = 'metrics_viz.html' ap = argparse.ArgumentParser() ap.add_argument('metrics_names', nargs='+', default=None, help='metric name(s) to track') ap.add_argument('-d', '--dir', type=str, default=None, help='dir path to find /*.metrics in') ap.add_argument('-l', '--list-nodes', default=False, action='store_true', help='list available node names with metrics') - ap.add_argument('-s', '--save', action='store_true', default=None, help=f'save plot to \'{default_output_file}\' file instead of showing it') + ap.add_argument('--nick-re', action='append', default=[], help='regexp to filter node names, may be repeated') + ap.add_argument('--nick-lre', action='append', default=[], help='label:regexp to filter node names, may be repeated') + ap.add_argument('-s', '--save', type=str, choices=['png', 'html'], help=f'save plot to \'{default_img_filename}\' or \'{default_html_filename}\' file instead of showing it') ap.add_argument('--diff', action='store_true', default=None, help='diff two gauge metrics instead of plotting their values. Requires two metrics names to be set') ap.add_argument('--verbose', default=False, action='store_true') @@ -128,16 +55,8 @@ def main(): return 1 metrics_files = sorted(glob.glob(os.path.join(args.dir, '*.metrics'))) - tf_inventory_path, filesByNick = gather_metrics_files_by_nick(metrics_files) - if tf_inventory_path: - # remap ip addresses to node names - ip_to_name = terraform_inventory_ip_not_names(tf_inventory_path) - for nick in filesByNick.keys(): - name = ip_to_name.get(nick) - if name: - val = filesByNick[nick] - filesByNick[name] = val - del filesByNick[nick] + metrics_files.extend(glob.glob(os.path.join(args.dir, 'terraform-inventory.host'))) + filesByNick = gather_metrics_files_by_nick(metrics_files, args.nick_re, args.nick_lre) if args.list_nodes: print('Available nodes:', ', '.join(sorted(filesByNick.keys()))) @@ -156,50 +75,76 @@ def main(): fig = make_subplots( rows=nrows, cols=1, - vertical_spacing=0.03, shared_xaxes=True) + vertical_spacing=0.03, shared_xaxes=True, + subplot_titles=[f'{name}' for name in sorted(metrics_names)], + ) fig['layout']['margin'] = { - 'l': 30, 'r': 10, 'b': 10, 't': 10 + 'l': 30, 'r': 10, 'b': 10, 't': 20 } fig['layout']['height'] = 500 * nrows # fig.update_layout(template="plotly_dark") - data = { - 'time': [], - } - raw_series = {} - for nick, items in filesByNick.items(): - active_metrics = set() - for dt, metrics_file in items.items(): + for nick, files_by_date in filesByNick.items(): + active_metrics = {} + data = {'time': []} + raw_series = {} + raw_times = {} + idx = 0 + for dt, metrics_file in files_by_date.items(): data['time'].append(dt) with open(metrics_file, 'rt') as f: - metrics, types = parse_metrics(f, nick, metrics_names, args.diff) - for metric_name, metric_value in metrics.items(): - raw_value = metric_value - if metric_name not in data: - data[metric_name] = [] - raw_series[metric_name] = [] - if types[metric_name] == TYPE_COUNTER: - if len(raw_series[metric_name]) > 0: - metric_value = (metric_value - raw_series[metric_name][-1]) / (dt - data['time'][-2]).total_seconds() - else: - metric_value = 0 - data[metric_name].append(metric_value) - raw_series[metric_name].append(raw_value) - - active_metrics.add(metric_name) - - for i, metric in enumerate(sorted(active_metrics)): - fig.append_trace(go.Scatter( - x=data['time'], - y=data[metric], - name=metric, - mode='lines+markers', - line=dict(width=1), - ), i+1, 1) + metrics = parse_metrics(f, nick, metrics_names, args.diff) + for metric_name, metrics_seq in metrics.items(): + active_metric_names = [] + for metric in metrics_seq: + raw_value = metric.value + + full_name = metric.string() + if full_name not in data: + # handle gaps in data, sometimes metric file might miss a value + # but the chart requires matching x and y series (time and metric value) + # data is what does into the chart, and raw_series is used to calculate + data[full_name] = [0] * len(files_by_date) + raw_series[full_name] = [] + raw_times[full_name] = [] + + metric_value = metric.value + if metric.type == MetricType.COUNTER: + if len(raw_series[full_name]) > 0 and len(raw_times[full_name]) > 0: + metric_value = (metric_value - raw_series[full_name][-1]) / (dt - raw_times[full_name][-1]).total_seconds() + else: + metric_value = 0 + + data[full_name][idx] = metric_value + raw_series[full_name].append(raw_value) + raw_times[full_name].append(dt) + + active_metric_names.append(full_name) + + active_metric_names.sort() + active_metrics[metric_name] = active_metric_names + idx += 1 + + for i, metric_pair in enumerate(sorted(active_metrics.items())): + metric_name, metric_fullnames = metric_pair + for metric_fullname in metric_fullnames: + fig.append_trace(go.Scatter( + x=data['time'], + y=data[metric_fullname], + name=metric_fullname, + mode='lines+markers', + line=dict(width=1), + ), i+1, 1) if args.save: - fig.write_image(os.path.join(args.dir, default_output_file)) + if args.save == 'html': + target_path = os.path.join(args.dir, default_html_filename) + fig.write_html(target_path) + else: + target_path = os.path.join(args.dir, default_img_filename) + fig.write_image(target_path) + print(f'Saved plot to {target_path}') else: fig.show() diff --git a/test/heapwatch/requirements.txt b/test/heapwatch/requirements.txt index d4d68874dd..db92372c6d 100644 --- a/test/heapwatch/requirements.txt +++ b/test/heapwatch/requirements.txt @@ -5,3 +5,6 @@ matplotlib==3.7.2 plotly==5.16.0 py-algorand-sdk==2.3.0 kaleido==0.2.1 +networkx==3.3 +gravis=0.1.0 +termcolor=2.4.0 diff --git a/test/heapwatch/topology-extract-p2p.py b/test/heapwatch/topology-extract-p2p.py new file mode 100644 index 0000000000..41f2be9ffc --- /dev/null +++ b/test/heapwatch/topology-extract-p2p.py @@ -0,0 +1,104 @@ +""" +P2P network topology extraction script from node.log files. + +1. Run P2P scenario like scenario1s-p2p +2. Fetch logs with `algonet play fetch_node_logs` +3. Extract logs +``` +cd nodelog +find . -name 'nodelog.tar.gz' -print | xargs -I{} tar -zxf {} +``` +4. Run this script `python3 topology-extract-p2p.py -o top.json nodelog` +5. Run the visualizer `topology-viz.py top.json` +""" +import argparse +from datetime import datetime +import json +import re +import os +import sys + + +def main(): + # Regex patterns to find node IDs and connections + node_pattern = r"P2P host created: peer ID (\w{52})" + edge_pattern = r"Made outgoing connection to peer (\w{52})" + + ap = argparse.ArgumentParser() + ap.add_argument('log_dir_path', help='logs directory path') + ap.add_argument('-o', '--output', type=argparse.FileType('wt', encoding='utf-8'), help=f'save topology to the file specified instead of showing it') + ap.add_argument('-t', '--timestamp', action='store_true', help=f'store connection timestamp for each edge') + + args = ap.parse_args() + + # Directory containing log files + log_dir_path = args.log_dir_path + + nodes = [] + edges = [] + mapping = {} + + # Iterate through all files in the specified directory + for filename in os.listdir(log_dir_path): + if filename.endswith("-node.log"): + with open(os.path.join(log_dir_path, filename), 'r') as file: + mapped = filename[:len(filename) - len('-node.log')] + mapped = mapped.replace('relay', 'R') + mapped = mapped.replace('nonParticipatingNode', 'NPN') + mapped = mapped.replace('node', 'N') + node_id = None + for line in file: + # Check if line contains relevant substrings before parsing as JSON + if "P2P host created" in line or "Made outgoing connection to peer" in line: + data = json.loads(line.strip()) + + # Check for node creation + if "P2P host created" in data.get("msg", ""): + match = re.search(node_pattern, data["msg"]) + if match: + node_id = match.group(1) + nodes.append(node_id) + mapping[node_id] = mapped + + # Check for connections + elif "Made outgoing connection to peer" in data.get("msg", ""): + match = re.search(edge_pattern, data["msg"]) + if match: + target_node_id = match.group(1) + match = re.findall(r"/p2p/(\w{52})", data["local"]) + if match: + source_node_id = match[0] + else: + print('WARN: no local addr set', data, file=sys.stderr) + source_node_id = node_id + + if args.timestamp: + # datetime is not serializable, so we store it as string for now + edge = (source_node_id, target_node_id, {'dt': data["time"]}) + else: + edge = (source_node_id, target_node_id) + + edges.append(edge) + + result = { + "mapping": mapping, + "nodes": nodes, + "edges": edges + } + + if args.timestamp and not args.output: + edges = sorted(edges, key=lambda x: x[2]['dt']) + for edge in edges: + ts = datetime.strptime(edge[2]['dt'], "%Y-%m-%dT%H:%M:%S.%f%z") + print('%15s %5s -> %-5s' % (ts.strftime('%H:%M:%S.%f'), mapping[edge[0]], mapping[edge[1]])) + return + + if args.output: + json.dump(result, args.output, indent=2) + else: + json.dump(result, sys.stdout, indent=2) + print(file=sys.stdout) + + +if __name__ == '__main__': + main() diff --git a/test/heapwatch/topology-extract-ws.py b/test/heapwatch/topology-extract-ws.py new file mode 100644 index 0000000000..75f1d99f57 --- /dev/null +++ b/test/heapwatch/topology-extract-ws.py @@ -0,0 +1,115 @@ +""" +WSNet network topology extraction script from node.log files. + +1. Run cluster scenario like scenario1s +2. Fetch logs with `algonet play fetch_node_logs` +3. Extract logs +``` +cd nodelog +find . -name 'nodelog.tar.gz' -print | xargs -I{} tar -zxf {} +``` +4. Run this script `python3 topology-extract-ws.py -o top.json -i ../terraform-inventory.json nodelog` +5. Run the visualizer `topology-viz.py top.json` +""" +import argparse +from datetime import datetime +import json +import os +import sys + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('log_dir_path', help='logs directory path') + ap.add_argument('-i', '--inventory-file', type=argparse.FileType('rt', encoding='utf-8'), required=True, help='terraform inventory file path') + ap.add_argument('-o', '--output', type=argparse.FileType('wt', encoding='utf-8'), help=f'save topology to the file specified instead of showing it') + ap.add_argument('-t', '--timestamp', action='store_true', help=f'store connection timestamp for each edge') + + args = ap.parse_args() + + # Directory containing log files + log_dir_path = args.log_dir_path + inventory_file = args.inventory_file + + nodes = [] + edges = [] + mapping = {} + + inventory = json.load(inventory_file) + + ip_to_name = {} + for k, v in inventory.items(): + if k.startswith('name_'): + name = k.split('_')[1].upper() + if not isinstance(v, list) or len(v) != 1: + raise RuntimeError(f"Invalid inventory entry, expected a single item list: {k}={v}") + ip = v[0] + ip_to_name[ip] = name + # no need for mapping but keep the data compatible with the topology-viz script + mapping[name] = name + + # Iterate through all files in the specified directory + for filename in os.listdir(log_dir_path): + if filename.endswith('-node.log'): + with open(os.path.join(log_dir_path, filename), 'r') as file: + mapped = filename[:len(filename) - len('-node.log')] + mapped = mapped.replace('relay', 'R') + mapped = mapped.replace('nonParticipatingNode', 'NPN') + mapped = mapped.replace('node', 'N') + nodes.append(mapped) + for line in file: + # Check if line contains relevant substrings before parsing as JSON + if "Accepted incoming connection from peer" in line or "Made outgoing connection to peer" in line: + data = json.loads(line.strip()) + + # Check for incoming connections + if "Accepted incoming connection from peer" in data.get("msg", ""): + remote = data['remote'] + remote_ip = remote.split(':')[0] + remote_name = ip_to_name[remote_ip] + source = remote_name + target = mapped + edges.append((source, target)) + + # Check for outgoing connections + elif "Made outgoing connection to peer" in data.get('msg', ""): + remote = data['remote'] + name: str = remote.split('.')[0] + # check ip or name + if name.isdigit(): + remote_ip = remote.split(':')[0] + remote_name = ip_to_name[remote_ip] + target = remote_name + source = mapped + else: + target = name.upper() + source = mapped + + if args.timestamp: + # datetime is not serializable, so we store it as string for now + edge = (source, target, {'dt': data["time"]}) + else: + edge = (source, target) + + edges.append(edge) + + result = { + "mapping": mapping, + "nodes": nodes, + "edges": edges + } + + if args.timestamp and not args.output: + edges = sorted(edges, key=lambda x: x[2]['dt']) + for edge in edges: + ts = datetime.strptime(edge[2]['dt'], "%Y-%m-%dT%H:%M:%S.%f%z") + print('%15s %5s -> %-5s' % (ts.strftime('%H:%M:%S.%f'), edge[0], edge[1])) + return + + if args.output: + json.dump(result, args.output, indent=2) + else: + json.dump(result, sys.stdout, indent=2) + print(file=sys.stdout) + +if __name__ == '__main__': + main() diff --git a/test/heapwatch/topology-viz.py b/test/heapwatch/topology-viz.py new file mode 100644 index 0000000000..1393421696 --- /dev/null +++ b/test/heapwatch/topology-viz.py @@ -0,0 +1,75 @@ +""" +P2P network topology visualization script. +See topology-extract-p2p[-ws].py for details. +""" +import argparse +import json +import sys + +import gravis as gv +import networkx as nx + +ap = argparse.ArgumentParser() +ap.add_argument('topology_filename', help='topology json file') +ap.add_argument('-o', '--output', type=argparse.FileType('wt', encoding='utf-8'), help=f'save plot to the file specified instead of showing it') + +args = ap.parse_args() + +with open(args.topology_filename, 'rt') as f: + topology = json.load(f) + +# Create a new directed graph +G = nx.DiGraph() + +G.add_edges_from(topology['edges']) +nx.relabel_nodes(G, topology['mapping'], copy=False) + +# Set node colors +for node in G: + if node.startswith('R'): + G.nodes[node]['color'] = 'red' + elif node.startswith('NPN'): + G.nodes[node]['color'] = 'blue' + elif node.startswith('N'): + G.nodes[node]['color'] = 'green' + else: + raise RuntimeError(f"Unknown node type: {node}") + +# Calculate in-degrees +in_degrees = dict(G.in_degree()) +out_degrees = dict(G.out_degree()) +degree_centrality = nx.degree_centrality(G) +load_centrality = nx.algorithms.load_centrality(G) + +for node in G: + size = max(2, in_degrees[node]) + G.nodes[node]['size'] = size + G.nodes[node]['in_degree'] = in_degrees[node] + G.nodes[node]['out_degree'] = out_degrees[node] + hover = f'In: {in_degrees[node]}, Out: {out_degrees[node]}' + hover += f'\nDegree centrality: {degree_centrality[node]:.2f}' + hover += f'\nLoad centrality: {load_centrality[node]:.2f}' + G.nodes[node]['hover'] = hover + +print('Transitivity:', nx.transitivity(G)) +print('Clustering coefficient:', nx.average_clustering(G)) +print('Avg shortest path length:', nx.average_shortest_path_length(G.to_undirected(as_view=True))) + +res = gv.d3( + G, + node_hover_tooltip=True, + node_size_data_source='size', + node_label_size_factor=0.5, + use_node_size_normalization=True, + node_size_normalization_max=20, + use_edge_size_normalization=True, + edge_curvature=0.1 + ) + +if not args.output: + res.display() + sys.exit(0) + +# Save to file +data = res.to_html() +args.output.write(data) diff --git a/test/testdata/configs/config-v34.json b/test/testdata/configs/config-v34.json index d695679067..b8701ccab3 100644 --- a/test/testdata/configs/config-v34.json +++ b/test/testdata/configs/config-v34.json @@ -30,7 +30,7 @@ "ConnectionsRateLimitingWindowSeconds": 1, "CrashDBDir": "", "DNSBootstrapID": ".algorand.network?backup=.algorand.net&dedup=.algorand-.(network|net)", - "DNSSecurityFlags": 1, + "DNSSecurityFlags": 9, "DeadlockDetection": 0, "DeadlockDetectionThreshold": 30, "DisableAPIAuth": false, diff --git a/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/genesis.json b/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/genesis.json new file mode 100644 index 0000000000..7ae67edf88 --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/genesis.json @@ -0,0 +1,30 @@ +{ + "NetworkName": "hello-p2p", + "VersionModifier": "", + "ConsensusProtocol": "future", + "FirstPartKeyRound": 0, + "LastPartKeyRound": 5000, + "PartKeyDilution": 0, + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 25, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 25, + "Online": true + }, + { + "Name": "Wallet3", + "Stake": 25, + "Online": true + }, + { + "Name": "Wallet4", + "Stake": 25, + "Online": false + } + ] +} diff --git a/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/net.json b/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/net.json new file mode 100644 index 0000000000..423d31c1a4 --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/net.json @@ -0,0 +1,107 @@ +{ + "Hosts": [ + { + "Name": "R1", + "Nodes": [ + { + "Name": "relay1", + "IsRelay": true, + "Wallets": [ + { + "Name": "Wallet1", + "ParticipationOnly": false + } + ], + "NetAddress": "{{NetworkPort}}", + "APIEndpoint": "{{APIEndpoint}}", + "APIToken": "{{APIToken}}", + "AdminAPIToken": "{{AdminAPIToken}}", + "EnableTelemetry": true, + "TelemetryURI": "{{TelemetryURI}}", + "EnableMetrics": true, + "MetricsURI": "{{MetricsURI}}", + "EnableService": false, + "EnableBlockStats": true, + "P2PBootstrap": true, + "ConfigJSONOverride": "{ \"DNSBootstrapID\": \".algodev.network\", \"DeadlockDetection\": -1, \"PeerPingPeriodSeconds\": 30, \"EnableAgreementReporting\": true, \"EnableAgreementTimeMetrics\": true, \"EnableAssembleStats\": true, \"EnableProcessBlockStats\": true, \"EnableProfiler\": true, \"EnableRuntimeMetrics\": true, \"EnableExperimentalAPI\": true, \"EnableAccountUpdatesStats\": true, \"EnableP2P\": true }" + } + ] + }, + { + "Name": "R2", + "Nodes": [ + { + "Name": "relay2", + "IsRelay": true, + "Wallets": [ + { + "Name": "Wallet2", + "ParticipationOnly": false + } + ], + "NetAddress": "{{NetworkPort}}", + "APIEndpoint": "{{APIEndpoint}}", + "APIToken": "{{APIToken}}", + "AdminAPIToken": "{{AdminAPIToken}}", + "EnableTelemetry": true, + "TelemetryURI": "{{TelemetryURI}}", + "EnableMetrics": true, + "MetricsURI": "{{MetricsURI}}", + "EnableService": false, + "EnableBlockStats": true, + "P2PBootstrap": true, + "ConfigJSONOverride": "{ \"DNSBootstrapID\": \".algodev.network\",\"DeadlockDetection\": -1, \"PeerPingPeriodSeconds\": 30, \"EnableAgreementReporting\": true, \"EnableAgreementTimeMetrics\": true, \"EnableAssembleStats\": true, \"EnableProcessBlockStats\": true, \"EnableProfiler\": true, \"EnableRuntimeMetrics\": true, \"EnableExperimentalAPI\": true, \"EnableAccountUpdatesStats\": true, \"EnableP2P\": true }" + } + ] + }, + { + "Name": "N1", + "Group": "", + "Nodes": [ + { + "Name": "node1", + "Wallets": [ + { + "Name": "Wallet3", + "ParticipationOnly": false + } + ], + "APIEndpoint": "{{APIEndpoint}}", + "APIToken": "{{APIToken}}", + "AdminAPIToken": "{{AdminAPIToken}}", + "EnableTelemetry": false, + "TelemetryURI": "{{TelemetryURI}}", + "EnableMetrics": true, + "MetricsURI": "{{MetricsURI}}", + "EnableService": false, + "EnableBlockStats": false, + "ConfigJSONOverride": "{ \"TxPoolExponentialIncreaseFactor\": 1, \"DNSBootstrapID\": \".algodev.network\", \"DeadlockDetection\": -1, \"PeerPingPeriodSeconds\": 30, \"BaseLoggerDebugLevel\": 4, \"EnableProfiler\": true, \"CadaverSizeTarget\": 0, \"EnableAccountUpdatesStats\": true, \"EnableProfiler\": true, \"EnableRuntimeMetrics\": true, \"EnableExperimentalAPI\": true, \"EnableAccountUpdatesStats\": true, \"EnableP2P\": true }" + } + ] + }, + { + "Name": "NPN1", + "Group": "", + "Nodes": [ + { + "Name": "nonParticipatingNode1", + "Wallets": [ + { + "Name": "Wallet4", + "ParticipationOnly": false + } + ], + "APIEndpoint": "{{APIEndpoint}}", + "APIToken": "{{APIToken}}", + "AdminAPIToken": "{{AdminAPIToken}}", + "EnableTelemetry": false, + "EnableMetrics": true, + "MetricsURI": "{{MetricsURI}}", + "EnableService": false, + "EnableBlockStats": false, + "ConfigJSONOverride": "{ \"TxPoolExponentialIncreaseFactor\": 1, \"DNSBootstrapID\": \".algodev.network\", \"DeadlockDetection\": -1, \"BaseLoggerDebugLevel\": 4, \"CadaverSizeTarget\": 0, \"EnableProfiler\": true, \"EnableRuntimeMetrics\": true, \"EnableExperimentalAPI\": true, \"EnableAccountUpdatesStats\": true, \"EnableP2P\": true}" + } + ] + } + ] +} diff --git a/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/recipe.json b/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/recipe.json new file mode 100644 index 0000000000..a2f88f63b4 --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/recipe.json @@ -0,0 +1,7 @@ +{ + "GenesisFile":"genesis.json", + "NetworkFile":"net.json", + "ConfigFile": "../../configs/reference.json", + "HostTemplatesFile": "../../hosttemplates/hosttemplates.json", + "TopologyFile": "topology.json" +} diff --git a/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/topology.json b/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/topology.json new file mode 100644 index 0000000000..acc7cca9ec --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/hello-world-small-p2p/topology.json @@ -0,0 +1,20 @@ +{ + "Hosts": [ + { + "Name": "R1", + "Template": "AWS-US-EAST-1-Small" + }, + { + "Name": "R2", + "Template": "AWS-US-EAST-1-Small" + }, + { + "Name": "N1", + "Template": "AWS-US-EAST-1-Small" + }, + { + "Name": "NPN1", + "Template": "AWS-US-EAST-1-Small" + } + ] +} diff --git a/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/genesis.json b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/genesis.json new file mode 100644 index 0000000000..7ae67edf88 --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/genesis.json @@ -0,0 +1,30 @@ +{ + "NetworkName": "hello-p2p", + "VersionModifier": "", + "ConsensusProtocol": "future", + "FirstPartKeyRound": 0, + "LastPartKeyRound": 5000, + "PartKeyDilution": 0, + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 25, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 25, + "Online": true + }, + { + "Name": "Wallet3", + "Stake": 25, + "Online": true + }, + { + "Name": "Wallet4", + "Stake": 25, + "Online": false + } + ] +} diff --git a/test/testdata/deployednettemplates/recipes/hello-world/hosttemplates.json b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/hosttemplates.json similarity index 100% rename from test/testdata/deployednettemplates/recipes/hello-world/hosttemplates.json rename to test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/hosttemplates.json diff --git a/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/net.json b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/net.json new file mode 100644 index 0000000000..8ea8328c62 --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/net.json @@ -0,0 +1,101 @@ +{ + "Hosts": [ + { + "Name": "R1", + "Nodes": [ + { + "Name": "relay1", + "IsRelay": true, + "Wallets": [ + { + "Name": "Wallet1", + "ParticipationOnly": false + } + ], + "NetAddress": "{{NetworkPort}}", + "APIEndpoint": "{{APIEndpoint}}", + "APIToken": "{{APIToken}}", + "EnableTelemetry": true, + "TelemetryURI": "{{TelemetryURI}}", + "EnableMetrics": true, + "MetricsURI": "{{MetricsURI}}", + "EnableService": false, + "EnableBlockStats": true, + "P2PBootstrap": true, + "ConfigJSONOverride": "{ \"DNSBootstrapID\": \".algodev.network\", \"DeadlockDetection\": -1, \"PeerPingPeriodSeconds\": 30, \"EnableAgreementReporting\": true, \"EnableAgreementTimeMetrics\": true, \"EnableAssembleStats\": true, \"EnableProcessBlockStats\": true, \"EnableP2P\": true }" + } + ] + }, + { + "Name": "R2", + "Nodes": [ + { + "Name": "relay2", + "IsRelay": true, + "Wallets": [ + { + "Name": "Wallet2", + "ParticipationOnly": false + } + ], + "NetAddress": "{{NetworkPort}}", + "APIEndpoint": "{{APIEndpoint}}", + "APIToken": "{{APIToken}}", + "EnableTelemetry": true, + "TelemetryURI": "{{TelemetryURI}}", + "EnableMetrics": true, + "MetricsURI": "{{MetricsURI}}", + "EnableService": false, + "EnableBlockStats": true, + "ConfigJSONOverride": "{ \"DNSBootstrapID\": \".algodev.network\",\"DeadlockDetection\": -1, \"PeerPingPeriodSeconds\": 30, \"EnableAgreementReporting\": true, \"EnableAgreementTimeMetrics\": true, \"EnableAssembleStats\": true, \"EnableProcessBlockStats\": true, \"EnableP2P\": true }" + } + ] + }, + { + "Name": "N1", + "Group": "", + "Nodes": [ + { + "Name": "node1", + "Wallets": [ + { + "Name": "Wallet3", + "ParticipationOnly": false + } + ], + "APIEndpoint": "{{APIEndpoint}}", + "APIToken": "{{APIToken}}", + "EnableTelemetry": false, + "TelemetryURI": "{{TelemetryURI}}", + "EnableMetrics": false, + "MetricsURI": "{{MetricsURI}}", + "EnableService": false, + "EnableBlockStats": false, + "ConfigJSONOverride": "{ \"TxPoolExponentialIncreaseFactor\": 1, \"DNSBootstrapID\": \".algodev.network\", \"DeadlockDetection\": -1, \"PeerPingPeriodSeconds\": 30, \"BaseLoggerDebugLevel\": 4, \"EnableProfiler\": true, \"CadaverSizeTarget\": 0, \"EnableAccountUpdatesStats\": true, \"EnableP2P\": true }" + } + ] + }, + { + "Name": "NPN1", + "Group": "", + "Nodes": [ + { + "Name": "nonParticipatingNode1", + "Wallets": [ + { + "Name": "Wallet4", + "ParticipationOnly": false + } + ], + "APIEndpoint": "{{APIEndpoint}}", + "APIToken": "{{APIToken}}", + "EnableTelemetry": false, + "EnableMetrics": false, + "EnableService": false, + "EnableBlockStats": false, + "ConfigJSONOverride": "{ \"TxPoolExponentialIncreaseFactor\": 1, \"DNSBootstrapID\": \".algodev.network\", \"DeadlockDetection\": -1, \"BaseLoggerDebugLevel\": 4, \"CadaverSizeTarget\": 0, \"EnableP2P\": true }" + } + ] + } + ] +} diff --git a/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/recipe.json b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/recipe.json new file mode 100644 index 0000000000..be6b71ec55 --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/recipe.json @@ -0,0 +1,7 @@ +{ + "GenesisFile":"genesis.json", + "NetworkFile":"net.json", + "ConfigFile": "../../configs/reference.json", + "HostTemplatesFile": "../../hosttemplates/t2micro-useast1.json", + "TopologyFile": "topology.json" +} diff --git a/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/topology.json b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/topology.json new file mode 100644 index 0000000000..acc7cca9ec --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/hello-world-tiny-p2p/topology.json @@ -0,0 +1,20 @@ +{ + "Hosts": [ + { + "Name": "R1", + "Template": "AWS-US-EAST-1-Small" + }, + { + "Name": "R2", + "Template": "AWS-US-EAST-1-Small" + }, + { + "Name": "N1", + "Template": "AWS-US-EAST-1-Small" + }, + { + "Name": "NPN1", + "Template": "AWS-US-EAST-1-Small" + } + ] +} diff --git a/test/testdata/deployednettemplates/recipes/hello-world/genesis.json b/test/testdata/deployednettemplates/recipes/hello-world/genesis.json index 218b694d5f..b7fdd9502b 100644 --- a/test/testdata/deployednettemplates/recipes/hello-world/genesis.json +++ b/test/testdata/deployednettemplates/recipes/hello-world/genesis.json @@ -3,7 +3,7 @@ "VersionModifier": "", "ConsensusProtocol": "future", "FirstPartKeyRound": 0, - "LastPartKeyRound": 1000300, + "LastPartKeyRound": 5000, "PartKeyDilution": 0, "Wallets": [ { diff --git a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/Makefile b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/Makefile new file mode 100644 index 0000000000..f4ec4b3c1f --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/Makefile @@ -0,0 +1,23 @@ +# scenario1s is scenario1 but smaller, (100 nodes, 100 wallets) -> (20 nodes, 20 wallets), each algod gets single tenancy on a smaller ec2 instance +PARAMS=-w 20 -R 8 -N 20 -n 20 --npn-algod-nodes 10 --node-template node.json --relay-template relay.json --non-participating-node-template nonPartNode.json + +.PHONY: clean all + +all: net.json genesis.json topology.json + +node.json nonPartNode.json relay.json: + python3 copy-node-configs.py + +net.json: node.json nonPartNode.json relay.json ${GOPATH}/bin/netgoal Makefile + netgoal generate -t net -r /tmp/wat -o net.json ${PARAMS} + +genesis.json: ${GOPATH}/bin/netgoal Makefile + netgoal generate -t genesis -r /tmp/wat -o genesis.l.json ${PARAMS} + jq '.LastPartKeyRound=5000|.NetworkName="s1s-p2p"|.ConsensusProtocol="future"' < genesis.l.json > genesis.json + rm genesis.l.json + +topology.json: ../scenario1s/gen_topology.py + python3 ../scenario1s/gen_topology.py + +clean: + rm -f net.json genesis.json node.json nonPartNode.json relay.json topology.json diff --git a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/README.md b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/README.md new file mode 100644 index 0000000000..1cad95bc2d --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/README.md @@ -0,0 +1,16 @@ +# Scenario1s for P2P testing + +This is a copy of scenario1s with the following changes in nodes configuration: +1. All nodes get `"EnableP2P": true` into their config. +1. All relays additionally get `"P2PBootstrap": true` to their netgoal config. + +## Build + +```sh +export GOPATH=~/go +make +``` + +## Run + +Run as usual cluster test scenario with algonet. diff --git a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/copy-node-configs.py b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/copy-node-configs.py new file mode 100644 index 0000000000..6ffbc01d8d --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/copy-node-configs.py @@ -0,0 +1,55 @@ +""" +Copies node.json, relay.json and nonPartNode.json from scenario1s: +1. Append \"EnableP2P\": true to all configs +2. Set P2PBootstrap: true to relay.json +3. Set DNSSecurityFlags: 0 to all configs +""" + +import json +import os + +CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) +SCENARIO1S_DIR = os.path.join(CURRENT_DIR, "..", "scenario1s") + +def main(): + """main""" + with open(os.path.join(SCENARIO1S_DIR, "node.json"), "r") as f: + node = json.load(f) + with open(os.path.join(SCENARIO1S_DIR, "relay.json"), "r") as f: + relay = json.load(f) + with open(os.path.join(SCENARIO1S_DIR, "nonPartNode.json"), "r") as f: + non_part_node = json.load(f) + + # make all relays P2PBootstrap'able + relay["P2PBootstrap"] = True + + # enable P2P for all configs + for config in (node, relay, non_part_node): + override = config.get("ConfigJSONOverride") + if override: + override_json = json.loads(override) + override_json["EnableP2P"] = True + override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC + config["ConfigJSONOverride"] = json.dumps(override_json) + altconfigs = config.get("AltConfigs", []) + if altconfigs: + for i, altconfig in enumerate(altconfigs): + override = altconfig.get("ConfigJSONOverride") + if override: + override_json = json.loads(override) + override_json["EnableP2P"] = True + override_json["DNSSecurityFlags"] = 0x8000 # set to some unused value otherwise 0 would be migrated to default that enables DNSSEC + altconfigs[i]["ConfigJSONOverride"] = json.dumps(override_json) + config["AltConfigs"] = altconfigs + + with open("node.json", "w") as f: + json.dump(node, f, indent=4) + with open("relay.json", "w") as f: + json.dump(relay, f, indent=4) + with open("nonPartNode.json", "w") as f: + json.dump(non_part_node, f, indent=4) + + print("Done!") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/test/testdata/deployednettemplates/recipes/scenario1s-p2p/recipe.json b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/recipe.json new file mode 100644 index 0000000000..a2f88f63b4 --- /dev/null +++ b/test/testdata/deployednettemplates/recipes/scenario1s-p2p/recipe.json @@ -0,0 +1,7 @@ +{ + "GenesisFile":"genesis.json", + "NetworkFile":"net.json", + "ConfigFile": "../../configs/reference.json", + "HostTemplatesFile": "../../hosttemplates/hosttemplates.json", + "TopologyFile": "topology.json" +} diff --git a/test/testdata/deployednettemplates/recipes/scenario1s/Makefile b/test/testdata/deployednettemplates/recipes/scenario1s/Makefile index ed8a70132e..8b83c38b6c 100644 --- a/test/testdata/deployednettemplates/recipes/scenario1s/Makefile +++ b/test/testdata/deployednettemplates/recipes/scenario1s/Makefile @@ -1,14 +1,14 @@ # scenario1s is scenario1 but smaller, (100 nodes, 100 wallets) -> (20 nodes, 20 wallets), each algod gets single tenancy on a smaller ec2 instance PARAMS=-w 20 -R 8 -N 20 -n 20 --npn-algod-nodes 10 --node-template node.json --relay-template relay.json --non-participating-node-template nonPartNode.json -all: net.json genesis.json topology.json bootstrappedFile.json +all: net.json genesis.json topology.json net.json: node.json nonPartNode.json ${GOPATH}/bin/netgoal Makefile netgoal generate -t net -r /tmp/wat -o net.json ${PARAMS} genesis.json: ${GOPATH}/bin/netgoal Makefile netgoal generate -t genesis -r /tmp/wat -o genesis.l.json ${PARAMS} - jq '.LastPartKeyRound=22000|.NetworkName="s1s"|.ConsensusProtocol="future"' < genesis.l.json > genesis.json + jq '.LastPartKeyRound=5000|.NetworkName="s1s"|.ConsensusProtocol="future"' < genesis.l.json > genesis.json rm genesis.l.json topology.json: gen_topology.py diff --git a/util/metrics/opencensus.go b/util/metrics/opencensus.go index 6d38436e6a..fefb1d054b 100644 --- a/util/metrics/opencensus.go +++ b/util/metrics/opencensus.go @@ -77,7 +77,8 @@ type statCounter struct { // WriteMetric outputs Prometheus metrics for all labels/values in statCounter func (st *statCounter) WriteMetric(buf *strings.Builder, parentLabels string) { - counter := makeCounter(MetricName{st.name, st.description}) + name := sanitizePrometheusName(st.name) + counter := makeCounter(MetricName{name, st.description}) for i := 0; i < len(st.labels); i++ { counter.AddUint64(uint64(st.values[i]), st.labels[i]) } @@ -103,7 +104,8 @@ type statDistribution struct { // WriteMetric outputs Prometheus metrics for all labels/values in statCounter func (st *statDistribution) WriteMetric(buf *strings.Builder, parentLabels string) { - gauge := makeGauge(MetricName{st.name, st.description}) + name := sanitizePrometheusName(st.name) + gauge := makeGauge(MetricName{name, st.description}) for i := 0; i < len(st.labels); i++ { gauge.SetLabels(uint64(st.values[i]), st.labels[i]) } diff --git a/util/metrics/opencensus_test.go b/util/metrics/opencensus_test.go index 7a20d75334..b1f5ff102a 100644 --- a/util/metrics/opencensus_test.go +++ b/util/metrics/opencensus_test.go @@ -57,6 +57,7 @@ func TestDHTOpenCensusMetrics(t *testing.T) { err := view.Register(receivedBytesView, sentMessagesView) require.NoError(t, err) + defer view.Unregister(receivedBytesView, sentMessagesView) ctx := context.Background() tags1 := []tag.Mutator{ @@ -132,4 +133,15 @@ func TestDHTOpenCensusMetrics(t *testing.T) { } } } + + // ensure the exported gatherer works + reg := MakeRegistry() + reg.Register(&OpencensusDefaultMetrics) + defer reg.Deregister(&OpencensusDefaultMetrics) + + var buf strings.Builder + reg.WriteMetrics(&buf, "") + + require.Contains(t, buf.String(), "my_sent_messages") + require.Contains(t, buf.String(), "my_received_bytes") } diff --git a/util/metrics/prometheus.go b/util/metrics/prometheus.go index edce668b93..b55f931001 100644 --- a/util/metrics/prometheus.go +++ b/util/metrics/prometheus.go @@ -33,7 +33,7 @@ type defaultPrometheusGatherer struct { // WriteMetric return prometheus converted to algorand format. // Supports only counter and gauge types and ignores go_ metrics. func (pg *defaultPrometheusGatherer) WriteMetric(buf *strings.Builder, parentLabels string) { - metrics := collectOpenCensusMetrics(pg.names) + metrics := collectPrometheusMetrics(pg.names) for _, metric := range metrics { metric.WriteMetric(buf, parentLabels) } diff --git a/util/metrics/prometheus_test.go b/util/metrics/prometheus_test.go index 18313ee220..75ef94f97e 100644 --- a/util/metrics/prometheus_test.go +++ b/util/metrics/prometheus_test.go @@ -74,6 +74,11 @@ func TestPrometheusMetrics(t *testing.T) { prometheus.DefaultRegisterer.MustRegister(counterLabels) prometheus.DefaultRegisterer.MustRegister(counter) + defer prometheus.DefaultRegisterer.Unregister(gaugeLabels) + defer prometheus.DefaultRegisterer.Unregister(gauge) + defer prometheus.DefaultRegisterer.Unregister(counterLabels) + defer prometheus.DefaultRegisterer.Unregister(counter) + // set some values tags := []string{"outbound", "protocol", "/test/proto"} gaugeLabels.WithLabelValues(tags...).Set(float64(1)) @@ -127,4 +132,17 @@ func TestPrometheusMetrics(t *testing.T) { m.AddMetric(values) require.Len(t, values, 1) } + + // ensure the exported gatherer works + reg := MakeRegistry() + reg.Register(&PrometheusDefaultMetrics) + defer reg.Deregister(&PrometheusDefaultMetrics) + + var buf strings.Builder + reg.WriteMetrics(&buf, "") + + require.Contains(t, buf.String(), metricNamespace+"_streams") + require.Contains(t, buf.String(), metricNamespace+"_protocols_count") + require.Contains(t, buf.String(), metricNamespace+"_identify_total") + require.Contains(t, buf.String(), metricNamespace+"_counter_total") } From 8e6c1af4401d4d1d64fe85236a91b42c73e4d511 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Wed, 12 Jun 2024 15:55:28 -0400 Subject: [PATCH 24/38] post merge: regen config-v34 --- test/testdata/configs/config-v34.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/testdata/configs/config-v34.json b/test/testdata/configs/config-v34.json index 4a9714115f..c5255d63bd 100644 --- a/test/testdata/configs/config-v34.json +++ b/test/testdata/configs/config-v34.json @@ -30,7 +30,7 @@ "ConnectionsRateLimitingWindowSeconds": 1, "CrashDBDir": "", "DNSBootstrapID": ".algorand.network?backup=.algorand.net&dedup=.algorand-.(network|net)", - "DNSSecurityFlags": 1, + "DNSSecurityFlags": 9, "DeadlockDetection": 0, "DeadlockDetectionThreshold": 30, "DisableAPIAuth": false, @@ -43,6 +43,7 @@ "EnableAgreementTimeMetrics": false, "EnableAssembleStats": false, "EnableBlockService": false, + "EnableDHTProviders": false, "EnableDeveloperAPI": false, "EnableExperimentalAPI": false, "EnableFollowMode": false, @@ -53,6 +54,7 @@ "EnableMetricReporting": false, "EnableOutgoingNetworkMessageFiltering": true, "EnableP2P": false, + "EnableP2PHybridMode": false, "EnablePingHandler": true, "EnableProcessBlockStats": false, "EnableProfiler": false, @@ -96,6 +98,7 @@ "OptimizeAccountsDatabaseOnStartup": false, "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, + "P2PListenAddress": "", "P2PPersistPeerID": false, "P2PPrivateKeyLocation": "", "ParticipationKeysRefreshInterval": 60000000000, From a513cbfbd6b480e7e573872be1a886d837672100 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Thu, 13 Jun 2024 17:50:12 -0400 Subject: [PATCH 25/38] remove mis-merged v34 fields from config-v33 --- test/testdata/configs/config-v33.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/testdata/configs/config-v33.json b/test/testdata/configs/config-v33.json index 433efcf82e..d9188ef748 100644 --- a/test/testdata/configs/config-v33.json +++ b/test/testdata/configs/config-v33.json @@ -43,7 +43,6 @@ "EnableAgreementTimeMetrics": false, "EnableAssembleStats": false, "EnableBlockService": false, - "EnableDHTProviders": false, "EnableDeveloperAPI": false, "EnableExperimentalAPI": false, "EnableFollowMode": false, @@ -54,7 +53,6 @@ "EnableMetricReporting": false, "EnableOutgoingNetworkMessageFiltering": true, "EnableP2P": false, - "EnableP2PHybridMode": false, "EnablePingHandler": true, "EnableProcessBlockStats": false, "EnableProfiler": false, @@ -97,7 +95,6 @@ "OptimizeAccountsDatabaseOnStartup": false, "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, - "P2PListenAddress": "", "P2PPersistPeerID": false, "P2PPrivateKeyLocation": "", "ParticipationKeysRefreshInterval": 60000000000, From 0dd0cb70389be11c066e224c7ccd50a3ea4effe1 Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:17:56 -0400 Subject: [PATCH 26/38] p2p: convert additional Multiplexer method to use generic implementation (#6034) --- data/txHandler.go | 2 +- network/gossipNode.go | 13 ++++---- network/multiplexer.go | 66 +++++++++++--------------------------- network/p2pNetwork_test.go | 21 ++++++------ 4 files changed, 36 insertions(+), 66 deletions(-) diff --git a/data/txHandler.go b/data/txHandler.go index bb515db2f7..98b7e55a6b 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -248,7 +248,7 @@ func (handler *TxHandler) Start() { { Tag: protocol.TxnTag, // create anonymous struct to hold the two functions and satisfy the network.MessageProcessor interface - MessageProcessor: struct { + MessageHandler: struct { network.ProcessorValidateFunc network.ProcessorHandleFunc }{ diff --git a/network/gossipNode.go b/network/gossipNode.go index eeeca95167..686d864d40 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -239,18 +239,17 @@ func (f ProcessorHandleFunc) Handle(message ValidatedMessage) OutgoingMessage { return f(message) } -// TaggedMessageHandler receives one type of broadcast messages -type TaggedMessageHandler struct { +type taggedMessageDispatcher[T any] struct { Tag - MessageHandler + MessageHandler T } +// TaggedMessageHandler receives one type of broadcast messages +type TaggedMessageHandler = taggedMessageDispatcher[MessageHandler] + // TaggedMessageProcessor receives one type of broadcast messages // and performs two stage processing: validating and handling -type TaggedMessageProcessor struct { - Tag - MessageProcessor -} +type TaggedMessageProcessor = taggedMessageDispatcher[MessageProcessor] // Propagate is a convenience function to save typing in the common case of a message handler telling us to propagate an incoming message // "return network.Propagate(msg)" instead of "return network.OutgoingMsg{network.Broadcast, msg.Tag, msg.Data}" diff --git a/network/multiplexer.go b/network/multiplexer.go index 2d69259c9d..dc38fba277 100644 --- a/network/multiplexer.go +++ b/network/multiplexer.go @@ -45,16 +45,6 @@ func getMap[T any](source *atomic.Value) map[Tag]T { return nil } -// getHandlersMap retrieves the handlers map. -func (m *Multiplexer) getHandlersMap() map[Tag]MessageHandler { - return getMap[MessageHandler](&m.msgHandlers) -} - -// getProcessorsMap retrieves the processors map. -func (m *Multiplexer) getProcessorsMap() map[Tag]MessageProcessor { - return getMap[MessageProcessor](&m.msgHandlers) -} - // Retrieves the handler for the given message Tag from the given value while. func getHandler[T any](source *atomic.Value, tag Tag) (T, bool) { if handlers := getMap[T](source); handlers != nil { @@ -77,41 +67,31 @@ func (m *Multiplexer) getProcessor(tag Tag) (MessageProcessor, bool) { // Handle is the "input" side of the multiplexer. It dispatches the message to the previously defined handler. func (m *Multiplexer) Handle(msg IncomingMessage) OutgoingMessage { - handler, ok := m.getHandler(msg.Tag) - - if ok { - outmsg := handler.Handle(msg) - return outmsg + if handler, ok := m.getHandler(msg.Tag); ok { + return handler.Handle(msg) } return OutgoingMessage{} } // Validate is an alternative "input" side of the multiplexer. It dispatches the message to the previously defined validator. func (m *Multiplexer) Validate(msg IncomingMessage) ValidatedMessage { - handler, ok := m.getProcessor(msg.Tag) - - if ok { - outmsg := handler.Validate(msg) - return outmsg + if handler, ok := m.getProcessor(msg.Tag); ok { + return handler.Validate(msg) } return ValidatedMessage{} } // Process is the second step of message handling after validation. It dispatches the message to the previously defined processor. func (m *Multiplexer) Process(msg ValidatedMessage) OutgoingMessage { - handler, ok := m.getProcessor(msg.Tag) - - if ok { - outmsg := handler.Handle(msg) - return outmsg + if handler, ok := m.getProcessor(msg.Tag); ok { + return handler.Handle(msg) } return OutgoingMessage{} } -// RegisterHandlers registers the set of given message handlers. -func (m *Multiplexer) RegisterHandlers(dispatch []TaggedMessageHandler) { - mp := make(map[Tag]MessageHandler) - if existingMap := m.getHandlersMap(); existingMap != nil { +func registerMultiplexer[T any](target *atomic.Value, dispatch []taggedMessageDispatcher[T]) { + mp := make(map[Tag]T) + if existingMap := getMap[T](target); existingMap != nil { for k, v := range existingMap { mp[k] = v } @@ -122,28 +102,20 @@ func (m *Multiplexer) RegisterHandlers(dispatch []TaggedMessageHandler) { } mp[v.Tag] = v.MessageHandler } - m.msgHandlers.Store(mp) + target.Store(mp) +} + +// RegisterHandlers registers the set of given message handlers. +func (m *Multiplexer) RegisterHandlers(dispatch []TaggedMessageHandler) { + registerMultiplexer(&m.msgHandlers, dispatch) } // RegisterProcessors registers the set of given message handlers. func (m *Multiplexer) RegisterProcessors(dispatch []TaggedMessageProcessor) { - mp := make(map[Tag]MessageProcessor) - if existingMap := m.getProcessorsMap(); existingMap != nil { - for k, v := range existingMap { - mp[k] = v - } - } - for _, v := range dispatch { - if _, has := mp[v.Tag]; has { - panic(fmt.Sprintf("Already registered a handler for tag %v", v.Tag)) - } - mp[v.Tag] = v.MessageProcessor - } - m.msgProcessors.Store(mp) + registerMultiplexer(&m.msgProcessors, dispatch) } -// ClearProcessors deregisters all the existing message handlers other than the one provided in the excludeTags list -func clear[T any](target *atomic.Value, excludeTags []Tag) { +func clearMultiplexer[T any](target *atomic.Value, excludeTags []Tag) { if len(excludeTags) == 0 { target.Store(make(map[Tag]T)) return @@ -168,10 +140,10 @@ func clear[T any](target *atomic.Value, excludeTags []Tag) { // ClearHandlers deregisters all the existing message handlers other than the one provided in the excludeTags list func (m *Multiplexer) ClearHandlers(excludeTags []Tag) { - clear[MessageHandler](&m.msgHandlers, excludeTags) + clearMultiplexer[MessageHandler](&m.msgHandlers, excludeTags) } // ClearProcessors deregisters all the existing message handlers other than the one provided in the excludeTags list func (m *Multiplexer) ClearProcessors(excludeTags []Tag) { - clear[MessageProcessor](&m.msgProcessors, excludeTags) + clearMultiplexer[MessageProcessor](&m.msgProcessors, excludeTags) } diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 0206871a54..715e8e6fab 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -103,7 +103,7 @@ func TestP2PSubmitTX(t *testing.T) { passThroughHandler := []TaggedMessageProcessor{ { Tag: protocol.TxnTag, - MessageProcessor: struct { + MessageHandler: struct { ProcessorValidateFunc ProcessorHandleFunc }{ @@ -195,7 +195,7 @@ func TestP2PSubmitTXNoGossip(t *testing.T) { passThroughHandler := []TaggedMessageProcessor{ { Tag: protocol.TxnTag, - MessageProcessor: struct { + MessageHandler: struct { ProcessorValidateFunc ProcessorHandleFunc }{ @@ -829,13 +829,13 @@ func TestP2PRelay(t *testing.T) { return netA.hasPeers() && netB.hasPeers() }, 2*time.Second, 50*time.Millisecond) - makeCounterHandler := func(numExpected int) ([]TaggedMessageProcessor, *int, chan struct{}) { - numActual := 0 + makeCounterHandler := func(numExpected int) ([]TaggedMessageProcessor, *atomic.Uint32, chan struct{}) { + var numActual atomic.Uint32 counterDone := make(chan struct{}) counterHandler := []TaggedMessageProcessor{ { Tag: protocol.TxnTag, - MessageProcessor: struct { + MessageHandler: struct { ProcessorValidateFunc ProcessorHandleFunc }{ @@ -843,8 +843,7 @@ func TestP2PRelay(t *testing.T) { return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatorData: nil} }), ProcessorHandleFunc(func(msg ValidatedMessage) OutgoingMessage { - numActual++ - if numActual >= numExpected { + if count := numActual.Add(1); int(count) >= numExpected { close(counterDone) } return OutgoingMessage{Action: Ignore} @@ -916,10 +915,10 @@ func TestP2PRelay(t *testing.T) { select { case <-counterDone: case <-time.After(2 * time.Second): - if *count < expectedMsgs { - require.Failf(t, "One or more messages failed to reach destination network", "%d > %d", expectedMsgs, *count) - } else if *count > expectedMsgs { - require.Failf(t, "One or more messages that were expected to be dropped, reached destination network", "%d < %d", expectedMsgs, *count) + if c := count.Load(); c < expectedMsgs { + require.Failf(t, "One or more messages failed to reach destination network", "%d > %d", expectedMsgs, c) + } else if c > expectedMsgs { + require.Failf(t, "One or more messages that were expected to be dropped, reached destination network", "%d < %d", expectedMsgs, c) } } } From 8a584dd4b48310352ed42f9506f44523b27cd316 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:35:59 -0400 Subject: [PATCH 27/38] p2p feature PR: CR fixes (#6038) --- agreement/fuzzer/networkFacade_test.go | 2 +- agreement/gossip/network_test.go | 2 +- components/mocks/mockNetwork.go | 2 +- config/localTemplate.go | 4 +-- config/local_defaults.go | 2 +- daemon/algod/server.go | 2 +- data/txHandler.go | 32 ++++++++--------- installer/config.json.example | 2 +- logging/log.go | 2 +- netdeploy/remote/nodecfg/nodeDir.go | 10 +++--- network/connPerfMon_test.go | 2 +- network/gossipNode.go | 14 ++++---- network/hybridNetwork.go | 8 ++--- network/limitcaller/rateLimitingTransport.go | 23 +++++++----- network/p2p/dnsaddr/resolve.go | 2 +- network/p2p/http.go | 2 +- network/p2p/logger.go | 3 +- network/p2p/peerstore/peerstore.go | 32 +++++++++++------ network/p2p/peerstore/peerstore_test.go | 34 +++++++++--------- network/p2p/pubsub.go | 4 ++- network/p2pNetwork.go | 4 +-- network/p2pNetwork_test.go | 6 ++-- network/phonebook/phonebook.go | 12 +++---- network/wsNetwork.go | 6 ++-- node/node.go | 1 + node/node_test.go | 38 ++++++-------------- test/testdata/configs/config-v34.json | 2 +- util/metrics/opencensus_test.go | 6 ++-- 28 files changed, 129 insertions(+), 130 deletions(-) diff --git a/agreement/fuzzer/networkFacade_test.go b/agreement/fuzzer/networkFacade_test.go index 804fb1e7ff..d1ec7bf8a5 100644 --- a/agreement/fuzzer/networkFacade_test.go +++ b/agreement/fuzzer/networkFacade_test.go @@ -348,7 +348,7 @@ func (n *NetworkFacade) ReceiveMessage(sourceNode int, tag protocol.Tag, data [] n.pushPendingReceivedMessage() } -func (n *NetworkFacade) Disconnect(sender network.DisconnectablePeer) { +func (n *NetworkFacade) Disconnect(sender network.DeadlineSettableConn) { sourceNode := n.peerToNode[sender.(*facadePeer)] n.fuzzer.Disconnect(n.nodeID, sourceNode) } diff --git a/agreement/gossip/network_test.go b/agreement/gossip/network_test.go index e1c5f49613..760456e66c 100644 --- a/agreement/gossip/network_test.go +++ b/agreement/gossip/network_test.go @@ -135,7 +135,7 @@ func (w *whiteholeNetwork) Relay(ctx context.Context, tag protocol.Tag, data []b func (w *whiteholeNetwork) BroadcastSimple(tag protocol.Tag, data []byte) error { return w.Broadcast(context.Background(), tag, data, true, nil) } -func (w *whiteholeNetwork) Disconnect(badnode network.DisconnectablePeer) { +func (w *whiteholeNetwork) Disconnect(badnode network.DeadlineSettableConn) { return } func (w *whiteholeNetwork) DisconnectPeers() { diff --git a/components/mocks/mockNetwork.go b/components/mocks/mockNetwork.go index c0b7724e07..a6a55e7ec4 100644 --- a/components/mocks/mockNetwork.go +++ b/components/mocks/mockNetwork.go @@ -60,7 +60,7 @@ func (network *MockNetwork) RequestConnectOutgoing(replace bool, quit <-chan str } // Disconnect - unused function -func (network *MockNetwork) Disconnect(badpeer network.DisconnectablePeer) { +func (network *MockNetwork) Disconnect(badpeer network.DeadlineSettableConn) { } // DisconnectPeers - unused function diff --git a/config/localTemplate.go b/config/localTemplate.go index 8427bbdbf2..2e82f9fa60 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -603,8 +603,8 @@ type Local struct { // EnableP2PHybridMode turns on both websockets and P2P networking. EnableP2PHybridMode bool `version[34]:"false"` - // P2PListenAddress sets the listen address used for P2P networking, if hybrid mode is set. - P2PListenAddress string `version[34]:""` + // P2PNetAddress sets the listen address used for P2P networking, if hybrid mode is set. + P2PNetAddress string `version[34]:""` // EnableDHT will turn on the hash table for use with capabilities advertisement EnableDHTProviders bool `version[34]:"false"` diff --git a/config/local_defaults.go b/config/local_defaults.go index 54fffe1479..ae2ed22ebf 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -119,7 +119,7 @@ var defaultLocal = Local{ OptimizeAccountsDatabaseOnStartup: false, OutgoingMessageFilterBucketCount: 3, OutgoingMessageFilterBucketSize: 128, - P2PListenAddress: "", + P2PNetAddress: "", P2PPersistPeerID: false, P2PPrivateKeyLocation: "", ParticipationKeysRefreshInterval: 60000000000, diff --git a/daemon/algod/server.go b/daemon/algod/server.go index f1f6d44099..5250f7de40 100644 --- a/daemon/algod/server.go +++ b/daemon/algod/server.go @@ -289,7 +289,7 @@ func (s *Server) Start() { fmt.Print("Initializing the Algorand node... ") err := s.node.Start() if err != nil { - msg := fmt.Sprintf("Failed to start alg Algorand node: %v", err) + msg := fmt.Sprintf("Failed to start an Algorand node: %v", err) s.log.Error(msg) fmt.Println(msg) os.Exit(1) diff --git a/data/txHandler.go b/data/txHandler.go index 98b7e55a6b..c797bc41f3 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -240,10 +240,12 @@ func (handler *TxHandler) Start() { if handler.msgCache != nil { handler.msgCache.Start(handler.ctx, 60*time.Second) } + // wsNetwork handler handler.net.RegisterHandlers([]network.TaggedMessageHandler{ {Tag: protocol.TxnTag, MessageHandler: network.HandlerFunc(handler.processIncomingTxn)}, }) + // libp2p pubsub validator and handler abstracted as TaggedMessageProcessor handler.net.RegisterProcessors([]network.TaggedMessageProcessor{ { Tag: protocol.TxnTag, @@ -595,7 +597,7 @@ func (handler *TxHandler) dedupCanonical(unverifiedTxGroup []transactions.Signed // - the key used for insertion if the message was not found in the cache // - the capacity guard returned by the elastic rate limiter // - a boolean indicating if the message was a duplicate or the sender is rate limited -func (handler *TxHandler) incomingMsgDupErlCheck(data []byte, sender network.DisconnectablePeer) (*crypto.Digest, *util.ErlCapacityGuard, bool) { +func (handler *TxHandler) incomingMsgDupErlCheck(data []byte, sender network.DeadlineSettableConn) (*crypto.Digest, *util.ErlCapacityGuard, bool) { var msgKey *crypto.Digest var capguard *util.ErlCapacityGuard var isDup bool @@ -679,7 +681,7 @@ func decodeMsg(data []byte) (unverifiedTxGroup []transactions.SignedTxn, consume // incomingTxGroupDupRateLimit checks // - if the incoming transaction group has been seen before after reencoding to canonical representation, and // - if the sender is rate limited by the per-application rate limiter. -func (handler *TxHandler) incomingTxGroupDupRateLimit(unverifiedTxGroup []transactions.SignedTxn, encodedExpectedSize int, sender network.DisconnectablePeer) (*crypto.Digest, bool) { +func (handler *TxHandler) incomingTxGroupDupRateLimit(unverifiedTxGroup []transactions.SignedTxn, encodedExpectedSize int, sender network.DeadlineSettableConn) (*crypto.Digest, bool) { var canonicalKey *crypto.Digest if handler.txCanonicalCache != nil { var isDup bool @@ -711,11 +713,9 @@ func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) net accepted := false defer func() { // if we failed to put the item onto the backlog, we should release the capacity if any - if !accepted { - if capguard != nil { - if capErr := capguard.Release(); capErr != nil { - logging.Base().Warnf("processIncomingTxn: failed to release capacity to ElasticRateLimiter: %v", capErr) - } + if !accepted && capguard != nil { + if capErr := capguard.Release(); capErr != nil { + logging.Base().Warnf("processIncomingTxn: failed to release capacity to ElasticRateLimiter: %v", capErr) } } }() @@ -779,38 +779,36 @@ func (handler *TxHandler) validateIncomingTxMessage(rawmsg network.IncomingMessa accepted := false defer func() { // if we failed to put the item onto the backlog, we should release the capacity if any - if !accepted { - if capguard != nil { - if capErr := capguard.Release(); capErr != nil { - logging.Base().Warnf("validateIncomingTxMessage: failed to release capacity to ElasticRateLimiter: %v", capErr) - } + if !accepted && capguard != nil { + if capErr := capguard.Release(); capErr != nil { + logging.Base().Warnf("validateIncomingTxMessage: failed to release capacity to ElasticRateLimiter: %v", capErr) } } }() if shouldDrop { // this TX message was found in the duplicate cache, or ERL rate-limited it - return network.ValidatedMessage{Action: network.Ignore, ValidatorData: nil} + return network.ValidatedMessage{Action: network.Ignore, ValidatedMessage: nil} } unverifiedTxGroup, consumed, invalid := decodeMsg(rawmsg.Data) if invalid { // invalid encoding or exceeding txgroup, disconnect from this peer - return network.ValidatedMessage{Action: network.Disconnect, ValidatorData: nil} + return network.ValidatedMessage{Action: network.Disconnect, ValidatedMessage: nil} } canonicalKey, drop := handler.incomingTxGroupDupRateLimit(unverifiedTxGroup, consumed, rawmsg.Sender) if drop { // this re-serialized txgroup was detected as a duplicate by the canonical message cache, // or it was rate-limited by the per-app rate limiter - return network.ValidatedMessage{Action: network.Ignore, ValidatorData: nil} + return network.ValidatedMessage{Action: network.Ignore, ValidatedMessage: nil} } accepted = true return network.ValidatedMessage{ Action: network.Accept, Tag: rawmsg.Tag, - ValidatorData: &validatedIncomingTxMessage{ + ValidatedMessage: &validatedIncomingTxMessage{ rawmsg: rawmsg, unverifiedTxGroup: unverifiedTxGroup, msgKey: msgKey, @@ -822,7 +820,7 @@ func (handler *TxHandler) validateIncomingTxMessage(rawmsg network.IncomingMessa // processIncomingTxMessage is the handler for the MessageProcessor implementation used by P2PNetwork. func (handler *TxHandler) processIncomingTxMessage(validatedMessage network.ValidatedMessage) network.OutgoingMessage { - msg := validatedMessage.ValidatorData.(*validatedIncomingTxMessage) + msg := validatedMessage.ValidatedMessage.(*validatedIncomingTxMessage) select { case handler.backlogQueue <- &txBacklogMsg{ rawmsg: &msg.rawmsg, diff --git a/installer/config.json.example b/installer/config.json.example index c5255d63bd..7f16155303 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -98,7 +98,7 @@ "OptimizeAccountsDatabaseOnStartup": false, "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, - "P2PListenAddress": "", + "P2PNetAddress": "", "P2PPersistPeerID": false, "P2PPrivateKeyLocation": "", "ParticipationKeysRefreshInterval": 60000000000, diff --git a/logging/log.go b/logging/log.go index 47a14ec722..770bf08bb9 100644 --- a/logging/log.go +++ b/logging/log.go @@ -319,7 +319,7 @@ func (l logger) getOutput() io.Writer { } func (l logger) SetJSONFormatter() { - l.entry.Logger.Formatter = &logrus.JSONFormatter{TimestampFormat: "2006-01-02T15:04:05.000000Z07:00"} + l.entry.Logger.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02T15:04:05.000000Z07:00"}) } func (l logger) Entry() *logrus.Entry { diff --git a/netdeploy/remote/nodecfg/nodeDir.go b/netdeploy/remote/nodecfg/nodeDir.go index 26b1c9b8d1..bdfc037438 100644 --- a/netdeploy/remote/nodecfg/nodeDir.go +++ b/netdeploy/remote/nodecfg/nodeDir.go @@ -174,21 +174,21 @@ func (nd *nodeDir) configureP2PDNSBootstrap(p2pBootstrap bool) error { } // ensure p2p config params set are what is expected: // - EnableP2P or EnableP2PHybridMode - // - NetAddress or P2PListenAddress is set + // - NetAddress or P2PNetAddress is set // - EnableGossipService if !nd.config.EnableP2P && !nd.config.EnableP2PHybridMode { return errors.New("p2p bootstrap requires EnableP2P or EnableP2PHybridMode to be set") } - if nd.NetAddress == "" && nd.config.P2PListenAddress == "" { - return errors.New("p2p bootstrap requires NetAddress or P2PListenAddress to be set") + if nd.NetAddress == "" && nd.config.P2PNetAddress == "" { + return errors.New("p2p bootstrap requires NetAddress or P2PNetAddress to be set") } if !nd.config.EnableGossipService { return errors.New("p2p bootstrap requires EnableGossipService to be set") } netAddress := nd.NetAddress - if nd.config.P2PListenAddress != "" { - netAddress = nd.config.P2PListenAddress + if nd.config.P2PNetAddress != "" { + netAddress = nd.config.P2PNetAddress } key, err := p2p.GetPrivKey(config.Local{P2PPersistPeerID: true}, nd.dataDir) diff --git a/network/connPerfMon_test.go b/network/connPerfMon_test.go index 560be72a96..fe10abbd34 100644 --- a/network/connPerfMon_test.go +++ b/network/connPerfMon_test.go @@ -48,7 +48,7 @@ func makeMsgPool(N int, peers []Peer) (out []IncomingMessage) { addMsg := func(msgCount int) { for i := 0; i < msgCount; i++ { - msg.Sender = peers[(int(msgIndex)+i)%len(peers)].(DisconnectablePeer) + msg.Sender = peers[(int(msgIndex)+i)%len(peers)].(DeadlineSettableConn) timer += int64(7 * time.Nanosecond) msg.Received = timer out = append(out, msg) diff --git a/network/gossipNode.go b/network/gossipNode.go index 686d864d40..8356c254dc 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -29,8 +29,8 @@ import ( // Peer opaque interface for referring to a neighbor in the network type Peer interface{} -// DisconnectablePeer is a Peer with a long-living connection to a network that can be disconnected -type DisconnectablePeer interface { +// DeadlineSettableConn is a Peer with a long-living connection to a network that can be disconnected +type DeadlineSettableConn interface { GetNetwork() GossipNode } @@ -62,7 +62,7 @@ type GossipNode interface { Address() (string, bool) Broadcast(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error Relay(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error - Disconnect(badnode DisconnectablePeer) + Disconnect(badnode DeadlineSettableConn) DisconnectPeers() // only used by testing // RegisterHTTPHandler path accepts gorilla/mux path annotations @@ -127,7 +127,7 @@ var outgoingMessagesBufferSize = int( // IncomingMessage represents a message arriving from some peer in our p2p network type IncomingMessage struct { - Sender DisconnectablePeer + Sender DeadlineSettableConn Tag Tag Data []byte Err error @@ -171,9 +171,9 @@ type OutgoingMessage struct { // ValidatedMessage is a message that has been validated and is ready to be processed. // Think as an intermediate one between IncomingMessage and OutgoingMessage type ValidatedMessage struct { - Action ForwardingPolicy - Tag Tag - ValidatorData interface{} + Action ForwardingPolicy + Tag Tag + ValidatedMessage interface{} } // ForwardingPolicy is an enum indicating to whom we should send a message diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go index 6041d95f9a..6f97a04926 100644 --- a/network/hybridNetwork.go +++ b/network/hybridNetwork.go @@ -41,7 +41,7 @@ type HybridP2PNetwork struct { func NewHybridP2PNetwork(log logging.Logger, cfg config.Local, datadir string, phonebookAddresses []string, genesisID string, networkID protocol.NetworkID, nodeInfo NodeInfo) (*HybridP2PNetwork, error) { // supply alternate NetAddress for P2P network p2pcfg := cfg - p2pcfg.NetAddress = cfg.P2PListenAddress + p2pcfg.NetAddress = cfg.P2PNetAddress p2pnet, err := NewP2PNetwork(log, p2pcfg, datadir, phonebookAddresses, genesisID, networkID, nodeInfo) if err != nil { return nil, err @@ -115,7 +115,7 @@ func (n *HybridP2PNetwork) Relay(ctx context.Context, tag protocol.Tag, data []b } // Disconnect implements GossipNode -func (n *HybridP2PNetwork) Disconnect(badnode DisconnectablePeer) { +func (n *HybridP2PNetwork) Disconnect(badnode DeadlineSettableConn) { net := badnode.GetNetwork() if net == n.p2pNetwork { n.p2pNetwork.Disconnect(badnode) @@ -180,13 +180,13 @@ func (n *HybridP2PNetwork) ClearHandlers() { n.wsNetwork.ClearHandlers() } -// RegisterProcessors adds to the set of given message handlers. +// RegisterProcessors adds to the set of given message processors. func (n *HybridP2PNetwork) RegisterProcessors(dispatch []TaggedMessageProcessor) { n.p2pNetwork.RegisterProcessors(dispatch) n.wsNetwork.RegisterProcessors(dispatch) } -// ClearProcessors deregisters all the existing message handlers. +// ClearProcessors deregisters all the existing message processors. func (n *HybridP2PNetwork) ClearProcessors() { n.p2pNetwork.ClearProcessors() n.wsNetwork.ClearProcessors() diff --git a/network/limitcaller/rateLimitingTransport.go b/network/limitcaller/rateLimitingTransport.go index 5b2e4e67f0..45bc0725ed 100644 --- a/network/limitcaller/rateLimitingTransport.go +++ b/network/limitcaller/rateLimitingTransport.go @@ -22,12 +22,13 @@ import ( "time" "github.com/algorand/go-algorand/util" + "github.com/libp2p/go-libp2p/core/peer" ) // ConnectionTimeStore is a subset of the phonebook that is used to store the connection times. type ConnectionTimeStore interface { - GetConnectionWaitTime(addrOrInfo interface{}) (bool, time.Duration, time.Time) - UpdateConnectionTime(addrOrInfo interface{}, provisionalTime time.Time) bool + GetConnectionWaitTime(addrOrPeerID string) (bool, time.Duration, time.Time) + UpdateConnectionTime(addrOrPeerID string, provisionalTime time.Time) bool } // RateLimitingTransport is the transport for execute a single HTTP transaction, obtaining the Response for a given Request. @@ -64,9 +65,9 @@ func MakeRateLimitingTransport(phonebook ConnectionTimeStore, queueingTimeout ti } } -// MakeRateLimitingTransportWithTransport creates a rate limiting http transport that would limit the requests rate +// MakeRateLimitingTransportWithRoundTripper creates a rate limiting http transport that would limit the requests rate // according to the entries in the phonebook. -func MakeRateLimitingTransportWithTransport(phonebook ConnectionTimeStore, queueingTimeout time.Duration, rt http.RoundTripper, target interface{}, maxIdleConnsPerHost int) RateLimitingTransport { +func MakeRateLimitingTransportWithRoundTripper(phonebook ConnectionTimeStore, queueingTimeout time.Duration, rt http.RoundTripper, target interface{}, maxIdleConnsPerHost int) RateLimitingTransport { return RateLimitingTransport{ phonebook: phonebook, innerTransport: rt, @@ -81,13 +82,17 @@ func (r *RateLimitingTransport) RoundTrip(req *http.Request) (res *http.Response var waitTime time.Duration var provisionalTime time.Time queueingDeadline := time.Now().Add(r.queueingTimeout) - var host interface{} = req.Host + addrOrPeerID := req.Host + // p2p/http clients have per-connection transport and address info so use that if len(req.Host) == 0 && req.URL != nil && len(req.URL.Host) == 0 { - // p2p/http clients have per-connection transport and address info so use that - host = r.targetAddr + addrInfo, ok := r.targetAddr.(*peer.AddrInfo) + if !ok { + return nil, errors.New("rateLimitingTransport: request without Host/URL and targetAddr is not a peer.AddrInfo") + } + addrOrPeerID = string(addrInfo.ID) } for { - _, waitTime, provisionalTime = r.phonebook.GetConnectionWaitTime(host) + _, waitTime, provisionalTime = r.phonebook.GetConnectionWaitTime(addrOrPeerID) if waitTime == 0 { break // break out of the loop and proceed to the connection } @@ -99,6 +104,6 @@ func (r *RateLimitingTransport) RoundTrip(req *http.Request) (res *http.Response return nil, ErrConnectionQueueingTimeout } res, err = r.innerTransport.RoundTrip(req) - r.phonebook.UpdateConnectionTime(host, provisionalTime) + r.phonebook.UpdateConnectionTime(addrOrPeerID, provisionalTime) return } diff --git a/network/p2p/dnsaddr/resolve.go b/network/p2p/dnsaddr/resolve.go index dbe2b002a1..5e0e8007fc 100644 --- a/network/p2p/dnsaddr/resolve.go +++ b/network/p2p/dnsaddr/resolve.go @@ -35,7 +35,7 @@ func Iterate(initial multiaddr.Multiaddr, controller ResolveController, f func(d if resolver == nil { return errors.New("passed controller has no resolvers Iterate") } - const maxHops = 100 + const maxHops = 25 // any reasonable number to prevent infinite loop in case of circular dnsaddr hops := 0 var toResolve = []multiaddr.Multiaddr{initial} for resolver != nil && len(toResolve) > 0 { diff --git a/network/p2p/http.go b/network/p2p/http.go index eea40c127e..9f2622d015 100644 --- a/network/p2p/http.go +++ b/network/p2p/http.go @@ -85,7 +85,7 @@ func MakeHTTPClientWithRateLimit(addrInfo *peer.AddrInfo, pstore limitcaller.Con if err != nil { return nil, err } - rlrt := limitcaller.MakeRateLimitingTransportWithTransport(pstore, queueingTimeout, cl.Transport, addrInfo, maxIdleConnsPerHost) + rlrt := limitcaller.MakeRateLimitingTransportWithRoundTripper(pstore, queueingTimeout, cl.Transport, addrInfo, maxIdleConnsPerHost) cl.Transport = &rlrt return cl, nil diff --git a/network/p2p/logger.go b/network/p2p/logger.go index 8ac9dc7b97..cde67f7f58 100644 --- a/network/p2p/logger.go +++ b/network/p2p/logger.go @@ -70,7 +70,8 @@ func EnableP2PLogging(log logging.Logger, l logging.Level) { } func (c *loggingCore) Enabled(l zapcore.Level) bool { - return c.log.IsLevelEnabled(c.level) + level := levelsMap[l] + return c.log.IsLevelEnabled(level) } func (c *loggingCore) With(fields []zapcore.Field) zapcore.Core { diff --git a/network/p2p/peerstore/peerstore.go b/network/p2p/peerstore/peerstore.go index 9bbb48ab46..3eda0d3686 100644 --- a/network/p2p/peerstore/peerstore.go +++ b/network/p2p/peerstore/peerstore.go @@ -23,6 +23,7 @@ import ( "time" "github.com/algorand/go-algorand/network/phonebook" + "github.com/algorand/go-deadlock" "github.com/libp2p/go-libp2p/core/peer" libp2p "github.com/libp2p/go-libp2p/core/peerstore" mempstore "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" @@ -53,6 +54,7 @@ type addressData struct { // networkNames: lists the networks to which the given address belongs. networkNames map[string]bool + mu *deadlock.RWMutex // role is the role that this address serves. role phonebook.PhoneBookEntryRoles @@ -127,12 +129,12 @@ func (ps *PeerStore) UpdateRetryAfter(addr string, retryAfter time.Time) { // The connection should be established when the waitTime is 0. // It will register a provisional next connection time when the waitTime is 0. // The provisional time should be updated after the connection with UpdateConnectionTime -func (ps *PeerStore) GetConnectionWaitTime(addr interface{}) (bool, time.Duration, time.Time) { +func (ps *PeerStore) GetConnectionWaitTime(addrOrPeerID string) (bool, time.Duration, time.Time) { curTime := time.Now() - info := addr.(*peer.AddrInfo) var timeSince time.Duration var numElmtsToRemove int - metadata, err := ps.Get(info.ID, addressDataKey) + peerID := peer.ID(addrOrPeerID) + metadata, err := ps.Get(peerID, addressDataKey) if err != nil { return false, 0 /* not used */, curTime /* not used */ } @@ -151,9 +153,9 @@ func (ps *PeerStore) GetConnectionWaitTime(addr interface{}) (bool, time.Duratio } // Remove the expired elements from e.data[addr].recentConnectionTimes - ps.popNElements(numElmtsToRemove, info.ID) + ps.popNElements(numElmtsToRemove, peerID) // If there are max number of connections within the time window, wait - metadata, _ = ps.Get(info.ID, addressDataKey) + metadata, _ = ps.Get(peerID, addressDataKey) ad, ok = metadata.(addressData) if !ok { return false, 0 /* not used */, curTime /* not used */ @@ -169,14 +171,14 @@ func (ps *PeerStore) GetConnectionWaitTime(addr interface{}) (bool, time.Duratio // Update curTime, since it may have significantly changed if waited provisionalTime := time.Now() // Append the provisional time for the next connection request - ps.appendTime(info.ID, provisionalTime) + ps.appendTime(peerID, provisionalTime) return true, 0 /* no wait. proceed */, provisionalTime } // UpdateConnectionTime updates the connection time for the given address. -func (ps *PeerStore) UpdateConnectionTime(addr interface{}, provisionalTime time.Time) bool { - info := addr.(*peer.AddrInfo) - metadata, err := ps.Get(info.ID, addressDataKey) +func (ps *PeerStore) UpdateConnectionTime(addrOrPeerID string, provisionalTime time.Time) bool { + peerID := peer.ID(addrOrPeerID) + metadata, err := ps.Get(peerID, addressDataKey) if err != nil { return false } @@ -185,7 +187,7 @@ func (ps *PeerStore) UpdateConnectionTime(addr interface{}, provisionalTime time return false } defer func() { - _ = ps.Put(info.ID, addressDataKey, ad) + _ = ps.Put(peerID, addressDataKey, ad) }() @@ -216,9 +218,11 @@ func (ps *PeerStore) ReplacePeerList(addressesThey []interface{}, networkName st data, _ := ps.Get(pid, addressDataKey) if data != nil { ad := data.(addressData) + ad.mu.RLock() if ad.networkNames[networkName] && ad.role == role && !ad.persistent { removeItems[pid] = true } + ad.mu.RUnlock() } } @@ -229,7 +233,9 @@ func (ps *PeerStore) ReplacePeerList(addressesThey []interface{}, networkName st // we already have this. // Update the networkName ad := data.(addressData) + ad.mu.Lock() ad.networkNames[networkName] = true + ad.mu.Unlock() // do not remove this entry delete(removeItems, info.ID) @@ -278,6 +284,7 @@ func (ps *PeerStore) Length() int { func makePhonebookEntryData(networkName string, role phonebook.PhoneBookEntryRoles, persistent bool) addressData { pbData := addressData{ networkNames: make(map[string]bool), + mu: &deadlock.RWMutex{}, recentConnectionTimes: make([]time.Time, 0), role: role, persistent: persistent, @@ -292,8 +299,11 @@ func (ps *PeerStore) deletePhonebookEntry(peerID peer.ID, networkName string) { return } ad := data.(addressData) + ad.mu.Lock() delete(ad.networkNames, networkName) - if len(ad.networkNames) == 0 { + isEmpty := len(ad.networkNames) == 0 + ad.mu.Unlock() + if isEmpty { ps.ClearAddrs(peerID) _ = ps.Put(peerID, addressDataKey, nil) } diff --git a/network/p2p/peerstore/peerstore_test.go b/network/p2p/peerstore/peerstore_test.go index f0c67a93b4..e855013d76 100644 --- a/network/p2p/peerstore/peerstore_test.go +++ b/network/p2p/peerstore/peerstore_test.go @@ -337,17 +337,17 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { info2, _ := peerInfoFromDomainPort(addr2) // Address not in. Should return false - addrInPhonebook, _, provisionalTime := entries.GetConnectionWaitTime(info1) + addrInPhonebook, _, provisionalTime := entries.GetConnectionWaitTime(string(info1.ID)) require.Equal(t, false, addrInPhonebook) - require.Equal(t, false, entries.UpdateConnectionTime(info1, provisionalTime)) + require.Equal(t, false, entries.UpdateConnectionTime(string(info1.ID), provisionalTime)) // Test the addresses are populated in the phonebook and a // time can be added to one of them entries.ReplacePeerList([]interface{}{info1, info2}, "default", PhoneBookEntryRelayRole) - addrInPhonebook, waitTime, provisionalTime := entries.GetConnectionWaitTime(info1) + addrInPhonebook, waitTime, provisionalTime := entries.GetConnectionWaitTime(string(info1.ID)) require.Equal(t, true, addrInPhonebook) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(info1, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(string(info1.ID), provisionalTime)) data, _ := entries.Get(info1.ID, addressDataKey) require.NotNil(t, data) ad := data.(addressData) @@ -360,9 +360,9 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { } // add another value to addr - addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(info1) + addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(string(info1.ID)) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(info1, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(string(info1.ID), provisionalTime)) data, _ = entries.Get(info1.ID, addressDataKey) ad = data.(addressData) phBookData = ad.recentConnectionTimes @@ -375,9 +375,9 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { // the first time should be removed and a new one added // there should not be any wait - addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(info1) + addrInPhonebook, waitTime, provisionalTime = entries.GetConnectionWaitTime(string(info1.ID)) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(info1, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(string(info1.ID), provisionalTime)) data, _ = entries.Get(info1.ID, addressDataKey) ad = data.(addressData) phBookData2 := ad.recentConnectionTimes @@ -392,9 +392,9 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { // add 3 values to another address. should not wait // value 1 - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(string(info2.ID)) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(info2, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(string(info2.ID), provisionalTime)) // introduce a gap between the two requests so that only the first will be removed later when waited // simulate passing a unit of time @@ -406,13 +406,13 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { } // value 2 - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(string(info2.ID)) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(info2, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(string(info2.ID), provisionalTime)) // value 3 - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(string(info2.ID)) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(info2, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(string(info2.ID), provisionalTime)) data2, _ = entries.Get(info2.ID, addressDataKey) ad2 = data2.(addressData) @@ -421,7 +421,7 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { require.Equal(t, 3, len(phBookData)) // add another element to trigger wait - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(string(info2.ID)) require.Greater(t, int64(waitTime), int64(0)) // no element should be removed data2, _ = entries.Get(info2.ID, addressDataKey) @@ -436,9 +436,9 @@ func TestWaitAndAddConnectionTimeLongtWindow(t *testing.T) { } // The wait should be sufficient - _, waitTime, provisionalTime = entries.GetConnectionWaitTime(info2) + _, waitTime, provisionalTime = entries.GetConnectionWaitTime(string(info2.ID)) require.Equal(t, time.Duration(0), waitTime) - require.Equal(t, true, entries.UpdateConnectionTime(info2, provisionalTime)) + require.Equal(t, true, entries.UpdateConnectionTime(string(info2.ID), provisionalTime)) // only one element should be removed, and one added data2, _ = entries.Get(info2.ID, addressDataKey) ad2 = data2.(addressData) diff --git a/network/p2p/pubsub.go b/network/p2p/pubsub.go index 8e67ac6725..a968bcb6a9 100644 --- a/network/p2p/pubsub.go +++ b/network/p2p/pubsub.go @@ -53,6 +53,8 @@ const ( // TXTopicName defines a pubsub topic for TX messages const TXTopicName = "/algo/tx/0.1.0" +const incomingThreads = 20 // matches to number wsNetwork workers + func makePubSub(ctx context.Context, cfg config.Local, host host.Host) (*pubsub.PubSub, error) { //defaultParams := pubsub.DefaultGossipSubParams() @@ -95,7 +97,7 @@ func makePubSub(ctx context.Context, cfg config.Local, host host.Host) (*pubsub. pubsub.WithValidateQueueSize(256), pubsub.WithMessageSignaturePolicy(pubsub.StrictNoSign), // pubsub.WithValidateThrottle(cfg.TxBacklogSize), - pubsub.WithValidateWorkers(20), // match to number wsNetwork workers + pubsub.WithValidateWorkers(incomingThreads), } return pubsub.NewGossipSub(ctx, host, options...) diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 22da1915f3..95cc3295f4 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -527,7 +527,7 @@ func (n *P2PNetwork) Relay(ctx context.Context, tag protocol.Tag, data []byte, w } // Disconnect from a peer, probably due to protocol errors. -func (n *P2PNetwork) Disconnect(badpeer DisconnectablePeer) { +func (n *P2PNetwork) Disconnect(badpeer DeadlineSettableConn) { var peerID peer.ID var wsp *wsPeer @@ -555,7 +555,7 @@ func (n *P2PNetwork) Disconnect(badpeer DisconnectablePeer) { } } -func (n *P2PNetwork) disconnectThread(badnode DisconnectablePeer, reason disconnectReason) { +func (n *P2PNetwork) disconnectThread(badnode DeadlineSettableConn, reason disconnectReason) { defer n.wg.Done() n.Disconnect(badnode) // ignores reason } diff --git a/network/p2pNetwork_test.go b/network/p2pNetwork_test.go index 715e8e6fab..5bd582ead0 100644 --- a/network/p2pNetwork_test.go +++ b/network/p2pNetwork_test.go @@ -108,7 +108,7 @@ func TestP2PSubmitTX(t *testing.T) { ProcessorHandleFunc }{ ProcessorValidateFunc(func(msg IncomingMessage) ValidatedMessage { - return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatorData: nil} + return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatedMessage: nil} }), ProcessorHandleFunc(func(msg ValidatedMessage) OutgoingMessage { return OutgoingMessage{Action: Ignore} @@ -200,7 +200,7 @@ func TestP2PSubmitTXNoGossip(t *testing.T) { ProcessorHandleFunc }{ ProcessorValidateFunc(func(msg IncomingMessage) ValidatedMessage { - return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatorData: nil} + return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatedMessage: nil} }), ProcessorHandleFunc(func(msg ValidatedMessage) OutgoingMessage { return OutgoingMessage{Action: Ignore} @@ -840,7 +840,7 @@ func TestP2PRelay(t *testing.T) { ProcessorHandleFunc }{ ProcessorValidateFunc(func(msg IncomingMessage) ValidatedMessage { - return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatorData: nil} + return ValidatedMessage{Action: Accept, Tag: msg.Tag, ValidatedMessage: nil} }), ProcessorHandleFunc(func(msg ValidatedMessage) OutgoingMessage { if count := numActual.Add(1); int(count) >= numExpected { diff --git a/network/phonebook/phonebook.go b/network/phonebook/phonebook.go index a1486c3d5a..634ca9c16c 100644 --- a/network/phonebook/phonebook.go +++ b/network/phonebook/phonebook.go @@ -55,12 +55,12 @@ type Phonebook interface { // The connection should be established when the waitTime is 0. // It will register a provisional next connection time when the waitTime is 0. // The provisional time should be updated after the connection with UpdateConnectionTime - GetConnectionWaitTime(addr interface{}) (addrInPhonebook bool, + GetConnectionWaitTime(addrOrPeerID string) (addrInPhonebook bool, waitTime time.Duration, provisionalTime time.Time) // UpdateConnectionTime will update the provisional connection time. // Returns true of the addr was in the phonebook - UpdateConnectionTime(addr interface{}, provisionalTime time.Time) bool + UpdateConnectionTime(addrOrPeerID string, provisionalTime time.Time) bool // ReplacePeerList merges a set of addresses with that passed in for networkName // new entries in dnsAddresses are being added @@ -231,10 +231,10 @@ func (e *phonebookImpl) UpdateRetryAfter(addr string, retryAfter time.Time) { // The connection should be established when the waitTime is 0. // It will register a provisional next connection time when the waitTime is 0. // The provisional time should be updated after the connection with UpdateConnectionTime -func (e *phonebookImpl) GetConnectionWaitTime(a interface{}) (addrInPhonebook bool, +func (e *phonebookImpl) GetConnectionWaitTime(addrOrPeerID string) (addrInPhonebook bool, waitTime time.Duration, provisionalTime time.Time) { - addr := a.(string) + addr := addrOrPeerID e.lock.Lock() defer e.lock.Unlock() @@ -278,8 +278,8 @@ func (e *phonebookImpl) GetConnectionWaitTime(a interface{}) (addrInPhonebook bo // UpdateConnectionTime will update the provisional connection time. // Returns true of the addr was in the phonebook -func (e *phonebookImpl) UpdateConnectionTime(a interface{}, provisionalTime time.Time) bool { - addr := a.(string) +func (e *phonebookImpl) UpdateConnectionTime(addrOrPeerID string, provisionalTime time.Time) bool { + addr := addrOrPeerID e.lock.Lock() defer e.lock.Unlock() diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 92a32091c4..090fcccd87 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -362,7 +362,7 @@ type networkPeerManager interface { // used by msgHandler Broadcast(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error - disconnectThread(badnode DisconnectablePeer, reason disconnectReason) + disconnectThread(badnode DeadlineSettableConn, reason disconnectReason) checkPeersConnectivity() } @@ -477,13 +477,13 @@ func (wn *WebsocketNetwork) RelayArray(ctx context.Context, tags []protocol.Tag, return nil } -func (wn *WebsocketNetwork) disconnectThread(badnode DisconnectablePeer, reason disconnectReason) { +func (wn *WebsocketNetwork) disconnectThread(badnode DeadlineSettableConn, reason disconnectReason) { defer wn.wg.Done() wn.disconnect(badnode, reason) } // Disconnect from a peer, probably due to protocol errors. -func (wn *WebsocketNetwork) Disconnect(node DisconnectablePeer) { +func (wn *WebsocketNetwork) Disconnect(node DeadlineSettableConn) { wn.disconnect(node, disconnectBadData) } diff --git a/node/node.go b/node/node.go index bfb32ba3ff..e5bbc19516 100644 --- a/node/node.go +++ b/node/node.go @@ -463,6 +463,7 @@ func (node *AlgorandFullNode) Stop() { node.highPriorityCryptoVerificationPool.Shutdown() node.lowPriorityCryptoVerificationPool.Shutdown() node.cryptoPool.Shutdown() + node.ledger.Close() node.cancelCtx() } diff --git a/node/node_test.go b/node/node_test.go index 361f3478f7..080c6b54d8 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -45,7 +45,6 @@ import ( "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util" "github.com/algorand/go-algorand/util/db" - "github.com/algorand/go-algorand/util/execpool" ) var expectedAgreementTime = 2*config.Protocol.BigLambda + config.Protocol.SmallLambda + config.Consensus[protocol.ConsensusCurrentVersion].AgreementFilterTimeout + 2*time.Second @@ -86,7 +85,7 @@ func (ni nodeInfo) p2pMultiAddr() string { type configHook func(ni nodeInfo, cfg config.Local) (nodeInfo, config.Local) type phonebookHook func([]nodeInfo, int) []string -func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationPool execpool.BacklogPool, customConsensus config.ConsensusProtocols) ([]*AlgorandFullNode, []string) { +func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, customConsensus config.ConsensusProtocols) ([]*AlgorandFullNode, []string) { minMoneyAtStart := 10000 maxMoneyAtStart := 100000 gen := rand.New(rand.NewSource(2)) @@ -111,14 +110,14 @@ func setupFullNodes(t *testing.T, proto protocol.ConsensusVersion, verificationP } return phonebook } - nodes, wallets := setupFullNodesEx(t, proto, verificationPool, customConsensus, acctStake, configHook, phonebookHook) + nodes, wallets := setupFullNodesEx(t, proto, customConsensus, acctStake, configHook, phonebookHook) require.Len(t, nodes, numAccounts) require.Len(t, wallets, numAccounts) return nodes, wallets } func setupFullNodesEx( - t *testing.T, proto protocol.ConsensusVersion, verificationPool execpool.BacklogPool, customConsensus config.ConsensusProtocols, + t *testing.T, proto protocol.ConsensusVersion, customConsensus config.ConsensusProtocols, acctStake []basics.MicroAlgos, configHook configHook, phonebookHook phonebookHook, ) ([]*AlgorandFullNode, []string) { @@ -264,10 +263,7 @@ func TestSyncingFullNode(t *testing.T) { t.Skip("Test is too heavy for amd64 builder running in parallel with other packages") } - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - defer backlogPool.Shutdown() - - nodes, wallets := setupFullNodes(t, protocol.ConsensusCurrentVersion, backlogPool, nil) + nodes, wallets := setupFullNodes(t, protocol.ConsensusCurrentVersion, nil) for i := 0; i < len(nodes); i++ { defer os.Remove(wallets[i]) defer nodes[i].Stop() @@ -329,10 +325,7 @@ func TestInitialSync(t *testing.T) { t.Skip("Test is too heavy for amd64 builder running in parallel with other packages") } - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - defer backlogPool.Shutdown() - - nodes, wallets := setupFullNodes(t, protocol.ConsensusCurrentVersion, backlogPool, nil) + nodes, wallets := setupFullNodes(t, protocol.ConsensusCurrentVersion, nil) for i := 0; i < len(nodes); i++ { defer os.Remove(wallets[i]) defer nodes[i].Stop() @@ -370,9 +363,6 @@ func TestSimpleUpgrade(t *testing.T) { t.Skip("Test is too heavy for amd64 builder running in parallel with other packages") } - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - defer backlogPool.Shutdown() - // ConsensusTest0 is a version of ConsensusV0 used for testing // (it has different approved upgrade paths). const consensusTest0 = protocol.ConsensusVersion("test0") @@ -409,7 +399,7 @@ func TestSimpleUpgrade(t *testing.T) { testParams1.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} configurableConsensus[consensusTest1] = testParams1 - nodes, wallets := setupFullNodes(t, consensusTest0, backlogPool, configurableConsensus) + nodes, wallets := setupFullNodes(t, consensusTest0, configurableConsensus) for i := 0; i < len(nodes); i++ { defer os.Remove(wallets[i]) defer nodes[i].Stop() @@ -896,16 +886,14 @@ func TestNodeHybridTopology(t *testing.T) { ni.p2pID, err = p2p.PeerIDFromPublicKey(privKey.GetPublic()) require.NoError(t, err) - cfg.P2PListenAddress = ni.p2pNetAddr() + cfg.P2PNetAddress = ni.p2pNetAddr() return ni, cfg } phonebookHook := func(ni []nodeInfo, i int) []string { switch i { case 0: - // node 0 (N) only accept connections to work around the peer selector - // ConnectedOut priority. TODO: merge switching to archival peers from master - // when ready. + // node 0 (N) only accept connections at the beginning to learn about archival node from DHT t.Logf("Node%d phonebook: empty", i) return []string{} case 1: @@ -923,10 +911,7 @@ func TestNodeHybridTopology(t *testing.T) { return nil } - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - defer backlogPool.Shutdown() - - nodes, wallets := setupFullNodesEx(t, consensusTest0, backlogPool, configurableConsensus, acctStake, configHook, phonebookHook) + nodes, wallets := setupFullNodesEx(t, consensusTest0, configurableConsensus, acctStake, configHook, phonebookHook) require.Len(t, nodes, 3) require.Len(t, wallets, 3) for i := 0; i < len(nodes); i++ { @@ -1017,10 +1002,7 @@ func TestNodeP2PRelays(t *testing.T) { return nil } - backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) - defer backlogPool.Shutdown() - - nodes, wallets := setupFullNodesEx(t, consensusTest0, backlogPool, configurableConsensus, acctStake, configHook, phonebookHook) + nodes, wallets := setupFullNodesEx(t, consensusTest0, configurableConsensus, acctStake, configHook, phonebookHook) require.Len(t, nodes, 3) require.Len(t, wallets, 3) for i := 0; i < len(nodes); i++ { diff --git a/test/testdata/configs/config-v34.json b/test/testdata/configs/config-v34.json index c5255d63bd..7f16155303 100644 --- a/test/testdata/configs/config-v34.json +++ b/test/testdata/configs/config-v34.json @@ -98,7 +98,7 @@ "OptimizeAccountsDatabaseOnStartup": false, "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, - "P2PListenAddress": "", + "P2PNetAddress": "", "P2PPersistPeerID": false, "P2PPrivateKeyLocation": "", "ParticipationKeysRefreshInterval": 60000000000, diff --git a/util/metrics/opencensus_test.go b/util/metrics/opencensus_test.go index b1f5ff102a..f5401af541 100644 --- a/util/metrics/opencensus_test.go +++ b/util/metrics/opencensus_test.go @@ -37,9 +37,9 @@ func TestDHTOpenCensusMetrics(t *testing.T) { defaultBytesDistribution := view.Distribution(1024, 2048, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824, 4294967296) - keyMessageType, _ := tag.NewKey("message_type") - keyPeerID, _ := tag.NewKey("peer_id") - keyInstanceID, _ := tag.NewKey("instance_id") + keyMessageType := tag.MustNewKey("message_type") + keyPeerID := tag.MustNewKey("peer_id") + keyInstanceID := tag.MustNewKey("instance_id") sentMessages := stats.Int64("my_sent_messages", "Total number of messages sent per RPC", stats.UnitDimensionless) receivedBytes := stats.Int64("my_received_bytes", "Total received bytes per RPC", stats.UnitBytes) From 63bf1a93fcac4c9e86fc74428138550d1c00d7dc Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Fri, 21 Jun 2024 19:31:04 -0400 Subject: [PATCH 28/38] config: document EnableP2PHybridMode takes precedence over EnableP2P --- config/localTemplate.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/localTemplate.go b/config/localTemplate.go index 2e82f9fa60..314b83a78b 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -597,7 +597,8 @@ type Local struct { // When it exceeds this capacity, it redirects the block requests to a different node BlockServiceMemCap uint64 `version[28]:"500000000"` - // EnableP2P turns on the peer to peer network + // EnableP2P turns on the peer to peer network. + // When both EnableP2P and EnableP2PHybridMode (below) are set, EnableP2PHybridMode takes precedence. EnableP2P bool `version[31]:"false"` // EnableP2PHybridMode turns on both websockets and P2P networking. From 66a1d354417bd89f147b28f951004f3f33c0e066 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:22:58 -0400 Subject: [PATCH 29/38] Update node/node_test.go --- node/node_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/node/node_test.go b/node/node_test.go index 080c6b54d8..a86e945bbe 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -165,9 +165,6 @@ func setupFullNodesEx( t.Logf("Root directory of node %d (%s): %s\n", i, ni.wsNetAddr(), rootDirectory) - // // Save empty phonebook - we'll add peers after they've been assigned listening ports - // err := config.SavePhonebookToDisk(make([]string, 0), rootDirectory) - // require.NoError(t, err) genesisDir := filepath.Join(rootDirectory, g.ID()) os.Mkdir(genesisDir, 0700) From a6248b29a0e4518c68d71fc60257fd4d95267daf Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:03:53 -0400 Subject: [PATCH 30/38] p2p: more CR fixes: ERL and DHT err logging (#6040) Co-authored-by: chris erway --- data/txHandler.go | 70 ++++++++++++-------------------- network/p2p/capabilities.go | 5 ++- network/p2p/capabilities_test.go | 6 ++- network/p2pNetwork.go | 5 ++- 4 files changed, 36 insertions(+), 50 deletions(-) diff --git a/data/txHandler.go b/data/txHandler.go index a66de93ea3..15e43dbc91 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -595,24 +595,30 @@ func (handler *TxHandler) dedupCanonical(unverifiedTxGroup []transactions.Signed return &d, false } -// incomingMsgDupErlCheck runs the duplicate and rate limiting checks on a raw incoming messages. +// incomingMsgDupCheck runs the duplicate check on a raw incoming message. // Returns: // - the key used for insertion if the message was not found in the cache -// - the capacity guard returned by the elastic rate limiter -// - a boolean indicating if the message was a duplicate or the sender is rate limited -func (handler *TxHandler) incomingMsgDupErlCheck(data []byte, sender network.DeadlineSettableConn) (*crypto.Digest, *util.ErlCapacityGuard, bool) { +// - a boolean indicating if the message was a duplicate +func (handler *TxHandler) incomingMsgDupCheck(data []byte) (*crypto.Digest, bool) { var msgKey *crypto.Digest - var capguard *util.ErlCapacityGuard var isDup bool if handler.msgCache != nil { // check for duplicate messages // this helps against relaying duplicates if msgKey, isDup = handler.msgCache.CheckAndPut(data); isDup { transactionMessagesDupRawMsg.Inc(nil) - return msgKey, capguard, true + return msgKey, true } } + return msgKey, false +} +// incomingMsgErlCheck runs the rate limiting check on a sender. +// Returns: +// - the capacity guard returned by the elastic rate limiter +// - a boolean indicating if the sender is rate limited +func (handler *TxHandler) incomingMsgErlCheck(sender network.DeadlineSettableConn) (*util.ErlCapacityGuard, bool) { + var capguard *util.ErlCapacityGuard var err error if handler.erl != nil { congestedERL := float64(cap(handler.backlogQueue))*handler.backlogCongestionThreshold < float64(len(handler.backlogQueue)) @@ -625,14 +631,14 @@ func (handler *TxHandler) incomingMsgDupErlCheck(data []byte, sender network.Dea handler.erl.EnableCongestionControl() // if there is no capacity, it is the same as if we failed to put the item onto the backlog, so report such transactionMessagesDroppedFromBacklog.Inc(nil) - return msgKey, capguard, true + return capguard, true } // if the backlog Queue has 50% of its buffer back, turn congestion control off if !congestedERL { handler.erl.DisableCongestionControl() } } - return msgKey, capguard, false + return capguard, false } // decodeMsg decodes TX message buffer into transactions.SignedTxn, @@ -711,8 +717,12 @@ func (handler *TxHandler) incomingTxGroupDupRateLimit(unverifiedTxGroup []transa // - message are checked for duplicates // - transactions are checked for duplicates func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) network.OutgoingMessage { - msgKey, capguard, shouldDrop := handler.incomingMsgDupErlCheck(rawmsg.Data, rawmsg.Sender) + msgKey, shouldDrop := handler.incomingMsgDupCheck(rawmsg.Data) + if shouldDrop { + return network.OutgoingMessage{Action: network.Ignore} + } + capguard, shouldDrop := handler.incomingMsgErlCheck(rawmsg.Sender) accepted := false defer func() { // if we failed to put the item onto the backlog, we should release the capacity if any @@ -724,7 +734,7 @@ func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) net }() if shouldDrop { - // this TX message was found in the duplicate cache, or ERL rate-limited it + // this TX message was rate-limited by ERL return network.OutgoingMessage{Action: network.Ignore} } @@ -756,12 +766,7 @@ func (handler *TxHandler) processIncomingTxn(rawmsg network.IncomingMessage) net transactionMessagesDroppedFromBacklog.Inc(nil) // additionally, remove the txn from duplicate caches to ensure it can be re-submitted - if handler.txCanonicalCache != nil && canonicalKey != nil { - handler.txCanonicalCache.Delete(canonicalKey) - } - if handler.msgCache != nil && msgKey != nil { - handler.msgCache.DeleteByKey(msgKey) - } + handler.deleteFromCaches(msgKey, canonicalKey) } return network.OutgoingMessage{Action: network.Ignore} @@ -772,25 +777,12 @@ type validatedIncomingTxMessage struct { unverifiedTxGroup []transactions.SignedTxn msgKey *crypto.Digest canonicalKey *crypto.Digest - capguard *util.ErlCapacityGuard } // validateIncomingTxMessage is the validator for the MessageProcessor implementation used by P2PNetwork. func (handler *TxHandler) validateIncomingTxMessage(rawmsg network.IncomingMessage) network.ValidatedMessage { - msgKey, capguard, shouldDrop := handler.incomingMsgDupErlCheck(rawmsg.Data, rawmsg.Sender) - - accepted := false - defer func() { - // if we failed to put the item onto the backlog, we should release the capacity if any - if !accepted && capguard != nil { - if capErr := capguard.Release(); capErr != nil { - logging.Base().Warnf("validateIncomingTxMessage: failed to release capacity to ElasticRateLimiter: %v", capErr) - } - } - }() - - if shouldDrop { - // this TX message was found in the duplicate cache, or ERL rate-limited it + msgKey, isDup := handler.incomingMsgDupCheck(rawmsg.Data) + if isDup { return network.ValidatedMessage{Action: network.Ignore, ValidatedMessage: nil} } @@ -807,7 +799,6 @@ func (handler *TxHandler) validateIncomingTxMessage(rawmsg network.IncomingMessa return network.ValidatedMessage{Action: network.Ignore, ValidatedMessage: nil} } - accepted = true return network.ValidatedMessage{ Action: network.Accept, Tag: rawmsg.Tag, @@ -816,7 +807,6 @@ func (handler *TxHandler) validateIncomingTxMessage(rawmsg network.IncomingMessa unverifiedTxGroup: unverifiedTxGroup, msgKey: msgKey, canonicalKey: canonicalKey, - capguard: capguard, }, } } @@ -830,7 +820,7 @@ func (handler *TxHandler) processIncomingTxMessage(validatedMessage network.Vali unverifiedTxGroup: msg.unverifiedTxGroup, rawmsgDataHash: msg.msgKey, unverifiedTxGroupHash: msg.canonicalKey, - capguard: msg.capguard, + capguard: nil, }: default: // if we failed here we want to increase the corresponding metric. It might suggest that we @@ -838,17 +828,7 @@ func (handler *TxHandler) processIncomingTxMessage(validatedMessage network.Vali transactionMessagesDroppedFromBacklog.Inc(nil) // additionally, remove the txn from duplicate caches to ensure it can be re-submitted - if handler.txCanonicalCache != nil && msg.canonicalKey != nil { - handler.txCanonicalCache.Delete(msg.canonicalKey) - } - if handler.msgCache != nil && msg.msgKey != nil { - handler.msgCache.DeleteByKey(msg.msgKey) - } - if msg.capguard != nil { - if capErr := msg.capguard.Release(); capErr != nil { - logging.Base().Warnf("processIncomingTxMessage: failed to release capacity to ElasticRateLimiter: %v", capErr) - } - } + handler.deleteFromCaches(msg.msgKey, msg.canonicalKey) } return network.OutgoingMessage{Action: network.Ignore} } diff --git a/network/p2p/capabilities.go b/network/p2p/capabilities.go index f489773975..e5781aa389 100644 --- a/network/p2p/capabilities.go +++ b/network/p2p/capabilities.go @@ -67,9 +67,10 @@ func (c *CapabilitiesDiscovery) FindPeers(ctx context.Context, ns string, opts . } // Close should be called when fully shutting down the node -func (c *CapabilitiesDiscovery) Close() { - _ = c.dht.Close() +func (c *CapabilitiesDiscovery) Close() error { + err := c.dht.Close() c.wg.Wait() + return err } // Host exposes the underlying libp2p host.Host object diff --git a/network/p2p/capabilities_test.go b/network/p2p/capabilities_test.go index 2b98b49806..881860f647 100644 --- a/network/p2p/capabilities_test.go +++ b/network/p2p/capabilities_test.go @@ -309,7 +309,8 @@ func TestCapabilities_Varying(t *testing.T) { wg.Wait() for _, disc := range capsDisc[3:] { - disc.Close() + err := disc.Close() + require.NoError(t, err) // Make sure it actually closes disc.wg.Wait() } @@ -347,6 +348,7 @@ func TestCapabilities_ExcludesSelf(t *testing.T) { "Found self when searching for capability", ) - disc[0].Close() + err := disc[0].Close() + require.NoError(t, err) disc[0].wg.Wait() } diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index 95cc3295f4..be20f1943c 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -365,7 +365,10 @@ func (n *P2PNetwork) Start() error { // Stop closes sockets and stop threads. func (n *P2PNetwork) Stop() { if n.capabilitiesDiscovery != nil { - n.capabilitiesDiscovery.Close() + err := n.capabilitiesDiscovery.Close() + if err != nil { + n.log.Warnf("Error closing capabilities discovery: %v", err) + } } n.handler.ClearHandlers([]Tag{}) From a8074714c6029e5d1995b7a5996b132d684b1312 Mon Sep 17 00:00:00 2001 From: chris erway Date: Thu, 27 Jun 2024 13:17:37 -0400 Subject: [PATCH 31/38] remove unnecessary identityChallengeSigner Verify method --- network/netidentity.go | 5 ----- network/p2p/peerID.go | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/network/netidentity.go b/network/netidentity.go index ad82131bef..4d797a1a5b 100644 --- a/network/netidentity.go +++ b/network/netidentity.go @@ -97,7 +97,6 @@ type identityChallengeScheme interface { type identityChallengeSigner interface { Sign(message crypto.Hashable) crypto.Signature SignBytes(message []byte) crypto.Signature - Verify(message crypto.Hashable, sig crypto.Signature) bool PublicKey() crypto.PublicKey } @@ -113,10 +112,6 @@ func (s *identityChallengeLegacySigner) SignBytes(message []byte) crypto.Signatu return s.keys.SignBytes(message) } -func (s *identityChallengeLegacySigner) Verify(message crypto.Hashable, sig crypto.Signature) bool { - return s.keys.SignatureVerifier.Verify(message, sig) -} - func (s *identityChallengeLegacySigner) PublicKey() crypto.PublicKey { return s.keys.SignatureVerifier } diff --git a/network/p2p/peerID.go b/network/p2p/peerID.go index 8ca584cbe8..ca7526977b 100644 --- a/network/p2p/peerID.go +++ b/network/p2p/peerID.go @@ -133,16 +133,6 @@ func (p *PeerIDChallengeSigner) SignBytes(message []byte) algocrypto.Signature { return algocrypto.Signature(sig) } -// Verify implements the identityChallengeSigner interface. -func (p *PeerIDChallengeSigner) Verify(message algocrypto.Hashable, sig algocrypto.Signature) bool { - // libp2p Ed25519PublicKey.Verify() returns a bool and no error - ret, err := p.key.GetPublic().Verify(algocrypto.HashRep(message), sig[:]) - if err != nil { - panic(err) - } - return ret -} - // PublicKey implements the identityChallengeSigner interface. func (p *PeerIDChallengeSigner) PublicKey() algocrypto.PublicKey { // libp2p Ed25519PublicKey.Raw() returns a 32-byte public key and no error From c9ceb49da61b69c5f367c265b3e295c5ebb7500f Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:18:19 -0400 Subject: [PATCH 32/38] Update network/p2p/p2p.go --- network/p2p/p2p.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 9ee9d3fa43..617684391f 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -215,7 +215,7 @@ func MakeService(ctx context.Context, log logging.Logger, cfg config.Local, h ho }, nil } -// Close shuts down the P2P service +// Start starts the P2P service func (s *serviceImpl) Start() error { listenAddr, err := multiaddr.NewMultiaddr(s.listenAddr) if err != nil { From d2090c665e966303c88f66daa2bfca91bdffe458 Mon Sep 17 00:00:00 2001 From: chris erway Date: Thu, 27 Jun 2024 13:19:55 -0400 Subject: [PATCH 33/38] rename snapshoter => snapshotter --- network/wsNetwork.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 6e5656d517..404a60ee4c 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -1803,14 +1803,14 @@ type peerConnectionStater struct { lastPeerConnectionsSent time.Time } -type peerSnapshoter interface { +type peerSnapshotter interface { peerSnapshot(peers []*wsPeer) ([]*wsPeer, int32) } // sendPeerConnectionsTelemetryStatus sends a snapshot of the currently connected peers // to the telemetry server. Internally, it's using a timer to ensure that it would only // send the information once every hour ( configurable via PeerConnectionsUpdateInterval ) -func (pcs *peerConnectionStater) sendPeerConnectionsTelemetryStatus(snapshoter peerSnapshoter) { +func (pcs *peerConnectionStater) sendPeerConnectionsTelemetryStatus(snapshotter peerSnapshotter) { if !pcs.log.GetTelemetryEnabled() { return } @@ -1822,7 +1822,7 @@ func (pcs *peerConnectionStater) sendPeerConnectionsTelemetryStatus(snapshoter p pcs.lastPeerConnectionsSent = now var peers []*wsPeer - peers, _ = snapshoter.peerSnapshot(peers) + peers, _ = snapshotter.peerSnapshot(peers) connectionDetails := getPeerConnectionTelemetryDetails(now, peers) pcs.log.EventWithDetails(telemetryspec.Network, telemetryspec.PeerConnectionsEvent, connectionDetails) } From 4152b889d056e8563e676345d363bb08aab9c324 Mon Sep 17 00:00:00 2001 From: chris erway Date: Fri, 28 Jun 2024 11:04:13 -0400 Subject: [PATCH 34/38] trim file path in p2p logger --- network/p2p/logger.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/network/p2p/logger.go b/network/p2p/logger.go index cde67f7f58..26c738e1e1 100644 --- a/network/p2p/logger.go +++ b/network/p2p/logger.go @@ -20,6 +20,7 @@ package p2p import ( "runtime" + "strings" p2plogging "github.com/ipfs/go-log/v2" "github.com/sirupsen/logrus" @@ -103,8 +104,11 @@ func (c *loggingCore) Write(e zapcore.Entry, fields []zapcore.Field) error { } } event := c.log.WithFields(loggingFields).With("libp2p", e.LoggerName) + file := e.Caller.File + slash := strings.LastIndex(file, "/") + file = file[slash+1:] event = event.WithFields(logrus.Fields{ - "file": e.Caller.File, + "file": file, "line": e.Caller.Line, }) if function := runtime.FuncForPC(e.Caller.PC); function != nil { From 9fe41ad4866bc6bd36d7b5d21fe34290eeb9e6cf Mon Sep 17 00:00:00 2001 From: cce <51567+cce@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:04:49 -0400 Subject: [PATCH 35/38] Update network/p2pNetwork.go --- network/p2pNetwork.go | 1 + 1 file changed, 1 insertion(+) diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index ead4307a71..d0accd2ccc 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -192,6 +192,7 @@ type p2pPeerStats struct { txReceived atomic.Uint64 } +// gossipSubPeer implements the DeadlineSettableConn, IPAddressable, and ErlClient interfaces. type gossipSubPeer struct { peerID peer.ID net GossipNode From 65c0181613f72f42ec7cfda2e5c35509c8c23966 Mon Sep 17 00:00:00 2001 From: chris erway Date: Fri, 28 Jun 2024 11:35:22 -0400 Subject: [PATCH 36/38] rename DeadlineSettable => DeadlineSettableConn as intended in f6dd7a47e4e911ae38a87e2c5be36456fa1ec8f1 --- agreement/fuzzer/networkFacade_test.go | 2 +- agreement/gossip/network_test.go | 4 ++-- components/mocks/mockNetwork.go | 4 ++-- data/txHandler.go | 4 ++-- network/connPerfMon_test.go | 2 +- network/gossipNode.go | 14 +++++++------- network/hybridNetwork.go | 4 ++-- network/p2pNetwork.go | 6 +++--- network/wsNetwork.go | 8 ++++---- rpcs/ledgerService.go | 2 +- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/agreement/fuzzer/networkFacade_test.go b/agreement/fuzzer/networkFacade_test.go index d1ec7bf8a5..804fb1e7ff 100644 --- a/agreement/fuzzer/networkFacade_test.go +++ b/agreement/fuzzer/networkFacade_test.go @@ -348,7 +348,7 @@ func (n *NetworkFacade) ReceiveMessage(sourceNode int, tag protocol.Tag, data [] n.pushPendingReceivedMessage() } -func (n *NetworkFacade) Disconnect(sender network.DeadlineSettableConn) { +func (n *NetworkFacade) Disconnect(sender network.DisconnectablePeer) { sourceNode := n.peerToNode[sender.(*facadePeer)] n.fuzzer.Disconnect(n.nodeID, sourceNode) } diff --git a/agreement/gossip/network_test.go b/agreement/gossip/network_test.go index 760456e66c..a3c5328716 100644 --- a/agreement/gossip/network_test.go +++ b/agreement/gossip/network_test.go @@ -135,7 +135,7 @@ func (w *whiteholeNetwork) Relay(ctx context.Context, tag protocol.Tag, data []b func (w *whiteholeNetwork) BroadcastSimple(tag protocol.Tag, data []byte) error { return w.Broadcast(context.Background(), tag, data, true, nil) } -func (w *whiteholeNetwork) Disconnect(badnode network.DeadlineSettableConn) { +func (w *whiteholeNetwork) Disconnect(badnode network.DisconnectablePeer) { return } func (w *whiteholeNetwork) DisconnectPeers() { @@ -155,7 +155,7 @@ func (w *whiteholeNetwork) GetPeers(options ...network.PeerOption) []network.Pee } func (w *whiteholeNetwork) RegisterHTTPHandler(path string, handler http.Handler) { } -func (w *whiteholeNetwork) GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettable) { +func (w *whiteholeNetwork) GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettableConn) { return nil } diff --git a/components/mocks/mockNetwork.go b/components/mocks/mockNetwork.go index a6a55e7ec4..f933a553a9 100644 --- a/components/mocks/mockNetwork.go +++ b/components/mocks/mockNetwork.go @@ -60,7 +60,7 @@ func (network *MockNetwork) RequestConnectOutgoing(replace bool, quit <-chan str } // Disconnect - unused function -func (network *MockNetwork) Disconnect(badpeer network.DeadlineSettableConn) { +func (network *MockNetwork) Disconnect(badpeer network.DisconnectablePeer) { } // DisconnectPeers - unused function @@ -107,7 +107,7 @@ func (network *MockNetwork) RegisterHTTPHandler(path string, handler http.Handle func (network *MockNetwork) OnNetworkAdvance() {} // GetHTTPRequestConnection - empty implementation -func (network *MockNetwork) GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettable) { +func (network *MockNetwork) GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettableConn) { return nil } diff --git a/data/txHandler.go b/data/txHandler.go index 15e43dbc91..eae9586c47 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -617,7 +617,7 @@ func (handler *TxHandler) incomingMsgDupCheck(data []byte) (*crypto.Digest, bool // Returns: // - the capacity guard returned by the elastic rate limiter // - a boolean indicating if the sender is rate limited -func (handler *TxHandler) incomingMsgErlCheck(sender network.DeadlineSettableConn) (*util.ErlCapacityGuard, bool) { +func (handler *TxHandler) incomingMsgErlCheck(sender network.DisconnectablePeer) (*util.ErlCapacityGuard, bool) { var capguard *util.ErlCapacityGuard var err error if handler.erl != nil { @@ -690,7 +690,7 @@ func decodeMsg(data []byte) (unverifiedTxGroup []transactions.SignedTxn, consume // incomingTxGroupDupRateLimit checks // - if the incoming transaction group has been seen before after reencoding to canonical representation, and // - if the sender is rate limited by the per-application rate limiter. -func (handler *TxHandler) incomingTxGroupDupRateLimit(unverifiedTxGroup []transactions.SignedTxn, encodedExpectedSize int, sender network.DeadlineSettableConn) (*crypto.Digest, bool) { +func (handler *TxHandler) incomingTxGroupDupRateLimit(unverifiedTxGroup []transactions.SignedTxn, encodedExpectedSize int, sender network.DisconnectablePeer) (*crypto.Digest, bool) { var canonicalKey *crypto.Digest if handler.txCanonicalCache != nil { var isDup bool diff --git a/network/connPerfMon_test.go b/network/connPerfMon_test.go index fe10abbd34..560be72a96 100644 --- a/network/connPerfMon_test.go +++ b/network/connPerfMon_test.go @@ -48,7 +48,7 @@ func makeMsgPool(N int, peers []Peer) (out []IncomingMessage) { addMsg := func(msgCount int) { for i := 0; i < msgCount; i++ { - msg.Sender = peers[(int(msgIndex)+i)%len(peers)].(DeadlineSettableConn) + msg.Sender = peers[(int(msgIndex)+i)%len(peers)].(DisconnectablePeer) timer += int64(7 * time.Nanosecond) msg.Received = timer out = append(out, msg) diff --git a/network/gossipNode.go b/network/gossipNode.go index 8356c254dc..6a028ff193 100644 --- a/network/gossipNode.go +++ b/network/gossipNode.go @@ -29,8 +29,8 @@ import ( // Peer opaque interface for referring to a neighbor in the network type Peer interface{} -// DeadlineSettableConn is a Peer with a long-living connection to a network that can be disconnected -type DeadlineSettableConn interface { +// DisconnectablePeer is a Peer with a long-living connection to a network that can be disconnected +type DisconnectablePeer interface { GetNetwork() GossipNode } @@ -50,8 +50,8 @@ const ( PeersPhonebookArchivalNodes PeerOption = iota ) -// DeadlineSettable abstracts net.Conn and related types as deadline-settable -type DeadlineSettable interface { +// DeadlineSettableConn abstracts net.Conn and related types as deadline-settable +type DeadlineSettableConn interface { SetDeadline(time.Time) error SetReadDeadline(time.Time) error SetWriteDeadline(time.Time) error @@ -62,7 +62,7 @@ type GossipNode interface { Address() (string, bool) Broadcast(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error Relay(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error - Disconnect(badnode DeadlineSettableConn) + Disconnect(badnode DisconnectablePeer) DisconnectPeers() // only used by testing // RegisterHTTPHandler path accepts gorilla/mux path annotations @@ -106,7 +106,7 @@ type GossipNode interface { // GetHTTPRequestConnection returns the underlying connection for the given request. Note that the request must be the same // request that was provided to the http handler ( or provide a fallback Context() to that ) - GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettable) + GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettableConn) // GetGenesisID returns the network-specific genesisID. GetGenesisID() string @@ -127,7 +127,7 @@ var outgoingMessagesBufferSize = int( // IncomingMessage represents a message arriving from some peer in our p2p network type IncomingMessage struct { - Sender DeadlineSettableConn + Sender DisconnectablePeer Tag Tag Data []byte Err error diff --git a/network/hybridNetwork.go b/network/hybridNetwork.go index 6f97a04926..f324deb73f 100644 --- a/network/hybridNetwork.go +++ b/network/hybridNetwork.go @@ -115,7 +115,7 @@ func (n *HybridP2PNetwork) Relay(ctx context.Context, tag protocol.Tag, data []b } // Disconnect implements GossipNode -func (n *HybridP2PNetwork) Disconnect(badnode DeadlineSettableConn) { +func (n *HybridP2PNetwork) Disconnect(badnode DisconnectablePeer) { net := badnode.GetNetwork() if net == n.p2pNetwork { n.p2pNetwork.Disconnect(badnode) @@ -214,7 +214,7 @@ func (n *HybridP2PNetwork) OnNetworkAdvance() { // GetHTTPRequestConnection returns the underlying connection for the given request. Note that the request must be the same // request that was provided to the http handler ( or provide a fallback Context() to that ) -func (n *HybridP2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettable) { +func (n *HybridP2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettableConn) { conn = n.wsNetwork.GetHTTPRequestConnection(request) if conn != nil { return conn diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index d0accd2ccc..cf575c773a 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -531,7 +531,7 @@ func (n *P2PNetwork) Relay(ctx context.Context, tag protocol.Tag, data []byte, w } // Disconnect from a peer, probably due to protocol errors. -func (n *P2PNetwork) Disconnect(badpeer DeadlineSettableConn) { +func (n *P2PNetwork) Disconnect(badpeer DisconnectablePeer) { var peerID peer.ID var wsp *wsPeer @@ -559,7 +559,7 @@ func (n *P2PNetwork) Disconnect(badpeer DeadlineSettableConn) { } } -func (n *P2PNetwork) disconnectThread(badnode DeadlineSettableConn, reason disconnectReason) { +func (n *P2PNetwork) disconnectThread(badnode DisconnectablePeer, reason disconnectReason) { defer n.wg.Done() n.Disconnect(badnode) // ignores reason } @@ -727,7 +727,7 @@ func (n *P2PNetwork) OnNetworkAdvance() { // GetHTTPRequestConnection returns the underlying connection for the given request. Note that the request must be the same // request that was provided to the http handler ( or provide a fallback Context() to that ) -func (n *P2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettable) { +func (n *P2PNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettableConn) { addr := request.Context().Value(http.LocalAddrContextKey).(net.Addr) peerID, err := peer.Decode(addr.String()) if err != nil { diff --git a/network/wsNetwork.go b/network/wsNetwork.go index 652903912f..8a43ad5234 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -362,7 +362,7 @@ type networkPeerManager interface { // used by msgHandler Broadcast(ctx context.Context, tag protocol.Tag, data []byte, wait bool, except Peer) error - disconnectThread(badnode DeadlineSettableConn, reason disconnectReason) + disconnectThread(badnode DisconnectablePeer, reason disconnectReason) checkPeersConnectivity() } @@ -477,13 +477,13 @@ func (wn *WebsocketNetwork) RelayArray(ctx context.Context, tags []protocol.Tag, return nil } -func (wn *WebsocketNetwork) disconnectThread(badnode DeadlineSettableConn, reason disconnectReason) { +func (wn *WebsocketNetwork) disconnectThread(badnode DisconnectablePeer, reason disconnectReason) { defer wn.wg.Done() wn.disconnect(badnode, reason) } // Disconnect from a peer, probably due to protocol errors. -func (wn *WebsocketNetwork) Disconnect(node DeadlineSettableConn) { +func (wn *WebsocketNetwork) Disconnect(node DisconnectablePeer) { wn.disconnect(node, disconnectBadData) } @@ -1035,7 +1035,7 @@ func (wn *WebsocketNetwork) checkIncomingConnectionVariables(response http.Respo // request that was provided to the http handler ( or provide a fallback Context() to that ) // if the provided request has no associated connection, it returns nil. ( this should not happen for any http request that was registered // by WebsocketNetwork ) -func (wn *WebsocketNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettable) { +func (wn *WebsocketNetwork) GetHTTPRequestConnection(request *http.Request) (conn DeadlineSettableConn) { if wn.requestsTracker != nil { conn = wn.requestsTracker.GetRequestConnection(request) } diff --git a/rpcs/ledgerService.go b/rpcs/ledgerService.go index 08702e0ac0..823895a417 100644 --- a/rpcs/ledgerService.go +++ b/rpcs/ledgerService.go @@ -63,7 +63,7 @@ type LedgerForService interface { // httpGossipNode is a reduced interface for the gossipNode that only includes the methods needed by the LedgerService type httpGossipNode interface { RegisterHTTPHandler(path string, handler http.Handler) - GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettable) + GetHTTPRequestConnection(request *http.Request) (conn network.DeadlineSettableConn) } // LedgerService represents the Ledger RPC API From c87ceebd893e0d6ea6e000c0b5cfb23f39a1f640 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Fri, 28 Jun 2024 12:51:43 -0400 Subject: [PATCH 37/38] remove EnableMetricReporting from p2p net --- network/p2p/dht/dht.go | 8 +++----- network/p2p/p2p.go | 6 +----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/network/p2p/dht/dht.go b/network/p2p/dht/dht.go index 7cc290ef3b..1ea38bdd78 100644 --- a/network/p2p/dht/dht.go +++ b/network/p2p/dht/dht.go @@ -57,12 +57,10 @@ func MakeDHT(ctx context.Context, h host.Host, networkID algoproto.NetworkID, cf dht.BootstrapPeersFunc(bootstrapFunc), } - if cfg.EnableMetricReporting { - if err := view.Register(dhtmetrics.DefaultViews...); err != nil { - return nil, err - } - metrics.DefaultRegistry().Register(&metrics.OpencensusDefaultMetrics) + if err := view.Register(dhtmetrics.DefaultViews...); err != nil { + return nil, err } + metrics.DefaultRegistry().Register(&metrics.OpencensusDefaultMetrics) return dht.New(ctx, h, dhtCfg...) } diff --git a/network/p2p/p2p.go b/network/p2p/p2p.go index 617684391f..f4ed670f3e 100644 --- a/network/p2p/p2p.go +++ b/network/p2p/p2p.go @@ -125,11 +125,7 @@ func MakeHost(cfg config.Local, datadir string, pstore *pstore.PeerStore) (host. } var disableMetrics = func(cfg *libp2p.Config) error { return nil } - if !cfg.EnableMetricReporting { - disableMetrics = libp2p.DisableMetrics() - } else { - metrics.DefaultRegistry().Register(&metrics.PrometheusDefaultMetrics) - } + metrics.DefaultRegistry().Register(&metrics.PrometheusDefaultMetrics) host, err := libp2p.New( libp2p.Identity(privKey), From 4564473a169a39dff8c5132de2dda76c51fa29ee Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Fri, 28 Jun 2024 13:09:08 -0400 Subject: [PATCH 38/38] remove algorand-ws streams limiting --- network/p2pNetwork.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/network/p2pNetwork.go b/network/p2pNetwork.go index cf575c773a..7ebbb5a665 100644 --- a/network/p2pNetwork.go +++ b/network/p2pNetwork.go @@ -758,23 +758,6 @@ func (n *P2PNetwork) wsStreamHandler(ctx context.Context, p2pPeer peer.ID, strea return } } else { - n.wsPeersLock.Lock() - numOutgoingPeers := 0 - for _, peer := range n.wsPeers { - if peer.outgoing { - n.log.Debugf("outgoing peer orig=%s addr=%s", peer.OriginAddress(), peer.GetAddress()) - numOutgoingPeers++ - } - } - n.wsPeersLock.Unlock() - if numOutgoingPeers >= n.config.GossipFanout { - // this appears to be some auxiliary connection made by libp2p itself like DHT connection. - // skip this connection since there are already enough peers - n.log.Debugf("skipping outgoing connection to peer %s: num outgoing %d > fanout %d ", p2pPeer, numOutgoingPeers, n.config.GossipFanout) - stream.Close() - return - } - _, err := stream.Write([]byte("1")) if err != nil { n.log.Warnf("wsStreamHandler: error sending initial message: %s", err)