diff --git a/config.go b/config.go index 441423f..0303f3a 100644 --- a/config.go +++ b/config.go @@ -21,14 +21,13 @@ var ( customSpamParams = programs.CustomSpamParams{ ClientURLs: urls, FaucetURL: "http://localhost:8088", - SpamTypes: []string{spammer.TypeBlock}, - Rates: []int{1}, - Durations: []time.Duration{time.Second * 20}, - BlkToBeSent: []int{0}, + SpamType: spammer.TypeBlock, + Rate: 1, TimeUnit: time.Second, DelayBetweenConflicts: 0, NSpend: 2, Scenario: evilwallet.Scenario1(), + ScenarioName: "guava", DeepSpam: false, EnableRateSetter: false, AccountAlias: accountwallet.FaucetAccountAlias, diff --git a/main.go b/main.go index f34461a..a358881 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,8 @@ func main() { case ScriptInteractive: interactive.Run() case ScriptSpammer: - programs.CustomSpam(&customSpamParams, accWallet) + dispatcher := programs.NewDispatcher(accWallet) + dispatcher.RunSpam(&customSpamParams) case ScriptAccounts: accountsSubcommands(accWallet, accountsSubcommandsFlags) default: diff --git a/parse.go b/parse.go index 7a089c2..6240428 100644 --- a/parse.go +++ b/parse.go @@ -4,12 +4,12 @@ import ( "flag" "fmt" "os" - "strconv" "strings" "time" "github.com/iotaledger/evil-tools/pkg/accountwallet" "github.com/iotaledger/evil-tools/pkg/evilwallet" + "github.com/iotaledger/evil-tools/pkg/spammer" "github.com/iotaledger/hive.go/ierrors" ) @@ -56,12 +56,11 @@ func parseOptionFlagSet(flagSet *flag.FlagSet, args ...[]string) { func parseBasicSpamFlags() { urls := optionFlagSet.String("urls", "", "API urls for clients used in test separated with commas") - spamTypes := optionFlagSet.String("spammer", "", "Spammers used during test. Format: strings separated with comma, available options: 'blk' - block,"+ + spamType := optionFlagSet.String("spammer", "", "Spammers used during test. Format: strings separated with comma, available options: 'blk' - block,"+ " 'tx' - transaction, 'ds' - double spends spammers, 'nds' - n-spends spammer, 'custom' - spams with provided scenario, 'bb' - blowball") - rate := optionFlagSet.String("rate", "", "Spamming rate for provided 'spammer'. Format: numbers separated with comma, e.g. 10,100,1 if three spammers were provided for 'spammer' parameter.") - duration := optionFlagSet.String("duration", "", "Spam duration. Cannot be combined with flag 'blkNum'. Format: separated by commas list of decimal numbers, each with optional fraction and a unit suffix, such as '300ms', '-1.5h' or '2h45m'.\n Valid time units are 'ns', 'us', 'ms', 's', 'm', 'h'.") - blkNum := optionFlagSet.String("blkNum", "", "Spam duration in seconds. Cannot be combined with flag 'duration'. Format: numbers separated with comma, e.g. 10,100,1 if three spammers were provided for 'spammer' parameter.") - timeunit := optionFlagSet.Duration("tu", customSpamParams.TimeUnit, "Time unit for the spamming rate. Format: decimal numbers, each with optional fraction and a unit suffix, such as '300ms', '-1.5h' or '2h45m'.\n Valid time units are 'ns', 'us', 'ms', 's', 'm', 'h'.") + rate := optionFlagSet.Int("rate", customSpamParams.Rate, "Spamming rate for provided 'spammer'. Format: numbers separated with comma, e.g. 10,100,1 if three spammers were provided for 'spammer' parameter.") + duration := optionFlagSet.String("duration", "", "Spam duration. If not provided spam will lats infinitely. Format: separated by commas list of decimal numbers, each with optional fraction and a unit suffix, such as '300ms', '-1.5h' or '2h45m'.\n Valid time units are 'ns', 'us', 'ms', 's', 'm', 'h'.") + timeunit := optionFlagSet.Duration("unit", customSpamParams.TimeUnit, "Time unit for the spamming rate. Format: decimal numbers, each with optional fraction and a unit suffix, such as '300ms', '-1.5h' or '2h45m'.\n Valid time units are 'ns', 'us', 'ms', 's', 'm', 'h'.") delayBetweenConflicts := optionFlagSet.Duration("dbc", customSpamParams.DelayBetweenConflicts, "delayBetweenConflicts - Time delay between conflicts in double spend spamming") scenario := optionFlagSet.String("scenario", "", "Name of the EvilBatch that should be used for the spam. By default uses Scenario1. Possible scenarios can be found in evilwallet/customscenarion.go.") deepSpam := optionFlagSet.Bool("deep", customSpamParams.DeepSpam, "Enable the deep spam, by reusing outputs created during the spam.") @@ -74,26 +73,18 @@ func parseBasicSpamFlags() { parsedUrls := parseCommaSepString(*urls) customSpamParams.ClientURLs = parsedUrls } - if *spamTypes != "" { - parsedSpamTypes := parseCommaSepString(*spamTypes) - customSpamParams.SpamTypes = parsedSpamTypes - } - if *rate != "" { - parsedRates := parseCommaSepInt(*rate) - customSpamParams.Rates = parsedRates - } + customSpamParams.SpamType = *spamType + customSpamParams.Rate = *rate if *duration != "" { - parsedDurations := parseDurations(*duration) - customSpamParams.Durations = parsedDurations - } - if *blkNum != "" { - parsedBlkNums := parseCommaSepInt(*blkNum) - customSpamParams.BlkToBeSent = parsedBlkNums + customSpamParams.Duration, _ = time.ParseDuration(*duration) + } else { + customSpamParams.Duration = spammer.InfiniteDuration } if *scenario != "" { conflictBatch, ok := evilwallet.GetScenario(*scenario) if ok { customSpamParams.Scenario = conflictBatch + customSpamParams.ScenarioName = *scenario } } @@ -104,14 +95,6 @@ func parseBasicSpamFlags() { if *account != "" { customSpamParams.AccountAlias = *account } - - // fill in unused parameter: blkNum or duration with zeros - if *duration == "" && *blkNum != "" { - customSpamParams.Durations = make([]time.Duration, len(customSpamParams.BlkToBeSent)) - } - if *blkNum == "" && *duration != "" { - customSpamParams.BlkToBeSent = make([]int, len(customSpamParams.Durations)) - } } // readSubcommandsAndFlagSets splits the subcommands on multiple flag sets. @@ -436,23 +419,3 @@ func parseCommaSepString(urls string) []string { return split } - -func parseCommaSepInt(nums string) []int { - split := strings.Split(nums, ",") - parsed := make([]int, len(split)) - for i, num := range split { - parsed[i], _ = strconv.Atoi(num) - } - - return parsed -} - -func parseDurations(durations string) []time.Duration { - split := strings.Split(durations, ",") - parsed := make([]time.Duration, len(split)) - for i, dur := range split { - parsed[i], _ = time.ParseDuration(dur) - } - - return parsed -} diff --git a/pkg/evilwallet/evilwallet.go b/pkg/evilwallet/evilwallet.go index c88c015..eeb71b9 100644 --- a/pkg/evilwallet/evilwallet.go +++ b/pkg/evilwallet/evilwallet.go @@ -1,7 +1,6 @@ package evilwallet import ( - "fmt" "sync" "time" @@ -22,8 +21,15 @@ import ( const ( // FaucetRequestSplitNumber defines the number of outputs to split from a faucet request. - FaucetRequestSplitNumber = 120 - faucetTokensPerRequest iotago.BaseToken = 432_000_000 + FaucetRequestSplitNumber = 50 + // MaxBigWalletsCreatedAtOnce is maximum of evil wallets that can be created at once for non-infinite spam. + MaxBigWalletsCreatedAtOnce = 10 + // BigFaucetWalletDeposit indicates the minimum outputs left number that triggers funds requesting in the background. + BigFaucetWalletDeposit = 4 + // CheckFundsLeftInterval is the interval to check funds left in the background for requesting funds triggering. + CheckFundsLeftInterval = time.Second * 5 + // BigFaucetWalletsAtOnce number of faucet wallets requested at once in the background. + BigFaucetWalletsAtOnce = 2 ) var ( @@ -213,7 +219,7 @@ func (e *EvilWallet) RequestFreshBigFaucetWallets(numberOfWallets int) bool { } wg.Wait() - e.log.Debugf("Finished requesting %d wallets from faucet", numberOfWallets) + e.log.Debugf("Finished requesting %d wallets from faucet, outputs available: %d", numberOfWallets, e.UnspentOutputsLeft(Fresh)) return success } @@ -464,28 +470,11 @@ func (e *EvilWallet) CreateTransaction(options ...Option) (*models.PayloadIssuan e.addOutputsToOutputManager(signedTx, buildOptions.outputWallet, tempWallet, tempAddresses) e.registerOutputAliases(signedTx, addrAliasMap) - e.log.Debugf("\n %s", printTransaction(signedTx)) + //e.log.Debugf("\n %s", printTransaction(signedTx)) return txData, nil } -func printTransaction(tx *iotago.SignedTransaction) string { - txDetails := "" - txDetails += fmt.Sprintf("Transaction ID; %s, slotCreation: %d\n", lo.PanicOnErr(tx.ID()).ToHex(), tx.Transaction.CreationSlot) - for index, out := range tx.Transaction.Outputs { - txDetails += fmt.Sprintf("Output index: %d, base token: %d, stored mana: %d\n", index, out.BaseTokenAmount(), out.StoredMana()) - } - txDetails += fmt.Sprintln("Allotments:") - for _, allotment := range tx.Transaction.Allotments { - txDetails += fmt.Sprintf("AllotmentID: %s, value: %d\n", allotment.AccountID, allotment.Mana) - } - for _, allotment := range tx.Transaction.TransactionEssence.Allotments { - txDetails += fmt.Sprintf("al 2 AllotmentID: %s, value: %d\n", allotment.AccountID, allotment.Mana) - } - - return txDetails -} - // addOutputsToOutputManager adds output to the OutputManager if. func (e *EvilWallet) addOutputsToOutputManager(signedTx *iotago.SignedTransaction, outWallet, tmpWallet *Wallet, tempAddresses map[string]types.Empty) { for idx, o := range signedTx.Transaction.Outputs { @@ -762,7 +751,6 @@ func (e *EvilWallet) updateOutputBalances(buildOptions *Options) (err error) { } func (e *EvilWallet) makeTransaction(inputs []*models.Output, outputs iotago.Outputs[iotago.Output], w *Wallet, congestionResponse *apimodels.CongestionResponse, issuerAccountID iotago.AccountID) (tx *iotago.SignedTransaction, err error) { - e.log.Debugf("makeTransaction len(outputs): %d", len(outputs)) clt := e.Connector().GetClient() currentTime := time.Now() targetSlot := clt.LatestAPI().TimeProvider().SlotFromTime(currentTime) diff --git a/pkg/interactive/interactive.go b/pkg/interactive/interactive.go index da40421..9ad70b1 100644 --- a/pkg/interactive/interactive.go +++ b/pkg/interactive/interactive.go @@ -3,17 +3,16 @@ package interactive import ( "encoding/json" "fmt" - "io" "os" "strconv" "strings" - "text/tabwriter" "time" "github.com/AlecAivazis/survey/v2" "go.uber.org/atomic" "github.com/iotaledger/evil-tools/pkg/evilwallet" + "github.com/iotaledger/evil-tools/pkg/models" "github.com/iotaledger/evil-tools/pkg/spammer" "github.com/iotaledger/evil-tools/programs" "github.com/iotaledger/hive.go/ds/types" @@ -25,7 +24,6 @@ const ( faucetFundsCheck = time.Minute / 12 maxConcurrentSpams = 5 lastSpamsShowed = 15 - timeFormat = "2006/01/02 15:04:05" configFilename = "interactive_config.json" ) @@ -40,20 +38,7 @@ var ( minSpamOutputs int ) -type Config struct { - //nolint:tagliatelle - WebAPI []string `json:"webAPI"` - FaucetURL string `json:"faucetUrl"` - Rate int `json:"rate"` - DurationStr string `json:"duration"` - TimeUnitStr string `json:"timeUnit"` - Deep bool `json:"deepEnabled"` - Reuse bool `json:"reuseEnabled"` - Scenario string `json:"scenario"` - AutoRequesting bool `json:"autoRequestingEnabled"` - AutoRequestingAmount string `json:"autoRequestingAmount"` - UseRateSetter bool `json:"useRateSetter"` - +type config struct { duration time.Duration timeUnit time.Duration clientURLs map[string]types.Empty @@ -61,7 +46,6 @@ type Config struct { var configJSON = fmt.Sprintf(`{ "webAPI": ["http://localhost:8080","http://localhost:8090"], - "faucetUrl": "http://localhost:8088", "rate": 2, "duration": "20s", "timeUnit": "1s", @@ -73,15 +57,8 @@ var configJSON = fmt.Sprintf(`{ "useRateSetter": true }`, spammer.TypeTx) -var defaultConfig = Config{ - clientURLs: map[string]types.Empty{ - "http://localhost:8080": types.Void, - "http://localhost:8090": types.Void, - }, - FaucetURL: "http://localhost:8088", +var defaultConfig = models.Config{ Rate: 2, - duration: 20 * time.Second, - timeUnit: time.Second, Deep: false, Reuse: true, Scenario: spammer.TypeTx, @@ -90,6 +67,15 @@ var defaultConfig = Config{ UseRateSetter: true, } +var defaultInteractiveConfig = config{ + clientURLs: map[string]types.Empty{ + "http://localhost:8050": types.Void, + "http://localhost:8060": types.Void, + }, + duration: 20 * time.Second, + timeUnit: time.Second, +} + const ( requestAmount100 = "100" requestAmount10k = "10000" @@ -205,13 +191,14 @@ type Mode struct { preparingFunds bool - Config Config + Config models.Config + innerConfig config blkSent *atomic.Uint64 txSent *atomic.Uint64 scenariosSent *atomic.Uint64 activeSpammers map[int]*spammer.Spammer - spammerLog *SpammerLog + spammerLog *models.SpammerLog spamMutex syncutils.Mutex stdOutMutex syncutils.Mutex @@ -226,11 +213,12 @@ func NewInteractiveMode() *Mode { spamFinished: make(chan int), Config: defaultConfig, + innerConfig: defaultInteractiveConfig, blkSent: atomic.NewUint64(0), txSent: atomic.NewUint64(0), scenariosSent: atomic.NewUint64(0), - spammerLog: NewSpammerLog(), + spammerLog: models.NewSpammerLog(), activeSpammers: make(map[int]*spammer.Spammer), } } @@ -327,7 +315,7 @@ func (m *Mode) prepareFunds() { printer.FundsCurrentlyPreparedWarning() return } - if len(m.Config.clientURLs) == 0 { + if len(m.innerConfig.clientURLs) == 0 { printer.NotEnoughClientsWarning(1) } numToPrepareStr := "" @@ -379,9 +367,9 @@ func (m *Mode) spamMenu() { func (m *Mode) spamSubMenu(menuType string) { switch menuType { case spamDetails: - defaultTimeUnit := timeUnitToString(m.Config.duration) + defaultTimeUnit := timeUnitToString(m.innerConfig.duration) var spamSurvey spamDetailsSurvey - err := survey.Ask(spamDetailsQuestions(strconv.Itoa(int(m.Config.duration.Seconds())), strconv.Itoa(m.Config.Rate), defaultTimeUnit), &spamSurvey) + err := survey.Ask(spamDetailsQuestions(strconv.Itoa(int(m.innerConfig.duration.Seconds())), strconv.Itoa(m.Config.Rate), defaultTimeUnit), &spamSurvey) if err != nil { fmt.Println(err.Error()) m.mainMenu <- types.Void @@ -435,9 +423,9 @@ func (m *Mode) spamSubMenu(menuType string) { } func (m *Mode) areEnoughFundsAvailable() bool { - outputsNeeded := m.Config.Rate * int(m.Config.duration.Seconds()) - if m.Config.timeUnit == time.Minute { - outputsNeeded = int(float64(m.Config.Rate) * m.Config.duration.Minutes()) + outputsNeeded := m.Config.Rate * int(m.innerConfig.duration.Seconds()) + if m.innerConfig.timeUnit == time.Minute { + outputsNeeded = int(float64(m.Config.Rate) * m.innerConfig.duration.Minutes()) } return m.evilWallet.UnspentOutputsLeft(evilwallet.Fresh) < outputsNeeded && m.Config.Scenario != spammer.TypeBlock @@ -449,10 +437,10 @@ func (m *Mode) startSpam() { var s *spammer.Spammer if m.Config.Scenario == spammer.TypeBlock { - s = programs.SpamBlocks(m.evilWallet, m.Config.Rate, time.Second, m.Config.duration, 0, m.Config.UseRateSetter, "") + s = programs.SpamBlocks(m.evilWallet, m.Config.Rate, time.Second, m.innerConfig.duration, m.Config.UseRateSetter, "") } else { scenario, _ := evilwallet.GetScenario(m.Config.Scenario) - s = programs.SpamNestedConflicts(m.evilWallet, m.Config.Rate, time.Second, m.Config.duration, scenario, m.Config.Deep, m.Config.Reuse, m.Config.UseRateSetter, "") + s = programs.SpamNestedConflicts(m.evilWallet, m.Config.Rate, time.Second, m.innerConfig.duration, scenario, m.Config.Deep, m.Config.Reuse, m.Config.UseRateSetter, "") if s == nil { return } @@ -540,11 +528,11 @@ func (m *Mode) validateAndAddURL(url string) { if !ok { printer.URLWarning() } else { - if _, ok := m.Config.clientURLs[url]; ok { + if _, ok := m.innerConfig.clientURLs[url]; ok { printer.URLExists() return } - m.Config.clientURLs[url] = types.Void + m.innerConfig.clientURLs[url] = types.Void m.evilWallet.AddClient(url) } } @@ -636,12 +624,12 @@ func (m *Mode) parseSpamDetails(details spamDetailsSurvey) { } switch details.TimeUnit { case mpm: - m.Config.timeUnit = time.Minute + m.innerConfig.timeUnit = time.Minute case mps: - m.Config.timeUnit = time.Second + m.innerConfig.timeUnit = time.Second } m.Config.Rate = rate - m.Config.duration = dur + m.innerConfig.duration = dur fmt.Println(details) } @@ -658,15 +646,15 @@ func (m *Mode) parseScenario(scenario string) { func (m *Mode) removeUrls(urls []string) { for _, url := range urls { - if _, ok := m.Config.clientURLs[url]; ok { - delete(m.Config.clientURLs, url) + if _, ok := m.innerConfig.clientURLs[url]; ok { + delete(m.innerConfig.clientURLs, url) m.evilWallet.RemoveClient(url) } } } func (m *Mode) urlMapToList() (list []string) { - for url := range m.Config.clientURLs { + for url := range m.innerConfig.clientURLs { list = append(list, url) } @@ -731,26 +719,26 @@ func (m *Mode) loadConfig() { // convert urls array to map if len(m.Config.WebAPI) > 0 { // rewrite default value - for url := range m.Config.clientURLs { + for url := range m.innerConfig.clientURLs { m.evilWallet.RemoveClient(url) } - m.Config.clientURLs = make(map[string]types.Empty) + m.innerConfig.clientURLs = make(map[string]types.Empty) } for _, url := range m.Config.WebAPI { - m.Config.clientURLs[url] = types.Void + m.innerConfig.clientURLs[url] = types.Void m.evilWallet.AddClient(url) } // parse duration - d, err := time.ParseDuration(m.Config.DurationStr) + d, err := time.ParseDuration(m.Config.Duration) if err != nil { d = time.Minute } - u, err := time.ParseDuration(m.Config.TimeUnitStr) + u, err := time.ParseDuration(m.Config.TimeUnit) if err != nil { u = time.Second } - m.Config.duration = d - m.Config.timeUnit = u + m.innerConfig.duration = d + m.innerConfig.timeUnit = u } func (m *Mode) saveConfigsToFile() { @@ -763,15 +751,15 @@ func (m *Mode) saveConfigsToFile() { // update client urls m.Config.WebAPI = []string{} - for url := range m.Config.clientURLs { + for url := range m.innerConfig.clientURLs { m.Config.WebAPI = append(m.Config.WebAPI, url) } // update duration - m.Config.DurationStr = m.Config.duration.String() + m.Config.Duration = m.innerConfig.duration.String() // update time unit - m.Config.TimeUnitStr = m.Config.timeUnit.String() + m.Config.TimeUnit = m.innerConfig.timeUnit.String() jsonConfigs, err := json.MarshalIndent(m.Config, "", " ") if err != nil { @@ -818,85 +806,4 @@ func timeUnitToString(d time.Duration) string { // region SpammerLog /////////////////////////////////////////////////////////////////////////////////////////////////////////// -var ( - historyHeader = "scenario\tstart\tstop\tdeep\treuse\trate\tduration" - historyLineFmt = "%s\t%s\t%s\t%v\t%v\t%d\t%d\n" -) - -type SpammerLog struct { - spamDetails []Config - spamStartTime []time.Time - spamStopTime []time.Time - mu syncutils.Mutex -} - -func NewSpammerLog() *SpammerLog { - return &SpammerLog{ - spamDetails: make([]Config, 0), - spamStartTime: make([]time.Time, 0), - spamStopTime: make([]time.Time, 0), - } -} - -func (s *SpammerLog) SpamDetails(spamID int) *Config { - return &s.spamDetails[spamID] -} - -func (s *SpammerLog) StartTime(spamID int) time.Time { - return s.spamStartTime[spamID] -} - -func (s *SpammerLog) AddSpam(config Config) (spamID int) { - s.mu.Lock() - defer s.mu.Unlock() - - s.spamDetails = append(s.spamDetails, config) - s.spamStartTime = append(s.spamStartTime, time.Now()) - s.spamStopTime = append(s.spamStopTime, time.Time{}) - - return len(s.spamDetails) - 1 -} - -func (s *SpammerLog) SetSpamEndTime(spamID int) { - s.mu.Lock() - defer s.mu.Unlock() - - s.spamStopTime[spamID] = time.Now() -} - -func newTabWriter(writer io.Writer) *tabwriter.Writer { - return tabwriter.NewWriter(writer, 0, 0, 1, ' ', tabwriter.Debug|tabwriter.TabIndent) -} - -func (s *SpammerLog) LogHistory(lastLines int, writer io.Writer) { - s.mu.Lock() - defer s.mu.Unlock() - - w := newTabWriter(writer) - _, _ = fmt.Fprintln(w, historyHeader) - idx := len(s.spamDetails) - lastLines + 1 - if idx < 0 { - idx = 0 - } - for i, spam := range s.spamDetails[idx:] { - _, _ = fmt.Fprintf(w, historyLineFmt, spam.Scenario, s.spamStartTime[i].Format(timeFormat), s.spamStopTime[i].Format(timeFormat), - spam.Deep, spam.Deep, spam.Rate, int(spam.duration.Seconds())) - } - w.Flush() -} - -func (s *SpammerLog) LogSelected(lines []int, writer io.Writer) { - s.mu.Lock() - defer s.mu.Unlock() - - w := newTabWriter(writer) - _, _ = fmt.Fprintln(w, historyHeader) - for _, idx := range lines { - spam := s.spamDetails[idx] - _, _ = fmt.Fprintf(w, historyLineFmt, spam.Scenario, s.spamStartTime[idx].Format(timeFormat), s.spamStopTime[idx].Format(timeFormat), - spam.Deep, spam.Deep, spam.Rate, int(spam.duration.Seconds())) - } - w.Flush() -} - // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/interactive/menu.go b/pkg/interactive/menu.go index ad15e38..87405b7 100644 --- a/pkg/interactive/menu.go +++ b/pkg/interactive/menu.go @@ -75,7 +75,7 @@ func (p *Printer) EvilWalletStatus() { func (p *Printer) SpammerSettings() { rateUnit := "[mpm]" - if p.mode.Config.timeUnit == time.Second { + if p.mode.innerConfig.timeUnit == time.Second { rateUnit = "[mps]" } p.PrintTopLine() @@ -83,7 +83,7 @@ func (p *Printer) SpammerSettings() { p.PrintlnPoint(fmt.Sprintf("Scenario: %s", p.mode.Config.Scenario), level2) p.PrintlnPoint(fmt.Sprintf("Deep: %v, Reuse: %v", p.mode.Config.Deep, p.mode.Config.Reuse), level2) p.PrintlnPoint(fmt.Sprintf("Use rate-setter: %v", p.mode.Config.UseRateSetter), level2) - p.PrintlnPoint(fmt.Sprintf("Rate: %d%s, Duration: %d[s]", p.mode.Config.Rate, rateUnit, int(p.mode.Config.duration.Seconds())), level2) + p.PrintlnPoint(fmt.Sprintf("Rate: %d%s, Duration: %d[s]", p.mode.Config.Rate, rateUnit, int(p.mode.innerConfig.duration.Seconds())), level2) p.PrintLine() fmt.Println() } @@ -128,7 +128,7 @@ func (p *Printer) NotEnoughClientsWarning(numOfClient int) { func (p *Printer) clients() { p.Println(p.colorString("Provided clients:", "cyan"), level1) - for url := range p.mode.Config.clientURLs { + for url := range p.mode.innerConfig.clientURLs { p.PrintlnPoint(url, level2) } } @@ -175,7 +175,7 @@ func (p *Printer) CurrentSpams() { for id := range p.mode.activeSpammers { details := p.mode.spammerLog.SpamDetails(id) startTime := p.mode.spammerLog.StartTime(id) - endTime := startTime.Add(details.duration) + endTime := startTime.Add(p.mode.innerConfig.duration) timeLeft := int(time.Until(endTime).Seconds()) lines = append(lines, fmt.Sprintf("ID: %d, scenario: %s, time left: %d [s]", id, details.Scenario, timeLeft)) } diff --git a/pkg/models/config.go b/pkg/models/config.go new file mode 100644 index 0000000..76f1381 --- /dev/null +++ b/pkg/models/config.go @@ -0,0 +1,15 @@ +package models + +type Config struct { + WebAPI []string `json:"webAPI"` //nolint:tagliatelle + FaucetURL string `json:"faucetUrl"` + Rate int `json:"rate"` + Duration string `json:"duration"` + TimeUnit string `json:"timeUnit"` + Deep bool `json:"deepEnabled"` + Reuse bool `json:"reuseEnabled,omitempty"` + Scenario string `json:"scenario"` + AutoRequesting bool `json:"autoRequestingEnabled,omitempty"` + AutoRequestingAmount string `json:"autoRequestingAmount,omitempty"` + UseRateSetter bool `json:"useRateSetter,omitempty"` +} diff --git a/pkg/models/spamlog.go b/pkg/models/spamlog.go new file mode 100644 index 0000000..5d86091 --- /dev/null +++ b/pkg/models/spamlog.go @@ -0,0 +1,95 @@ +package models + +import ( + "fmt" + "io" + "text/tabwriter" + "time" + + "github.com/iotaledger/hive.go/runtime/syncutils" +) + +const ( + timeFormat = "2006/01/02 15:04:05" +) + +var ( + historyHeader = "scenario\tstart\tstop\tdeep\treuse\trate\tduration" + historyLineFmt = "%s\t%s\t%s\t%v\t%v\t%d\t%d\n" +) + +type SpammerLog struct { + spamDetails []Config + spamStartTime []time.Time + spamStopTime []time.Time + mu syncutils.Mutex +} + +func NewSpammerLog() *SpammerLog { + return &SpammerLog{ + spamDetails: make([]Config, 0), + spamStartTime: make([]time.Time, 0), + spamStopTime: make([]time.Time, 0), + } +} + +func (s *SpammerLog) SpamDetails(spamID int) *Config { + return &s.spamDetails[spamID] +} + +func (s *SpammerLog) StartTime(spamID int) time.Time { + return s.spamStartTime[spamID] +} + +func (s *SpammerLog) AddSpam(config Config) (spamID int) { + s.mu.Lock() + defer s.mu.Unlock() + + s.spamDetails = append(s.spamDetails, config) + s.spamStartTime = append(s.spamStartTime, time.Now()) + s.spamStopTime = append(s.spamStopTime, time.Time{}) + + return len(s.spamDetails) - 1 +} + +func (s *SpammerLog) SetSpamEndTime(spamID int) { + s.mu.Lock() + defer s.mu.Unlock() + + s.spamStopTime[spamID] = time.Now() +} + +func newTabWriter(writer io.Writer) *tabwriter.Writer { + return tabwriter.NewWriter(writer, 0, 0, 1, ' ', tabwriter.Debug|tabwriter.TabIndent) +} + +func (s *SpammerLog) LogHistory(lastLines int, writer io.Writer) { + s.mu.Lock() + defer s.mu.Unlock() + + w := newTabWriter(writer) + _, _ = fmt.Fprintln(w, historyHeader) + idx := len(s.spamDetails) - lastLines + 1 + if idx < 0 { + idx = 0 + } + for i, spam := range s.spamDetails[idx:] { + _, _ = fmt.Fprintf(w, historyLineFmt, spam.Scenario, s.spamStartTime[i].Format(timeFormat), s.spamStopTime[i].Format(timeFormat), + spam.Deep, spam.Deep, spam.Rate, spam.Duration) + } + w.Flush() +} + +func (s *SpammerLog) LogSelected(lines []int, writer io.Writer) { + s.mu.Lock() + defer s.mu.Unlock() + + w := newTabWriter(writer) + _, _ = fmt.Fprintln(w, historyHeader) + for _, idx := range lines { + spam := s.spamDetails[idx] + _, _ = fmt.Fprintf(w, historyLineFmt, spam.Scenario, s.spamStartTime[idx].Format(timeFormat), s.spamStopTime[idx].Format(timeFormat), + spam.Deep, spam.Deep, spam.Rate, spam.Duration) + } + w.Flush() +} diff --git a/pkg/spammer/errors.go b/pkg/spammer/errors.go index 0020f08..4f24ad0 100644 --- a/pkg/spammer/errors.go +++ b/pkg/spammer/errors.go @@ -52,6 +52,9 @@ func (e *ErrorCounter) GetTotalErrorCount() int64 { } func (e *ErrorCounter) GetErrorsSummary() string { + e.mutex.RLock() + defer e.mutex.RUnlock() + if len(e.errorsMap) == 0 { return "No errors encountered" } diff --git a/pkg/spammer/options.go b/pkg/spammer/options.go index 965968c..96a567c 100644 --- a/pkg/spammer/options.go +++ b/pkg/spammer/options.go @@ -64,19 +64,6 @@ func WithRateSetter(enable bool) Options { } } -// WithBatchesSent provides spammer with options regarding rate, time unit, and finishing spam criteria. Provide 0 to one of max parameters to skip it. -func WithBatchesSent(maxBatchesSent int) Options { - return func(s *Spammer) { - if s.SpamDetails == nil { - s.SpamDetails = &SpamDetails{ - MaxBatchesSent: maxBatchesSent, - } - } else { - s.SpamDetails.MaxBatchesSent = maxBatchesSent - } - } -} - // WithEvilWallet provides evil wallet instance, that will handle all spam logic according to provided EvilScenario. func WithEvilWallet(initWallets *evilwallet.EvilWallet) Options { return func(s *Spammer) { @@ -113,9 +100,10 @@ func WithBlowballSize(size int) Options { // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// type SpamDetails struct { - Rate int - TimeUnit time.Duration - MaxDuration time.Duration + Rate int + TimeUnit time.Duration + MaxDuration time.Duration + // calculated based on duration, 0 for infinite spamming MaxBatchesSent int BlowballSize int } diff --git a/pkg/spammer/spammer.go b/pkg/spammer/spammer.go index f87f19e..b272b41 100644 --- a/pkg/spammer/spammer.go +++ b/pkg/spammer/spammer.go @@ -26,6 +26,10 @@ const ( TypeBlowball = "bb" ) +const ( + InfiniteDuration = time.Duration(-1) +) + // region Spammer ////////////////////////////////////////////////////////////////////////////////////////////////////// type SpammingFunc func(*Spammer) @@ -147,12 +151,8 @@ func (s *Spammer) setupSpamDetails() { if s.SpamDetails.TimeUnit == 0 { s.SpamDetails.TimeUnit = time.Second } - // provided only maxBlkSent, calculating the default max for maxDuration - if s.SpamDetails.MaxDuration == 0 && s.SpamDetails.MaxBatchesSent > 0 { - s.SpamDetails.MaxDuration = time.Hour * 100 - } // provided only maxDuration, calculating the default max for maxBlkSent - if s.SpamDetails.MaxBatchesSent == 0 && s.SpamDetails.MaxDuration > 0 { + if s.SpamDetails.MaxDuration > 0 { s.SpamDetails.MaxBatchesSent = int(s.SpamDetails.MaxDuration.Seconds()/s.SpamDetails.TimeUnit.Seconds()*float64(s.SpamDetails.Rate)) + 1 } } @@ -178,7 +178,12 @@ func (s *Spammer) Spam() { s.log.Infof("Start spamming transactions with %d rate", s.SpamDetails.Rate) s.State.spamStartTime = time.Now() - timeExceeded := time.After(s.SpamDetails.MaxDuration) + + var timeExceeded <-chan time.Time + // if duration less than zero then spam infinitely + if s.SpamDetails.MaxDuration >= 0 { + timeExceeded = time.After(s.SpamDetails.MaxDuration) + } go func() { goroutineCount := atomic.NewInt32(0) @@ -212,7 +217,7 @@ func (s *Spammer) Spam() { } func (s *Spammer) CheckIfAllSent() { - if s.State.batchPrepared.Load() >= int64(s.SpamDetails.MaxBatchesSent) { + if s.SpamDetails.MaxDuration >= 0 && s.State.batchPrepared.Load() >= int64(s.SpamDetails.MaxBatchesSent) { s.log.Infof("Maximum number of blocks sent, stopping spammer...") s.done <- true } @@ -300,7 +305,10 @@ func (s *Spammer) PrepareAndPostBlock(txData *models.PayloadIssuanceData, issuer } } count := s.State.txSent.Add(1) - s.log.Debugf("Last block sent, ID: %s, txCount: %d", blockID.ToHex(), count) + //s.log.Debugf("Last block sent, ID: %s, txCount: %d", blockID.ToHex(), count) + if count%200 == 0 { + s.log.Infof("Blocks issued so far: %d, errors encountered: %d", count, s.ErrCounter.GetTotalErrorCount()) + } return blockID } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index c2214ba..b56fa3c 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "context" + "fmt" "time" "go.uber.org/atomic" @@ -15,7 +16,7 @@ import ( const ( MaxRetries = 20 - AwaitInterval = 1 * time.Second + AwaitInterval = 2 * time.Second ) // SplitBalanceEqually splits the balance equally between `splitNumber` outputs. @@ -36,7 +37,7 @@ func SplitBalanceEqually(splitNumber int, balance iotago.BaseToken) []iotago.Bas return outputBalances } -// AwaitTransactionToBeAccepted awaits for acceptance of a single transaction. +// AwaitBlockToBeConfirmed awaits for acceptance of a single transaction. func AwaitBlockToBeConfirmed(clt models.Client, blkID iotago.BlockID) error { for i := 0; i < MaxRetries; i++ { state := clt.GetBlockConfirmationState(blkID) @@ -56,9 +57,9 @@ func AwaitBlockToBeConfirmed(clt models.Client, blkID iotago.BlockID) error { // AwaitTransactionToBeAccepted awaits for acceptance of a single transaction. func AwaitTransactionToBeAccepted(clt models.Client, txID iotago.TransactionID, txLeft *atomic.Int64) error { for i := 0; i < MaxRetries; i++ { - resp, err := clt.GetBlockStateFromTransaction(txID) + resp, _ := clt.GetBlockStateFromTransaction(txID) if resp == nil { - UtilsLogger.Debugf("Block state API error: %v", err) + time.Sleep(AwaitInterval) continue } @@ -139,3 +140,20 @@ func AwaitOutputToBeAccepted(clt models.Client, outputID iotago.OutputID) bool { return false } + +func PrintTransaction(tx *iotago.SignedTransaction) string { + txDetails := "" + txDetails += fmt.Sprintf("Transaction ID; %s, slotCreation: %d\n", lo.PanicOnErr(tx.ID()).ToHex(), tx.Transaction.CreationSlot) + for index, out := range tx.Transaction.Outputs { + txDetails += fmt.Sprintf("Output index: %d, base token: %d, stored mana: %d\n", index, out.BaseTokenAmount(), out.StoredMana()) + } + txDetails += fmt.Sprintln("Allotments:") + for _, allotment := range tx.Transaction.Allotments { + txDetails += fmt.Sprintf("AllotmentID: %s, value: %d\n", allotment.AccountID, allotment.Mana) + } + for _, allotment := range tx.Transaction.TransactionEssence.Allotments { + txDetails += fmt.Sprintf("al 2 AllotmentID: %s, value: %d\n", allotment.AccountID, allotment.Mana) + } + + return txDetails +} diff --git a/programs/dispatcher.go b/programs/dispatcher.go new file mode 100644 index 0000000..126ff7c --- /dev/null +++ b/programs/dispatcher.go @@ -0,0 +1,34 @@ +package programs + +import ( + "github.com/iotaledger/evil-tools/pkg/accountwallet" + "github.com/iotaledger/evil-tools/pkg/models" +) + +type Runner struct { + spamDetails *models.Config + + finished chan bool +} + +type Dispatcher struct { + activeSpammers []*Runner + accWallet *accountwallet.AccountWallet +} + +func NewDispatcher(accWallet *accountwallet.AccountWallet) *Dispatcher { + return &Dispatcher{ + accWallet: accWallet, + } +} + +func (d *Dispatcher) RunSpam(params *CustomSpamParams) { + // todo custom spam should return a spammer instance, and the process should run in the background + // or we could inject channel to be able to stop the spammer + CustomSpam(params, d.accWallet) + + d.activeSpammers = append(d.activeSpammers, &Runner{ + finished: make(chan bool), + spamDetails: ConfigFromCustomSpamParams(params), + }) +} diff --git a/programs/params.go b/programs/params.go index 70373df..d624c5c 100644 --- a/programs/params.go +++ b/programs/params.go @@ -4,21 +4,35 @@ import ( "time" "github.com/iotaledger/evil-tools/pkg/evilwallet" + "github.com/iotaledger/evil-tools/pkg/models" ) type CustomSpamParams struct { ClientURLs []string FaucetURL string - SpamTypes []string - Rates []int - Durations []time.Duration - BlkToBeSent []int + SpamType string + Rate int + Duration time.Duration TimeUnit time.Duration DelayBetweenConflicts time.Duration NSpend int Scenario evilwallet.EvilBatch + ScenarioName string DeepSpam bool EnableRateSetter bool AccountAlias string BlowballSize int } + +func ConfigFromCustomSpamParams(params *CustomSpamParams) *models.Config { + return &models.Config{ + WebAPI: params.ClientURLs, + FaucetURL: "http://localhost:8088", + Rate: params.Rate, + Duration: params.Duration.String(), + TimeUnit: params.TimeUnit.String(), + Deep: params.DeepSpam, + Reuse: false, + Scenario: params.ScenarioName, + } +} diff --git a/programs/spammers.go b/programs/spammers.go index 3596f01..946ccd3 100644 --- a/programs/spammers.go +++ b/programs/spammers.go @@ -1,7 +1,6 @@ package programs import ( - "fmt" "sync" "time" @@ -13,87 +12,132 @@ import ( var log = utils.NewLogger("customSpam") -func CustomSpam(params *CustomSpamParams, accWallet *accountwallet.AccountWallet) { - w := evilwallet.NewEvilWallet(evilwallet.WithClients(params.ClientURLs...), evilwallet.WithAccountsWallet(accWallet)) - wg := sync.WaitGroup{} +func requestFaucetFunds(params *CustomSpamParams, w *evilwallet.EvilWallet) <-chan bool { + if params.SpamType == spammer.TypeBlock { + return nil + } + var numOfBigWallets = evilwallet.BigFaucetWalletsAtOnce + if params.Duration != spammer.InfiniteDuration { + numOfBigWallets = spammer.BigWalletsNeeded(params.Rate, params.TimeUnit, params.Duration) + if numOfBigWallets > evilwallet.MaxBigWalletsCreatedAtOnce { + numOfBigWallets = evilwallet.MaxBigWalletsCreatedAtOnce + log.Warnf("Reached maximum number of big wallets created at once: %d, use infinite spam instead", evilwallet.MaxBigWalletsCreatedAtOnce) + } + } + success := w.RequestFreshBigFaucetWallets(numOfBigWallets) + if !success { + log.Errorf("Failed to request faucet wallet") + return nil + } + if params.Duration != spammer.InfiniteDuration { + unspentOutputsLeft := w.UnspentOutputsLeft(evilwallet.Fresh) + log.Debugf("Prepared %d unspent outputs for spamming.", unspentOutputsLeft) - for i, sType := range params.SpamTypes { - log.Infof("Start spamming with rate: %d, time unit: %s, and spamming type: %s.", params.Rates[i], params.TimeUnit.String(), sType) + return nil + } + var requestingChan = make(<-chan bool) + log.Debugf("Start requesting faucet funds infinitely...") + go requestInfinitely(w, requestingChan) - if sType != spammer.TypeBlock && sType != spammer.TypeBlowball { - numOfBigWallets := spammer.BigWalletsNeeded(params.Rates[i], params.TimeUnit, params.Durations[i]) - fmt.Println("numOfBigWallets: ", numOfBigWallets) - success := w.RequestFreshBigFaucetWallets(numOfBigWallets) - if !success { - log.Errorf("Failed to request faucet wallet") + return requestingChan +} - return - } +func requestInfinitely(w *evilwallet.EvilWallet, done <-chan bool) { + for { + select { + case <-done: + log.Debug("Shutdown signal. Stopping requesting faucet funds for spam: %d", 0) - unspentOutputsLeft := w.UnspentOutputsLeft(evilwallet.Fresh) - log.Debugf("Prepared %d unspent outputs for spamming.", unspentOutputsLeft) - } + return + + case <-time.After(evilwallet.CheckFundsLeftInterval): + outputsLeft := w.UnspentOutputsLeft(evilwallet.Fresh) + // keep requesting over and over until we have at least deposit + if outputsLeft < evilwallet.BigFaucetWalletDeposit*evilwallet.FaucetRequestSplitNumber*evilwallet.FaucetRequestSplitNumber { + log.Debugf("Requesting new faucet funds, outputs left: %d", outputsLeft) + success := w.RequestFreshBigFaucetWallets(evilwallet.BigFaucetWalletsAtOnce) + if !success { + log.Errorf("Failed to request faucet wallet: %s, stopping next requests..., stopping spammer") - switch sType { - case spammer.TypeBlock: - wg.Add(1) - go func(i int) { - defer wg.Done() - s := SpamBlocks(w, params.Rates[i], params.TimeUnit, params.Durations[i], params.BlkToBeSent[i], params.EnableRateSetter, params.AccountAlias) - if s == nil { - return - } - s.Spam() - }(i) - case spammer.TypeBlowball: - wg.Add(1) - go func(i int) { - defer wg.Done() - - s := SpamBlowball(w, params.Rates[i], params.TimeUnit, params.Durations[i], params.BlowballSize, params.EnableRateSetter, params.AccountAlias) - if s == nil { - return - } - s.Spam() - }(i) - case spammer.TypeTx: - wg.Add(1) - go func(i int) { - defer wg.Done() - SpamTransaction(w, params.Rates[i], params.TimeUnit, params.Durations[i], params.DeepSpam, params.EnableRateSetter, params.AccountAlias) - }(i) - case spammer.TypeDs: - wg.Add(1) - go func(i int) { - defer wg.Done() - SpamDoubleSpends(w, params.Rates[i], params.NSpend, params.TimeUnit, params.Durations[i], params.DelayBetweenConflicts, params.DeepSpam, params.EnableRateSetter, params.AccountAlias) - }(i) - case spammer.TypeCustom: - wg.Add(1) - go func(i int) { - defer wg.Done() - s := SpamNestedConflicts(w, params.Rates[i], params.TimeUnit, params.Durations[i], params.Scenario, params.DeepSpam, false, params.EnableRateSetter, params.AccountAlias) - if s == nil { - return - } - s.Spam() - }(i) - case spammer.TypeAccounts: - wg.Add(1) - go func(i int) { - defer wg.Done() - - s := SpamAccounts(w, params.Rates[i], params.TimeUnit, params.Durations[i], params.EnableRateSetter, params.AccountAlias) - if s == nil { return } - s.Spam() - }(i) - default: - log.Warn("Spamming type not recognized. Try one of following: tx, ds, blk, custom, commitments") + log.Debugf("Requesting finished, currently available: %d unspent outputs for spamming.", w.UnspentOutputsLeft(evilwallet.Fresh)) + } } } +} + +func CustomSpam(params *CustomSpamParams, accWallet *accountwallet.AccountWallet) { + w := evilwallet.NewEvilWallet(evilwallet.WithClients(params.ClientURLs...), evilwallet.WithAccountsWallet(accWallet)) + wg := sync.WaitGroup{} + + log.Infof("Start spamming with rate: %d, time unit: %s, and spamming type: %s.", params.Rate, params.TimeUnit.String(), params.SpamType) + + // TODO here we can shutdown requesting when we will have evil-tools running in the background. + _ = requestFaucetFunds(params, w) + + sType := params.SpamType + + switch sType { + case spammer.TypeBlock: + wg.Add(1) + go func() { + defer wg.Done() + s := SpamBlocks(w, params.Rate, params.TimeUnit, params.Duration, params.EnableRateSetter, params.AccountAlias) + if s == nil { + return + } + s.Spam() + }() + case spammer.TypeBlowball: + wg.Add(1) + go func() { + defer wg.Done() + + s := SpamBlowball(w, params.Rate, params.TimeUnit, params.Duration, params.BlowballSize, params.EnableRateSetter, params.AccountAlias) + if s == nil { + return + } + s.Spam() + }() + case spammer.TypeTx: + wg.Add(1) + go func() { + defer wg.Done() + SpamTransaction(w, params.Rate, params.TimeUnit, params.Duration, params.DeepSpam, params.EnableRateSetter, params.AccountAlias) + }() + case spammer.TypeDs: + wg.Add(1) + go func() { + defer wg.Done() + SpamDoubleSpends(w, params.Rate, params.NSpend, params.TimeUnit, params.Duration, params.DelayBetweenConflicts, params.DeepSpam, params.EnableRateSetter, params.AccountAlias) + }() + case spammer.TypeCustom: + wg.Add(1) + go func() { + defer wg.Done() + s := SpamNestedConflicts(w, params.Rate, params.TimeUnit, params.Duration, params.Scenario, params.DeepSpam, false, params.EnableRateSetter, params.AccountAlias) + if s == nil { + return + } + s.Spam() + }() + case spammer.TypeAccounts: + wg.Add(1) + go func() { + defer wg.Done() + + s := SpamAccounts(w, params.Rate, params.TimeUnit, params.Duration, params.EnableRateSetter, params.AccountAlias) + if s == nil { + return + } + s.Spam() + }() + + default: + log.Warn("Spamming type not recognized. Try one of following: tx, ds, blk, custom, commitments") + } wg.Wait() log.Info("Basic spamming finished!") @@ -194,7 +238,7 @@ func SpamNestedConflicts(w *evilwallet.EvilWallet, rate int, timeUnit, duration return spammer.NewSpammer(options...) } -func SpamBlocks(w *evilwallet.EvilWallet, rate int, timeUnit, duration time.Duration, numBlkToSend int, enableRateSetter bool, accountAlias string) *spammer.Spammer { +func SpamBlocks(w *evilwallet.EvilWallet, rate int, timeUnit, duration time.Duration, enableRateSetter bool, accountAlias string) *spammer.Spammer { if w.NumOfClient() < 1 { log.Infof("Warning: At least one client is needed to spam.") } @@ -202,7 +246,6 @@ func SpamBlocks(w *evilwallet.EvilWallet, rate int, timeUnit, duration time.Dura options := []spammer.Options{ spammer.WithSpamRate(rate, timeUnit), spammer.WithSpamDuration(duration), - spammer.WithBatchesSent(numBlkToSend), spammer.WithRateSetter(enableRateSetter), spammer.WithEvilWallet(w), spammer.WithSpammingFunc(spammer.DataSpammingFunction),