diff --git a/agreement/proposalStore.go b/agreement/proposalStore.go index e9a0f66e96..4a8d09ad9c 100644 --- a/agreement/proposalStore.go +++ b/agreement/proposalStore.go @@ -374,7 +374,7 @@ func (store *proposalStore) lastRelevant(pv proposalValue) (p period, pinned boo } for per := range store.Relevant { - if per > p { + if per > p && store.Relevant[per] == pv { p = per } } diff --git a/buildnumber.dat b/buildnumber.dat index 0cfbf08886..00750edc07 100644 --- a/buildnumber.dat +++ b/buildnumber.dat @@ -1 +1 @@ -2 +3 diff --git a/cmd/algod/main.go b/cmd/algod/main.go index a0f8d7991d..575f2bf0cc 100644 --- a/cmd/algod/main.go +++ b/cmd/algod/main.go @@ -161,6 +161,10 @@ func main() { if err != nil { fmt.Fprintln(os.Stdout, "error loading telemetry config", err) } + if os.IsPermission(err) { + fmt.Fprintf(os.Stderr, "Permission error on accessing telemetry config: %v", err) + os.Exit(1) + } // Apply telemetry override. telemetryConfig.Enable = logging.TelemetryOverride(*telemetryOverride) diff --git a/cmd/goal/account.go b/cmd/goal/account.go index 719b9e5f1b..c9f37a024b 100644 --- a/cmd/goal/account.go +++ b/cmd/goal/account.go @@ -487,10 +487,10 @@ var listCmd = &cobra.Command{ if err == nil { params, ok := creatorInfo.AssetParams[aid] if ok { + unitName = "units" if params.UnitName != "" { unitName = params.UnitName } - unitName = "units" if params.AssetName != "" { assetName = fmt.Sprintf(", name %s", params.AssetName) } @@ -859,7 +859,8 @@ var listParticipationKeysCmd = &cobra.Command{ if err == nil { votingBytes := parts[fn].Voting.OneTimeSignatureVerifier vrfBytes := parts[fn].VRF.PK - if string(onlineAccountInfo.Participation.ParticipationPK) == string(votingBytes[:]) && + if onlineAccountInfo.Participation != nil && + (string(onlineAccountInfo.Participation.ParticipationPK) == string(votingBytes[:])) && (string(onlineAccountInfo.Participation.VRFPK) == string(vrfBytes[:])) && (onlineAccountInfo.Participation.VoteFirst == uint64(parts[fn].FirstValid)) && (onlineAccountInfo.Participation.VoteLast == uint64(parts[fn].LastValid)) && diff --git a/cmd/goal/asset.go b/cmd/goal/asset.go index b906b9bd38..a5b0ddc9ae 100644 --- a/cmd/goal/asset.go +++ b/cmd/goal/asset.go @@ -70,6 +70,7 @@ func init() { createAssetCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Write transaction to this file") createAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") createAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") + createAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") createAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") createAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") createAssetCmd.MarkFlagRequired("total") @@ -86,6 +87,7 @@ func init() { destroyAssetCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Write transaction to this file") destroyAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") destroyAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") + destroyAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") destroyAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") destroyAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") @@ -105,6 +107,7 @@ func init() { configAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") configAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") configAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") + configAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") configAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") configAssetCmd.MarkFlagRequired("manager") @@ -123,6 +126,7 @@ func init() { sendAssetCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Write transaction to this file") sendAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") sendAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") + sendAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") sendAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") sendAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") sendAssetCmd.MarkFlagRequired("to") @@ -142,6 +146,7 @@ func init() { freezeAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") freezeAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") freezeAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") + freezeAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") freezeAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") freezeAssetCmd.MarkFlagRequired("freezer") freezeAssetCmd.MarkFlagRequired("account") @@ -230,6 +235,7 @@ var createAssetCmd = &cobra.Command{ } tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) if err != nil { @@ -304,6 +310,7 @@ var destroyAssetCmd = &cobra.Command{ } tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) firstValid, lastValid, err = client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) if err != nil { @@ -391,6 +398,7 @@ var configAssetCmd = &cobra.Command{ } tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) firstValid, lastValid, err = client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) if err != nil { @@ -471,6 +479,7 @@ var sendAssetCmd = &cobra.Command{ } tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) firstValid, lastValid, err = client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) if err != nil { @@ -534,6 +543,7 @@ var freezeAssetCmd = &cobra.Command{ } tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) firstValid, lastValid, err = client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) if err != nil { diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index 41b0a38413..95cee4f8fa 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -248,6 +248,21 @@ func parseNoteField(cmd *cobra.Command) []byte { return noteBytes } +func parseLease(cmd *cobra.Command) (leaseBytes [32]byte) { + // Parse lease field + if cmd.Flags().Changed("lease") { + leaseBytesRaw, err := base64.StdEncoding.DecodeString(lease) + if err != nil { + reportErrorf(malformedLease, lease, err) + } + if len(leaseBytesRaw) != 32 { + reportErrorf(malformedLease, lease, fmt.Errorf("lease length %d != 32", len(leaseBytesRaw))) + } + copy(leaseBytes[:], leaseBytesRaw) + } + return +} + var sendCmd = &cobra.Command{ Use: "send", Short: "Send money to an address", @@ -301,21 +316,9 @@ var sendCmd = &cobra.Command{ } toAddressResolved := accountList.getAddressByName(toAddress) - // Parse lease field - var leaseBytes [32]byte - if cmd.Flags().Changed("lease") { - leaseBytesRaw, err := base64.StdEncoding.DecodeString(lease) - if err != nil { - reportErrorf(malformedLease, lease, err) - } - if len(leaseBytesRaw) != 32 { - reportErrorf(malformedLease, lease, fmt.Errorf("lease length %d != 32", len(leaseBytesRaw))) - } - copy(leaseBytes[:], leaseBytesRaw) - } - - // Parse notes field + // Parse notes and lease fields noteBytes := parseNoteField(cmd) + leaseBytes := parseLease(cmd) // If closing an account, resolve that address as well var closeToAddressResolved string diff --git a/cmd/goal/messages.go b/cmd/goal/messages.go index ddeb675dcc..92e9ec16dc 100644 --- a/cmd/goal/messages.go +++ b/cmd/goal/messages.go @@ -94,6 +94,9 @@ const ( soFlagError = "-s is not meaningful without -o" infoRawTxIssued = "Raw transaction ID %s issued" txPoolError = "Transaction %s kicked out of local node pool: %s" + addrNoSigError = "Exactly one of --address or --no-sig is required" + msigLookupError = "Could not lookup multisig information: %s" + msigParseError = "Multisig information parsing error: %s" infoAutoFeeSet = "Automatically set fee to %d MicroAlgos" diff --git a/cmd/goal/multisig.go b/cmd/goal/multisig.go index 40e6796059..e20ddb01b3 100644 --- a/cmd/goal/multisig.go +++ b/cmd/goal/multisig.go @@ -34,6 +34,7 @@ import ( var ( addr string msigAddr string + noSig bool ) func init() { @@ -44,8 +45,8 @@ func init() { addSigCmd.Flags().StringVarP(&txFilename, "tx", "t", "", "Partially-signed transaction file to add signature to") addSigCmd.Flags().StringVarP(&addr, "address", "a", "", "Address of the key to sign with") + addSigCmd.Flags().BoolVarP(&noSig, "no-sig", "n", false, "Fill in the transaction's multisig field with public keys and threshold information, but don't produce a signature") addSigCmd.MarkFlagRequired("tx") - addSigCmd.MarkFlagRequired("address") signProgramCmd.Flags().StringVarP(&programSource, "program", "p", "", "Program source to be compiled and signed") signProgramCmd.Flags().StringVarP(&progByteFile, "program-bytes", "P", "", "Program binary to be signed") @@ -76,12 +77,19 @@ var addSigCmd = &cobra.Command{ Long: `Start a multisig, or add a signature to an existing multisig, for a given transaction`, Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { - data, err := readFile(txFilename) if err != nil { reportErrorf(fileReadError, txFilename, err) } + // --address and --no-sig are mutually exclusive, since if + // we're not signing we don't need an address + if addr == "" && !noSig { + reportErrorf(addrNoSigError) + } else if addr != "" && noSig { + reportErrorf(addrNoSigError) + } + dataDir := ensureSingleDataDir() client := ensureKmdClient(dataDir) wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) @@ -98,9 +106,21 @@ var addSigCmd = &cobra.Command{ reportErrorf(txDecodeError, txFilename, err) } - msig, err := client.MultisigSignTransactionWithWallet(wh, pw, stxn.Txn, addr, stxn.Msig) - if err != nil { - reportErrorf(errorSigningTX, err) + var msig crypto.MultisigSig + if noSig { + multisigInfo, err := client.LookupMultisigAccount(wh, stxn.Txn.Sender.String()) + if err != nil { + reportErrorf(msigLookupError, err) + } + msig, err = msigInfoToMsig(multisigInfo) + if err != nil { + reportErrorf(msigParseError, err) + } + } else { + msig, err = client.MultisigSignTransactionWithWallet(wh, pw, stxn.Txn, addr, stxn.Msig) + if err != nil { + reportErrorf(errorSigningTX, err) + } } // The following line makes stxn.cachedEncodingLen incorrect, but it's okay because we're just serializing it to a file @@ -181,11 +201,11 @@ var signProgramCmd = &cobra.Command{ } multisigInfo, err := client.LookupMultisigAccount(wh, msigAddr) if err != nil { - reportErrorf("could not lookup multisig address", err) + reportErrorf(msigLookupError, err) } msig, err := msigInfoToMsig(multisigInfo) if err != nil { - reportErrorf("internal err processing msig: %s", err) + reportErrorf(msigParseError, err) } lsig.Msig = msig } diff --git a/config/config.go b/config/config.go index 67acb4176f..b17bd36742 100644 --- a/config/config.go +++ b/config/config.go @@ -747,6 +747,10 @@ type Local struct { // EnableRequestLogger enabled the logging of the incoming requests to the telemetry server. EnableRequestLogger bool + + // PeerConnectionsUpdateInterval defines the interval at which the peer connections information is being sent to the + // telemetry ( when enabled ). Defined in seconds. + PeerConnectionsUpdateInterval int } // Filenames of config files within the configdir (e.g. ~/.algorand) diff --git a/config/local_defaults.go b/config/local_defaults.go index 90f02b7e49..863730c665 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -87,6 +87,7 @@ var defaultLocalV5 = Local{ TxSyncIntervalSeconds: 60, TxSyncTimeoutSeconds: 30, TxSyncServeResponseSize: 1000000, + PeerConnectionsUpdateInterval: 3600, // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file } @@ -138,6 +139,7 @@ var defaultLocalV4 = Local{ TxSyncIntervalSeconds: 60, TxSyncTimeoutSeconds: 30, TxSyncServeResponseSize: 1000000, + // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file } @@ -347,7 +349,10 @@ func migrate(cfg Local) (newCfg Local, err error) { if newCfg.CatchupParallelBlocks == defaultLocalV4.CatchupParallelBlocks { newCfg.CatchupParallelBlocks = defaultLocalV5.CatchupParallelBlocks } - + if newCfg.PeerConnectionsUpdateInterval == defaultLocalV4.PeerConnectionsUpdateInterval { + newCfg.PeerConnectionsUpdateInterval = defaultLocalV5.PeerConnectionsUpdateInterval + } + newCfg.Version = 5 } diff --git a/daemon/kmd/wallet/driver/ledger.go b/daemon/kmd/wallet/driver/ledger.go index ef044a1018..ee2b404a0a 100644 --- a/daemon/kmd/wallet/driver/ledger.go +++ b/daemon/kmd/wallet/driver/ledger.go @@ -17,9 +17,11 @@ package driver import ( + "bytes" "encoding/binary" "errors" "fmt" + "sort" "github.com/algorand/go-deadlock" @@ -52,7 +54,9 @@ var ledgerWalletSupportedTxs = []protocol.TxType{protocol.PaymentTx, protocol.Ke // Ledger Nano S device. The device must run the Algorand wallet // application from https://github.com/algorand/ledger-app-algorand type LedgerWalletDriver struct { + mu deadlock.Mutex wallets map[string]*LedgerWallet + log logging.Logger } // LedgerWallet represents a particular wallet under the @@ -61,7 +65,6 @@ type LedgerWalletDriver struct { type LedgerWallet struct { mu deadlock.Mutex dev LedgerUSB - log logging.Logger } // CreateWallet implements the Driver interface. There is @@ -76,6 +79,9 @@ func (lwd *LedgerWalletDriver) CreateWallet(name []byte, id []byte, pw []byte, m // FetchWallet looks up a wallet by ID and returns it, failing if there's more // than one wallet with the given ID func (lwd *LedgerWalletDriver) FetchWallet(id []byte) (w wallet.Wallet, err error) { + lwd.mu.Lock() + defer lwd.mu.Unlock() + lw, ok := lwd.wallets[string(id)] if !ok { return nil, errWalletNotFound @@ -84,17 +90,59 @@ func (lwd *LedgerWalletDriver) FetchWallet(id []byte) (w wallet.Wallet, err erro return lw, nil } -// InitWithConfig accepts a driver configuration. Currently, the Ledger -// driver does not have any configuration parameters. However, we use -// this to enumerate the USB devices. -func (lwd *LedgerWalletDriver) InitWithConfig(cfg config.KMDConfig, log logging.Logger) error { - devs, err := LedgerEnumerate(log) +// scanWalletsLocked enumerates attached ledger devices and stores them. +// lwd.mu must be held +func (lwd *LedgerWalletDriver) scanWalletsLocked() error { + // Initialize wallets map + if lwd.wallets == nil { + lwd.wallets = make(map[string]*LedgerWallet) + } + + // Enumerate attached wallet devices + infos, err := LedgerEnumerate() if err != nil { return err } - lwd.wallets = make(map[string]*LedgerWallet) - for _, dev := range devs { + // Make map of existing device paths. We will pop each one that we + // are able to scan for, meaning anything left over is dead, and we + // should remove it + var curPaths map[string]bool + curPaths = make(map[string]bool) + for k := range lwd.wallets { + curPaths[k] = true + } + + // Try to open each new device, skipping ones that are already open. + var newDevs []LedgerUSB + for _, info := range infos { + if curPaths[info.Path] { + delete(curPaths, info.Path) + continue + } + + dev, err := info.Open() + if err != nil { + lwd.log.Warnf("enumerated but failed to open ledger %x: %v", info.ProductID, err) + continue + } + + newDevs = append(newDevs, LedgerUSB{ + hiddev: dev, + }) + } + + // Anything left in curPaths is no longer scanning. Close and remove + for deadPath := range curPaths { + err = lwd.wallets[deadPath].dev.hiddev.Close() + if err != nil { + lwd.log.Warnf("failed to close '%s': %v", deadPath, err) + } + delete(lwd.wallets, deadPath) + } + + // Add in new devices + for _, dev := range newDevs { id := dev.USBInfo().Path lwd.wallets[id] = &LedgerWallet{ dev: dev, @@ -103,8 +151,27 @@ func (lwd *LedgerWalletDriver) InitWithConfig(cfg config.KMDConfig, log logging. return nil } +// InitWithConfig accepts a driver configuration. Currently, the Ledger +// driver does not have any configuration parameters. However, we use +// this to enumerate the USB devices. +func (lwd *LedgerWalletDriver) InitWithConfig(cfg config.KMDConfig, log logging.Logger) error { + lwd.mu.Lock() + defer lwd.mu.Unlock() + + lwd.log = log + return lwd.scanWalletsLocked() +} + // ListWalletMetadatas returns all wallets supported by this driver. func (lwd *LedgerWalletDriver) ListWalletMetadatas() (metadatas []wallet.Metadata, err error) { + lwd.mu.Lock() + defer lwd.mu.Unlock() + + err = lwd.scanWalletsLocked() + if err != nil { + return + } + for _, w := range lwd.wallets { md, err := w.Metadata() if err != nil { @@ -114,6 +181,13 @@ func (lwd *LedgerWalletDriver) ListWalletMetadatas() (metadatas []wallet.Metadat metadatas = append(metadatas, md) } + metaSort := func(i, j int) bool { + return bytes.Compare(metadatas[i].ID, metadatas[j].ID) < 0 + } + + // Sort metadatas by ID + sort.Slice(metadatas, metaSort) + return metadatas, nil } diff --git a/daemon/kmd/wallet/driver/ledger_hid.go b/daemon/kmd/wallet/driver/ledger_hid.go index 86d275f393..ca87f4b71b 100644 --- a/daemon/kmd/wallet/driver/ledger_hid.go +++ b/daemon/kmd/wallet/driver/ledger_hid.go @@ -21,8 +21,6 @@ import ( "fmt" "github.com/karalabe/hid" - - "github.com/algorand/go-algorand/logging" ) const ledgerVendorID = 0x2c97 @@ -40,7 +38,7 @@ type LedgerUSBError uint16 // Error satisfies builtin interface `error` func (err LedgerUSBError) Error() string { - return fmt.Sprintf("Exchange: unexpected status 0x%x", err) + return fmt.Sprintf("Exchange: unexpected status 0x%x", uint16(err)) } // Protocol reference: @@ -197,24 +195,15 @@ func (l *LedgerUSB) USBInfo() hid.DeviceInfo { } // LedgerEnumerate returns all of the Ledger devices connected to this machine. -func LedgerEnumerate(log logging.Logger) ([]LedgerUSB, error) { +func LedgerEnumerate() ([]hid.DeviceInfo, error) { if !hid.Supported() { return nil, fmt.Errorf("HID not supported") } - var devs []LedgerUSB + var infos []hid.DeviceInfo for _, info := range hid.Enumerate(ledgerVendorID, 0) { - // Try to open the device - dev, err := info.Open() - if err != nil { - log.Warnf("enumerated but failed to open ledger %x: %v", info.ProductID, err) - continue - } - - devs = append(devs, LedgerUSB{ - hiddev: dev, - }) + infos = append(infos, info) } - return devs, nil + return infos, nil } diff --git a/daemon/kmd/wallet/driver/sqlite.go b/daemon/kmd/wallet/driver/sqlite.go index 5638869937..fabee925e4 100644 --- a/daemon/kmd/wallet/driver/sqlite.go +++ b/daemon/kmd/wallet/driver/sqlite.go @@ -1018,7 +1018,7 @@ func (sw *SQLiteWallet) LookupMultisigPreimage(addr crypto.Digest) (version, thr row := db.QueryRow("SELECT version, threshold, pks FROM msig_addrs WHERE address=?", addr[:]) err = row.Scan(&versionCandidate, &thresholdCandidate, &pksBlob) if err != nil { - err = errKeyNotFound + err = errMsigDataNotFound return } diff --git a/daemon/kmd/wallet/driver/sqlite_errors.go b/daemon/kmd/wallet/driver/sqlite_errors.go index 66915acdd9..79eef013e1 100644 --- a/daemon/kmd/wallet/driver/sqlite_errors.go +++ b/daemon/kmd/wallet/driver/sqlite_errors.go @@ -23,6 +23,7 @@ import ( var errDatabase = fmt.Errorf("database error") var errDatabaseConnect = fmt.Errorf("error connecting to database") var errKeyNotFound = fmt.Errorf("key does not exist in this wallet") +var errMsigDataNotFound = fmt.Errorf("multisig information (pks, threshold) for address does not exist in this wallet") var errSKToPK = fmt.Errorf("could not convert secret key to public key") var errSKToSeed = fmt.Errorf("could not convert secret key to seed") var errTampering = fmt.Errorf("derived public key mismatch, something fishy is going on with this wallet") diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index ddadd606d0..29569e9542 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -1254,9 +1254,12 @@ func disBnz(dis *disassembleState) { dis.nextpc = dis.pc + 3 offset := (uint(dis.program[dis.pc+1]) << 8) | uint(dis.program[dis.pc+2]) target := int(offset) + dis.pc + 3 - dis.labelCount++ - label := fmt.Sprintf("label%d", dis.labelCount) - dis.putLabel(label, target) + label, labelExists := dis.pendingLabels[target] + if !labelExists { + dis.labelCount++ + label = fmt.Sprintf("label%d", dis.labelCount) + dis.putLabel(label, target) + } _, dis.err = fmt.Fprintf(dis.out, "bnz %s\n", label) } diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 584b6b03e4..7888cc3810 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -270,16 +270,19 @@ len arg 5 len + +bnz label1 global MinTxnFee global MinBalance global MaxTxnLife txn Sender txn Fee +bnz label1 txn FirstValid txn LastValid txn Note txn Receiver txn Amount +label1: txn CloseRemainderTo txn VotePK txn SelectionPK diff --git a/installer/config.json.example b/installer/config.json.example index 2531d5f4d7..58459753ac 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -44,5 +44,6 @@ "TxPoolSize": 15000, "TxSyncIntervalSeconds": 60, "TxSyncServeResponseSize": 1000000, - "TxSyncTimeoutSeconds": 30 + "TxSyncTimeoutSeconds": 30, + "PeerConnectionsUpdateInterval": 3600 } diff --git a/logging/log.go b/logging/log.go index d756895b7d..aa99c20dce 100644 --- a/logging/log.go +++ b/logging/log.go @@ -154,7 +154,7 @@ type Logger interface { AddHook(hook logrus.Hook) EnableTelemetry(cfg TelemetryConfig) error - UpdateTelemetryURI(uri string) bool + UpdateTelemetryURI(uri string) error GetTelemetryEnabled() bool Metrics(category telemetryspec.Category, metrics telemetryspec.MetricDetails, details interface{}) Event(category telemetryspec.Category, identifier telemetryspec.Event) @@ -372,12 +372,12 @@ func (l logger) EnableTelemetry(cfg TelemetryConfig) (err error) { return EnableTelemetry(cfg, &l) } -func (l logger) UpdateTelemetryURI(uri string) bool { - if l.loggerState.telemetry.hook.UpdateHookURI(uri) { +func (l logger) UpdateTelemetryURI(uri string) (err error) { + err = l.loggerState.telemetry.hook.UpdateHookURI(uri) + if err == nil { telemetryConfig.URI = uri - return true } - return false + return } func (l logger) GetTelemetryEnabled() bool { diff --git a/logging/telemetryhook.go b/logging/telemetryhook.go index 8aa74b98ec..d3a9e5855e 100644 --- a/logging/telemetryhook.go +++ b/logging/telemetryhook.go @@ -195,11 +195,11 @@ func createTelemetryHook(cfg TelemetryConfig, history *logBuffer, hookFactory ho // Note: This will be removed with the externalized telemetry project. Return whether or not the URI was successfully // updated. -func (hook *asyncTelemetryHook) UpdateHookURI(uri string) bool { +func (hook *asyncTelemetryHook) UpdateHookURI(uri string) (err error) { updated := false if hook.wrappedHook == nil { - return false + return fmt.Errorf("asyncTelemetryHook.wrappedHook is nil") } tfh, ok := hook.wrappedHook.(*telemetryFilteredHook) @@ -208,7 +208,8 @@ func (hook *asyncTelemetryHook) UpdateHookURI(uri string) bool { copy := tfh.telemetryConfig copy.URI = uri - newHook, err := tfh.factory(copy) + var newHook logrus.Hook + newHook, err = tfh.factory(copy) if err == nil && newHook != nil { tfh.wrappedHook = newHook @@ -224,6 +225,8 @@ func (hook *asyncTelemetryHook) UpdateHookURI(uri string) bool { if updated { hook.urlUpdate <- true } + } else { + return fmt.Errorf("asyncTelemetryHook.wrappedHook does not implement telemetryFilteredHook") } - return updated + return } diff --git a/logging/telemetryspec/event.go b/logging/telemetryspec/event.go index db2aa06fe8..206023cffa 100644 --- a/logging/telemetryspec/event.go +++ b/logging/telemetryspec/event.go @@ -253,3 +253,26 @@ type HTTPRequestDetails struct { BodyLength uint64 // The returned body length, in bytes UserAgent string // The user-agent string ( if any ) } + +// PeerConnectionsEvent event +const PeerConnectionsEvent Event = "PeerConnections" + +// PeersConnectionDetails contains details for PeerConnectionsEvent +type PeersConnectionDetails struct { + IncomingPeers []PeerConnectionDetails + OutgoingPeers []PeerConnectionDetails +} + +// PeerConnectionDetails contains details for PeerConnectionsEvent regarding a single peer ( either incoming or outgoing ) +type PeerConnectionDetails struct { + // Address is the IP address of the remote connected socket + Address string + // The HostName is the TelemetryGUID passed via the X-Algorand-TelId header during the http connection handshake. + HostName string + // InstanceName is the node-specific hashed instance name that was passed via X-Algorand-InstanceName header during the http connection handshake. + InstanceName string + // ConnectionDuration is the duration of the connection, in seconds. + ConnectionDuration uint + // Endpoint is the dialed-to address, for an outgoing connection. Not being used for incoming connection. + Endpoint string `json:",omitempty"` +} diff --git a/network/wsNetwork.go b/network/wsNetwork.go index fe9f2455fb..d74084933e 100644 --- a/network/wsNetwork.go +++ b/network/wsNetwork.go @@ -317,6 +317,9 @@ 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 } type broadcastRequest struct { @@ -516,6 +519,7 @@ func (wn *WebsocketNetwork) setup() { wn.upgrader.ReadBufferSize = 4096 wn.upgrader.WriteBufferSize = 4096 wn.upgrader.EnableCompression = false + wn.lastPeerConnectionsSent = time.Now() wn.router = mux.NewRouter() wn.router.Handle(GossipNetworkPath, wn) wn.requestsTracker = makeRequestsTracker(wn.router, wn.log, wn.config) @@ -728,8 +732,10 @@ func (wn *WebsocketNetwork) ClearHandlers() { } func (wn *WebsocketNetwork) setHeaders(header http.Header) { - myTelemetryGUID := wn.log.GetTelemetryHostName() - header.Set(TelemetryIDHeader, myTelemetryGUID) + localTelemetryGUID := wn.log.GetTelemetryHostName() + localInstanceName := wn.log.GetInstanceName() + header.Set(TelemetryIDHeader, localTelemetryGUID) + header.Set(InstanceNameHeader, localInstanceName) header.Set(ProtocolVersionHeader, ProtocolVersion) header.Set(AddressHeader, wn.PublicAddress()) header.Set(NodeRandomHeader, wn.RandomID) @@ -1250,7 +1256,44 @@ func (wn *WebsocketNetwork) meshThread() { if request.done != nil { close(request.done) } + + // 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() + } +} + +// 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() { + now := time.Now() + if wn.lastPeerConnectionsSent.Add(time.Duration(wn.config.PeerConnectionsUpdateInterval)*time.Second).After(now) || wn.config.PeerConnectionsUpdateInterval <= 0 { + // it's not yet time to send the update. + return + } + wn.lastPeerConnectionsSent = now + var peers []*wsPeer + peers = wn.peerSnapshot(peers) + var connectionDetails telemetryspec.PeersConnectionDetails + for _, peer := range peers { + connDetail := telemetryspec.PeerConnectionDetails{ + ConnectionDuration: uint(now.Sub(peer.createTime).Seconds()), + HostName: peer.TelemetryGUID, + InstanceName: peer.InstanceName, + } + if peer.outgoing { + connDetail.Address = justHost(peer.conn.RemoteAddr().String()) + connDetail.Endpoint = peer.GetAddress() + connectionDetails.OutgoingPeers = append(connectionDetails.OutgoingPeers, connDetail) + } else { + connDetail.Address = peer.OriginAddress() + connectionDetails.IncomingPeers = append(connectionDetails.IncomingPeers, connDetail) + } } + + wn.log.EventWithDetails(telemetryspec.Network, telemetryspec.PeerConnectionsEvent, connectionDetails) } // prioWeightRefreshTime controls how often we refresh the weights @@ -1363,7 +1406,7 @@ func (wn *WebsocketNetwork) peersToPing() []*wsPeer { } func (wn *WebsocketNetwork) getDNSAddrs(dnsBootstrap string) []string { - srvPhonebook, err := tools_network.ReadFromSRV("algobootstrap", dnsBootstrap, wn.config.FallbackDNSResolverAddress) + srvPhonebook, err := tools_network.ReadFromSRV("algobootstrap", "tcp", dnsBootstrap, wn.config.FallbackDNSResolverAddress) if err != nil { // only log this warning on testnet or devnet if wn.NetworkID == config.Devnet || wn.NetworkID == config.Testnet { @@ -1549,7 +1592,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { } peer := &wsPeer{wsPeerCore: wsPeerCore{net: wn, rootURL: addr}, conn: conn, outgoing: true, incomingMsgFilter: wn.incomingMsgFilter, createTime: time.Now()} - peer.TelemetryGUID = response.Header.Get(TelemetryIDHeader) + peer.TelemetryGUID, peer.InstanceName, _ = getCommonHeaders(response.Header) peer.init(wn.config, wn.outgoingMessagesBufferSize) wn.addPeer(peer) localAddr, _ := wn.Address() @@ -1559,7 +1602,7 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) { Address: justHost(conn.RemoteAddr().String()), HostName: peer.TelemetryGUID, Incoming: false, - InstanceName: myInstanceName, + InstanceName: peer.InstanceName, }) peers.Set(float64(wn.NumPeers()), nil) diff --git a/scripts/travis/configure_dev.sh b/scripts/travis/configure_dev.sh index e8ea08439b..5df887d069 100755 --- a/scripts/travis/configure_dev.sh +++ b/scripts/travis/configure_dev.sh @@ -56,6 +56,12 @@ if [ "${OS}" = "linux" ]; then sudo apt-get update -y sudo apt-get -y install sqlite3 fi +elif [ "${OS}" = "darwin" ]; then + # we don't want to upgrade boost if we already have it, as it will try to update + # other components. + brew update + brew tap homebrew/cask + brew pin boost || true fi "${SCRIPTPATH}/../configure_dev.sh" diff --git a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp index 82711c029f..829cccf30d 100755 --- a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp +++ b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp @@ -21,6 +21,9 @@ namespace eval ::AlgorandGoal { namespace export GetLedgerSupply namespace export WaitForRound namespace export Report + namespace export ListParticipationKeys + namespace export AddParticipationKey + namespace export TakeAccountOnline # My Variables set version 1.0 @@ -669,6 +672,45 @@ proc ::AlgorandGoal::Report { TEST_PRIMARY_NODE_DIR } { -re {Last commited block: (\d+)} {puts "status check ok"} } } EXCEPTION ] } { - ::AlgorandGoal::Abort "ERROR in GetLedgerSupply: $EXCEPTION" + ::AlgorandGoal::Abort "ERROR in Report: $EXCEPTION" } } + +# List Participation keys +proc ::AlgorandGoal::ListParticipationKeys { TEST_PRIMARY_NODE_DIR } { + if { [ catch { + spawn goal account listpartkeys -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { ::AlgorandGoal::Abort "goal ListParticipationKeys timed out" } + close + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in ListParticipationKeys: $EXCEPTION" + } +} + +# Add a participation key +proc ::AlgorandGoal::AddParticipationKey { ADDRESS FIRST_ROUND LAST_ROUND TEST_PRIMARY_NODE_DIR } { + if { [ catch { + spawn goal account addpartkey --address $ADDRESS --roundFirstValid $FIRST_ROUND --roundLastValid $LAST_ROUND -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { ::AlgorandGoal::Abort "goal AddParticipationKey timed out" } + close + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AddParticipationKey: $EXCEPTION" + } +} + +# Register online participation with a given account +proc ::AlgorandGoal::TakeAccountOnline { ADDRESS FIRST_ROUND LAST_ROUND TEST_PRIMARY_NODE_DIR } { + if { [ catch { + spawn goal account changeonlinestatus --address $ADDRESS --firstvalid $FIRST_ROUND --lastvalid $LAST_ROUND -d datadir + expect { + timeout { ::AlgorandGoal::Abort "goal TakeAccountOnline timed out" } + close + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in TakeAccountOnline: $EXCEPTION" + } +} \ No newline at end of file diff --git a/test/e2e-go/cli/goal/expect/listExpiredParticipationKeyTest.exp b/test/e2e-go/cli/goal/expect/listExpiredParticipationKeyTest.exp new file mode 100644 index 0000000000..20305e7d1c --- /dev/null +++ b/test/e2e-go/cli/goal/expect/listExpiredParticipationKeyTest.exp @@ -0,0 +1,51 @@ +#!/usr/bin/expect -f + +set err 0 +log_user 1 + +if { [catch { + + source goalExpectCommon.exp + + set TEST_ALGO_DIR [lindex $argv 0] + set TEST_DATA_DIR [lindex $argv 1] + + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + puts "TEST_DATA_DIR: $TEST_DATA_DIR" + + set TIME_STAMP [clock seconds] + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary/ + set NETWORK_NAME test_net_expect_$TIME_STAMP + set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/ThreeNodesEvenDist.json" + + exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR + + # Create network + ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + set PRIMARY_WALLET_NAME unencrypted-default-wallet + + set PRIMARY_ACCOUNT_ADDRESS [::AlgorandGoal::GetHighestFundedAccountForWallet $PRIMARY_WALLET_NAME $TEST_PRIMARY_NODE_DIR] + + # Register participation keys + set ROUND_FIRST_VALID 1 + set KEY_EXPIRY_ROUND 10 + ::AlgorandGoal::AddParticipationKey $PRIMARY_ACCOUNT_ADDRESS $ROUND_FIRST_VALID $KEY_EXPIRY_ROUND $TEST_PRIMARY_NODE_DIR + ::AlgorandGoal::TakeAccountOnline $PRIMARY_ACCOUNT_ADDRESS $ROUND_FIRST_VALID $KEY_EXPIRY_ROUND $TEST_PRIMARY_NODE_DIR + + # Wait for expiry + ::AlgorandGoal::WaitForRound $KEY_EXPIRY_ROUND $TEST_PRIMARY_NODE_DIR + + # List participation keys + ::AlgorandGoal::ListParticipationKeys $TEST_PRIMARY_NODE_DIR + + # Clean up + ::AlgorandGoal::StopNetwork $NETWORK_NAME $TEST_ALGO_DIR $TEST_ROOT_DIR + + exit 0 + +} EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in listExpiredParticipationKeyTest: $EXCEPTION" +} diff --git a/test/testdata/configs/config-v5.json b/test/testdata/configs/config-v5.json index d89b3da832..334ac63c86 100644 --- a/test/testdata/configs/config-v5.json +++ b/test/testdata/configs/config-v5.json @@ -43,5 +43,6 @@ "TxSyncIntervalSeconds": 60, "TxSyncTimeoutSeconds": 30, "TxSyncServeResponseSize": 1000000, - "SuggestedFeeSlidingWindowSize": 50 + "SuggestedFeeSlidingWindowSize": 50, + "PeerConnectionsUpdateInterval": 3600 } diff --git a/tools/network/bootstrap.go b/tools/network/bootstrap.go index 782cfdc212..6904a9f299 100644 --- a/tools/network/bootstrap.go +++ b/tools/network/bootstrap.go @@ -9,14 +9,18 @@ import ( ) // ReadFromSRV is a helper to collect SRV addresses for a given name. -func ReadFromSRV(service string, name string, fallbackDNSResolverAddress string) (addrs []string, err error) { +func ReadFromSRV(service string, protocol string, name string, fallbackDNSResolverAddress string) (addrs []string, err error) { log := logging.Base() if name == "" { log.Debug("no dns lookup due to empty name") return } + if protocol != "tcp" && protocol != "udp" && protocol != "tls" { + err = fmt.Errorf("unsupported protocol '%s' specified", protocol) + return + } - _, records, sysLookupErr := net.LookupSRV(service, "tcp", name) + _, records, sysLookupErr := net.LookupSRV(service, protocol, name) if sysLookupErr != nil { var resolver Resolver // try to resolve the address. If it's an dotted-numbers format, it would return that right away. @@ -28,7 +32,7 @@ func ReadFromSRV(service string, name string, fallbackDNSResolverAddress string) log.Infof("ReadFromBootstrap: Failed to resolve fallback DNS resolver address '%s': %v; falling back to default fallback resolver address", fallbackDNSResolverAddress, err2) } - _, records, err = resolver.LookupSRV(context.Background(), service, "tcp", name) + _, records, err = resolver.LookupSRV(context.Background(), service, protocol, name) if err != nil { err = fmt.Errorf("ReadFromBootstrap: DNS LookupSRV failed when using system resolver(%v) as well as via %s due to %v", sysLookupErr, resolver.EffectiveResolverDNS(), err) return diff --git a/tools/network/telemetryURIUpdateService.go b/tools/network/telemetryURIUpdateService.go index a147b73ad9..aa0183dd3e 100644 --- a/tools/network/telemetryURIUpdateService.go +++ b/tools/network/telemetryURIUpdateService.go @@ -1,6 +1,8 @@ package network import ( + "net/url" + "strings" "time" "github.com/algorand/go-algorand/config" @@ -8,17 +10,45 @@ import ( "github.com/algorand/go-algorand/protocol" ) +type telemetrySrvReader interface { + readFromSRV(protocol string, bootstrapID string) (addrs []string, err error) +} + +type telemetryURIUpdater struct { + interval time.Duration + cfg config.Local + genesisNetwork protocol.NetworkID + log logging.Logger + abort chan struct{} + srvReader telemetrySrvReader +} + // StartTelemetryURIUpdateService starts a go routine which queries SRV records for a telemetry URI every func StartTelemetryURIUpdateService(interval time.Duration, cfg config.Local, genesisNetwork protocol.NetworkID, log logging.Logger, abort chan struct{}) { + updater := &telemetryURIUpdater{ + interval: interval, + cfg: cfg, + genesisNetwork: genesisNetwork, + log: log, + abort: abort, + } + updater.srvReader = updater + updater.Start() + +} +func (t *telemetryURIUpdater) Start() { go func() { - ticker := time.NewTicker(interval) + ticker := time.NewTicker(t.interval) defer ticker.Stop() updateTelemetryURI := func() { - endpoint := lookupTelemetryEndpoint(cfg, genesisNetwork, log) + endpointURL := t.lookupTelemetryURL() - if endpoint != "" && endpoint != log.GetTelemetryURI() { - log.UpdateTelemetryURI(endpoint) + if endpointURL != nil && endpointURL.String() != t.log.GetTelemetryURI() { + err := t.log.UpdateTelemetryURI(endpointURL.String()) + if err != nil { + t.log.Warnf("Unable to update telemetry URI to '%s' : %v", endpointURL.String(), err) + } } } @@ -28,27 +58,62 @@ func StartTelemetryURIUpdateService(interval time.Duration, cfg config.Local, ge select { case <-ticker.C: updateTelemetryURI() - case <-abort: + case <-t.abort: return } } }() } -func lookupTelemetryEndpoint(cfg config.Local, genesisNetwork protocol.NetworkID, log logging.Logger) string { - bootstrapArray := cfg.DNSBootstrapArray(genesisNetwork) +func (t *telemetryURIUpdater) lookupTelemetryURL() (url *url.URL) { + bootstrapArray := t.cfg.DNSBootstrapArray(t.genesisNetwork) bootstrapArray = append(bootstrapArray, "default.algodev.network") for _, bootstrapID := range bootstrapArray { - addrs, err := ReadFromSRV("telemetry", bootstrapID, cfg.FallbackDNSResolverAddress) + addrs, err := t.srvReader.readFromSRV("tls", bootstrapID) if err != nil { - log.Infof("An issue occurred reading telemetry entry for '%s': %v", bootstrapID, err) + t.log.Infof("An issue occurred reading telemetry entry for '_telemetry._tls.%s': %v", bootstrapID, err) } else if len(addrs) == 0 { - log.Infof("No telemetry entry for: '%s'", bootstrapID) + t.log.Infof("No telemetry entry for: '_telemetry._tls.%s'", bootstrapID) } else { - return addrs[0] + for _, addr := range addrs { + // the addr that we received from ReadFromSRV contains host:port, we need to prefix that with the schema. since it's the tls, we want to use https. + url, err = url.Parse("https://" + addr) + if err != nil { + t.log.Infof("a telemetry endpoint '%s' was retrieved for '_telemerty._tls.%s'. This does not seems to be a valid endpoint and will be ignored(%v).", addr, bootstrapID, err) + continue + } + return url + } + } + + addrs, err = t.srvReader.readFromSRV("tcp", bootstrapID) + if err != nil { + t.log.Infof("An issue occurred reading telemetry entry for '_telemetry._tcp.%s': %v", bootstrapID, err) + } else if len(addrs) == 0 { + t.log.Infof("No telemetry entry for: '_telemetry._tcp.%s'", bootstrapID) + } else { + for _, addr := range addrs { + if strings.HasPrefix(addr, "https://") { + // the addr that we received from ReadFromSRV should contain host:port. however, in some cases, it might contain a https prefix, where we want to take it as is. + url, err = url.Parse(addr) + } else { + // the addr that we received from ReadFromSRV contains host:port, we need to prefix that with the schema. since it's the tcp, we want to use http. + url, err = url.Parse("http://" + addr) + } + + if err != nil { + t.log.Infof("a telemetry endpoint '%s' was retrieved for '_telemerty._tcp.%s'. This does not seems to be a valid endpoint and will be ignored(%v).", addr, bootstrapID, err) + continue + } + return url + } } } - log.Warn("No telemetry endpoint was found.") - return "" + t.log.Warn("No telemetry endpoint was found.") + return nil +} + +func (t *telemetryURIUpdater) readFromSRV(protocol string, bootstrapID string) (addrs []string, err error) { + return ReadFromSRV("telemetry", protocol, bootstrapID, t.cfg.FallbackDNSResolverAddress) } diff --git a/tools/network/telemetryURIUpdateService_test.go b/tools/network/telemetryURIUpdateService_test.go new file mode 100644 index 0000000000..7971bcd285 --- /dev/null +++ b/tools/network/telemetryURIUpdateService_test.go @@ -0,0 +1,90 @@ +// Copyright (C) 2019 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package network + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" +) + +type telemetryURIUpdaterTest struct { + telemetryURIUpdater + readFromSRVResults map[string][]string +} + +func (t *telemetryURIUpdaterTest) readFromSRV(protocol string, bootstrapID string) (addrs []string, err error) { + if addr, ok := t.readFromSRVResults[protocol+bootstrapID]; ok { + return addr, nil + } + fmt.Printf("no result for %s %s\n", protocol, bootstrapID) + return nil, fmt.Errorf("no cached results") +} + +func makeTelemetryURIUpdaterTest(genesisNetwork protocol.NetworkID) *telemetryURIUpdaterTest { + t := &telemetryURIUpdaterTest{ + telemetryURIUpdater: telemetryURIUpdater{ + cfg: config.GetDefaultLocal(), + log: logging.Base(), + genesisNetwork: genesisNetwork, + }, + readFromSRVResults: make(map[string][]string), + } + t.srvReader = t + return t +} + +func (t *telemetryURIUpdaterTest) add(protocol, bootstrap string, addrs []string) { + t.readFromSRVResults[protocol+bootstrap] = addrs +} + +func TestTelemetryURILookup(t *testing.T) { + + // trivial success case. + uriUpdater := makeTelemetryURIUpdaterTest(config.Devnet) + uriUpdater.add("tcp", "devnet.algodev.network", []string{"myhost:4160"}) + uri := uriUpdater.lookupTelemetryURL() + require.NotNil(t, uri) + require.Equal(t, "http://myhost:4160", uri.String()) + + // check https prefixing + uriUpdater = makeTelemetryURIUpdaterTest(config.Devnet) + uriUpdater.add("tcp", "devnet.algodev.network", []string{"https://myhost:4160"}) + uri = uriUpdater.lookupTelemetryURL() + require.NotNil(t, uri) + require.Equal(t, "https://myhost:4160", uri.String()) + + // check https priority + uriUpdater = makeTelemetryURIUpdaterTest(config.Devnet) + uriUpdater.add("tcp", "devnet.algodev.network", []string{"myhost2:4160"}) + uriUpdater.add("tls", "devnet.algodev.network", []string{"myhost1:4160"}) + uri = uriUpdater.lookupTelemetryURL() + require.NotNil(t, uri) + require.Equal(t, "https://myhost1:4160", uri.String()) + + // check fallback + uriUpdater = makeTelemetryURIUpdaterTest(config.Devnet) + uriUpdater.add("tcp", "default.algodev.network", []string{"fallbackhost:8123"}) + uri = uriUpdater.lookupTelemetryURL() + require.NotNil(t, uri) + require.Equal(t, "http://fallbackhost:8123", uri.String()) +}