diff --git a/accountwallet/faucet.go b/accountwallet/faucet.go index 6b64e8e..10b7cd3 100644 --- a/accountwallet/faucet.go +++ b/accountwallet/faucet.go @@ -68,8 +68,8 @@ func (a *AccountWallet) RequestFaucetFunds(clt models.Client, receiveAddr iotago }, nil } -func (a *AccountWallet) PostWithBlock(clt models.Client, payload iotago.Payload, issuer blockhandler.Account, congestionResp *apimodels.CongestionResponse, issuerResp *apimodels.IssuanceBlockHeaderResponse, version iotago.Version) (iotago.BlockID, error) { - signedBlock, err := a.CreateBlock(payload, issuer, congestionResp, issuerResp, version) +func (a *AccountWallet) PostWithBlock(clt models.Client, payload iotago.Payload, issuer blockhandler.Account, congestionResp *apimodels.CongestionResponse, issuerResp *apimodels.IssuanceBlockHeaderResponse, version iotago.Version, strongParents ...iotago.BlockID) (iotago.BlockID, error) { + signedBlock, err := a.CreateBlock(payload, issuer, congestionResp, issuerResp, version, strongParents...) if err != nil { log.Errorf("failed to create block: %s", err) @@ -86,10 +86,17 @@ func (a *AccountWallet) PostWithBlock(clt models.Client, payload iotago.Payload, return blockID, nil } -func (a *AccountWallet) CreateBlock(payload iotago.Payload, issuer blockhandler.Account, congestionResp *apimodels.CongestionResponse, issuerResp *apimodels.IssuanceBlockHeaderResponse, version iotago.Version) (*iotago.ProtocolBlock, error) { +func (a *AccountWallet) CreateBlock(payload iotago.Payload, issuer blockhandler.Account, congestionResp *apimodels.CongestionResponse, issuerResp *apimodels.IssuanceBlockHeaderResponse, version iotago.Version, strongParents ...iotago.BlockID) (*iotago.ProtocolBlock, error) { issuingTime := time.Now() issuingSlot := a.client.LatestAPI().TimeProvider().SlotFromTime(issuingTime) apiForSlot := a.client.APIForSlot(issuingSlot) + if congestionResp == nil { + var err error + congestionResp, err = a.client.GetCongestion(issuer.ID()) + if err != nil { + return nil, ierrors.Wrap(err, "failed to get congestion data") + } + } blockBuilder := builder.NewBasicBlockBuilder(apiForSlot) @@ -102,7 +109,7 @@ func (a *AccountWallet) CreateBlock(payload iotago.Payload, issuer blockhandler. blockBuilder.SlotCommitmentID(commitmentID) blockBuilder.LatestFinalizedSlot(issuerResp.LatestFinalizedSlot) blockBuilder.IssuingTime(time.Now()) - blockBuilder.StrongParents(issuerResp.StrongParents) + blockBuilder.StrongParents(append(issuerResp.StrongParents, strongParents...)) blockBuilder.WeakParents(issuerResp.WeakParents) blockBuilder.ShallowLikeParents(issuerResp.ShallowLikeParents) diff --git a/config.go b/config.go index f8d412f..5e117e4 100644 --- a/config.go +++ b/config.go @@ -31,6 +31,7 @@ var ( DeepSpam: false, EnableRateSetter: false, AccountAlias: accountwallet.FaucetAccountAlias, + BlowballSize: 30, } accountsSubcommandsFlags []accountwallet.AccountSubcommands diff --git a/evilwallet/evilwallet.go b/evilwallet/evilwallet.go index 5935b8e..3e06003 100644 --- a/evilwallet/evilwallet.go +++ b/evilwallet/evilwallet.go @@ -117,13 +117,40 @@ func (e *EvilWallet) GetAccount(alias string) (blockhandler.Account, error) { return account.Account, nil } +func (e *EvilWallet) CreateBlock(clt models.Client, payload iotago.Payload, congestionResp *apimodels.CongestionResponse, issuer blockhandler.Account, strongParents ...iotago.BlockID) (*iotago.ProtocolBlock, error) { + var congestionSlot iotago.SlotIndex + version := clt.CommittedAPI().Version() + if congestionResp != nil { + congestionSlot = congestionResp.Slot + version = clt.APIForSlot(congestionSlot).Version() + } + + issuerResp, err := clt.GetBlockIssuance(congestionSlot) + if err != nil { + return nil, ierrors.Wrap(err, "failed to get block issuance data") + } + + block, err := e.accWallet.CreateBlock(payload, issuer, congestionResp, issuerResp, version, strongParents...) + if err != nil { + return nil, err + } + + return block, nil +} + func (e *EvilWallet) PrepareAndPostBlock(clt models.Client, payload iotago.Payload, congestionResp *apimodels.CongestionResponse, issuer blockhandler.Account) (iotago.BlockID, error) { - issuerResp, err := clt.GetBlockIssuance(congestionResp.Slot) + var congestionSlot iotago.SlotIndex + version := clt.CommittedAPI().Version() + if congestionResp != nil { + congestionSlot = congestionResp.Slot + version = clt.APIForSlot(congestionSlot).Version() + } + + issuerResp, err := clt.GetBlockIssuance(congestionSlot) if err != nil { return iotago.EmptyBlockID, ierrors.Wrap(err, "failed to get block issuance data") } - version := clt.APIForSlot(congestionResp.Slot).Version() blockID, err := e.accWallet.PostWithBlock(clt, payload, issuer, congestionResp, issuerResp, version) if err != nil { return iotago.EmptyBlockID, err diff --git a/interactive/interactive.go b/interactive/interactive.go index eff586c..2707eb3 100644 --- a/interactive/interactive.go +++ b/interactive/interactive.go @@ -139,7 +139,7 @@ const ( ) var ( - scenarios = []string{spammer.TypeBlock, spammer.TypeTx, spammer.TypeDs, "conflict-circle", "guava", "orange", "mango", "pear", "lemon", "banana", "kiwi", "peace"} + scenarios = []string{spammer.TypeBlock, spammer.TypeTx, spammer.TypeDs, spammer.TypeBlowball, "conflict-circle", "guava", "orange", "mango", "pear", "lemon", "banana", "kiwi", "peace"} confirms = []string{AnswerEnable, AnswerDisable} outputNumbers = []string{"100", "1000", "5000", "cancel"} timeUnits = []string{mpm, mps} diff --git a/main.go b/main.go index a3dbf7a..9f0f403 100644 --- a/main.go +++ b/main.go @@ -68,7 +68,7 @@ func main() { case ScriptAccounts: accountsSubcommands(accWallet, accountsSubcommandsFlags) default: - log.Warnf("Unknown parameter for script, possible values: interactive, basic, accounts, quick") + log.Warnf("Unknown parameter for script, possible values: interactive, spammer, accounts") } } diff --git a/models/connector.go b/models/connector.go index 913e24a..de3e7ca 100644 --- a/models/connector.go +++ b/models/connector.go @@ -180,6 +180,8 @@ type Client interface { PostBlock(block *iotago.ProtocolBlock) (iotago.BlockID, error) // PostData sends the given data (payload) by creating a block in the backend. PostData(data []byte) (blkID string, err error) + // GetBlockConfirmationState returns the AcceptanceState of a given block ID. + GetBlockConfirmationState(blkID iotago.BlockID) string // GetBlockState returns the AcceptanceState of a given transaction ID. GetBlockState(txID iotago.TransactionID) (resp *apimodels.BlockMetadataResponse, err error) // GetOutput gets the output of a given outputID. @@ -296,6 +298,16 @@ func (c *WebClient) GetOutput(outputID iotago.OutputID) iotago.Output { return res } +// GetBlockConfirmationState returns the AcceptanceState of a given block ID. +func (c *WebClient) GetBlockConfirmationState(blkID iotago.BlockID) string { + resp, err := c.client.BlockMetadataByBlockID(context.Background(), blkID) + if err != nil { + return "" + } + + return resp.BlockState +} + // GetBlockState returns the AcceptanceState of a given transaction ID. func (c *WebClient) GetBlockState(txID iotago.TransactionID) (*apimodels.BlockMetadataResponse, error) { return c.client.TransactionIncludedBlockMetadata(context.Background(), txID) diff --git a/parse.go b/parse.go index c1a5d36..49856f2 100644 --- a/parse.go +++ b/parse.go @@ -57,7 +57,7 @@ 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,"+ - " 'tx' - transaction, 'ds' - double spends spammers, 'nds' - n-spends spammer, 'custom' - spams with provided scenario") + " '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.") diff --git a/programs/params.go b/programs/params.go index a89a55f..ae39e2e 100644 --- a/programs/params.go +++ b/programs/params.go @@ -19,4 +19,5 @@ type CustomSpamParams struct { DeepSpam bool EnableRateSetter bool AccountAlias string + BlowballSize int } diff --git a/programs/spammers.go b/programs/spammers.go index 0d2d6a1..1a619de 100644 --- a/programs/spammers.go +++ b/programs/spammers.go @@ -20,17 +20,19 @@ func CustomSpam(params *CustomSpamParams, accWallet *accountwallet.AccountWallet 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) - 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") + 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 - } + return + } - unspentOutputsLeft := w.UnspentOutputsLeft(evilwallet.Fresh) - log.Debugf("Prepared %d unspent outputs for spamming.", unspentOutputsLeft) + unspentOutputsLeft := w.UnspentOutputsLeft(evilwallet.Fresh) + log.Debugf("Prepared %d unspent outputs for spamming.", unspentOutputsLeft) + } switch sType { case spammer.TypeBlock: @@ -43,6 +45,17 @@ func CustomSpam(params *CustomSpamParams, accWallet *accountwallet.AccountWallet } 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) { @@ -222,3 +235,26 @@ func SpamAccounts(w *evilwallet.EvilWallet, rate int, timeUnit, duration time.Du return spammer.NewSpammer(options...) } + +func SpamBlowball(w *evilwallet.EvilWallet, rate int, timeUnit, duration time.Duration, blowballSize int, enableRateSetter bool, accountAlias string) *spammer.Spammer { + if w.NumOfClient() < 1 { + log.Infof("Warning: At least one client is needed to spam.") + } + + // blowball spammer needs at least 40 seconds to finish + if duration < 40*time.Second { + duration = 40 * time.Second + } + + options := []spammer.Options{ + spammer.WithSpamRate(rate, timeUnit), + spammer.WithSpamDuration(duration), + spammer.WithBlowballSize(blowballSize), + spammer.WithRateSetter(enableRateSetter), + spammer.WithEvilWallet(w), + spammer.WithSpammingFunc(spammer.BlowballSpammingFunction), + spammer.WithAccountAlias(accountAlias), + } + + return spammer.NewSpammer(options...) +} diff --git a/spammer/options.go b/spammer/options.go index 0d079f4..2621b50 100644 --- a/spammer/options.go +++ b/spammer/options.go @@ -97,6 +97,19 @@ func WithTimeDelayForDoubleSpend(timeDelay time.Duration) Options { } } +// WithBlowballSize provides spammer with options regarding blowball size. +func WithBlowballSize(size int) Options { + return func(s *Spammer) { + if s.SpamDetails == nil { + s.SpamDetails = &SpamDetails{ + BlowballSize: size, + } + } else { + s.SpamDetails.BlowballSize = size + } + } +} + // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// type SpamDetails struct { @@ -104,4 +117,5 @@ type SpamDetails struct { TimeUnit time.Duration MaxDuration time.Duration MaxBatchesSent int + BlowballSize int } diff --git a/spammer/spammer.go b/spammer/spammer.go index e62f9ae..2eda55d 100644 --- a/spammer/spammer.go +++ b/spammer/spammer.go @@ -23,6 +23,7 @@ const ( TypeDs = "ds" TypeCustom = "custom" TypeAccounts = "accounts" + TypeBlowball = "bb" ) // region Spammer ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -226,30 +227,55 @@ func (s *Spammer) StopSpamming() { s.shutdown <- types.Void } -func (s *Spammer) PrepareAndPostBlock(txData *models.PayloadIssuanceData, issuerAlias string, clt models.Client) { +func (s *Spammer) PrepareBlock(txData *models.PayloadIssuanceData, issuerAlias string, clt models.Client, strongParents ...iotago.BlockID) *iotago.ProtocolBlock { if txData.Payload == nil { s.log.Debug(ErrPayloadIsNil) s.ErrCounter.CountError(ErrPayloadIsNil) - return + return nil } issuerAccount, err := s.EvilWallet.GetAccount(issuerAlias) if err != nil { s.log.Debug(ierrors.Wrapf(ErrFailGetAccount, err.Error())) s.ErrCounter.CountError(ierrors.Wrapf(ErrFailGetAccount, err.Error())) - return + return nil + } + block, err := s.EvilWallet.CreateBlock(clt, txData.Payload, txData.CongestionResponse, issuerAccount, strongParents...) + if err != nil { + s.log.Debug(ierrors.Wrapf(ErrFailPostBlock, err.Error())) + s.ErrCounter.CountError(ierrors.Wrapf(ErrFailPostBlock, err.Error())) + + return nil + } + + return block +} + +func (s *Spammer) PrepareAndPostBlock(txData *models.PayloadIssuanceData, issuerAlias string, clt models.Client) iotago.BlockID { + if txData.Payload == nil { + s.log.Debug(ErrPayloadIsNil) + s.ErrCounter.CountError(ErrPayloadIsNil) + + return iotago.EmptyBlockID + } + issuerAccount, err := s.EvilWallet.GetAccount(issuerAlias) + if err != nil { + s.log.Debug(ierrors.Wrapf(ErrFailGetAccount, err.Error())) + s.ErrCounter.CountError(ierrors.Wrapf(ErrFailGetAccount, err.Error())) + + return iotago.EmptyBlockID } blockID, err := s.EvilWallet.PrepareAndPostBlock(clt, txData.Payload, txData.CongestionResponse, issuerAccount) if err != nil { s.log.Debug(ierrors.Wrapf(ErrFailPostBlock, err.Error())) s.ErrCounter.CountError(ierrors.Wrapf(ErrFailPostBlock, err.Error())) - return + return iotago.EmptyBlockID } if txData.Payload.PayloadType() != iotago.PayloadSignedTransaction { - return + return blockID } //nolint:all,forcetypassert @@ -260,7 +286,7 @@ func (s *Spammer) PrepareAndPostBlock(txData *models.PayloadIssuanceData, issuer s.log.Debug(ierrors.Wrapf(ErrTransactionInvalid, err.Error())) s.ErrCounter.CountError(ierrors.Wrapf(ErrTransactionInvalid, err.Error())) - return + return blockID } // reuse outputs @@ -275,6 +301,8 @@ 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) + + return blockID } // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/spammer/spamming_functions.go b/spammer/spamming_functions.go index 4062c7e..8e03f44 100644 --- a/spammer/spamming_functions.go +++ b/spammer/spamming_functions.go @@ -7,6 +7,7 @@ import ( "github.com/iotaledger/evil-tools/models" "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/lo" iotago "github.com/iotaledger/iota.go/v4" ) @@ -82,3 +83,97 @@ func AccountSpammingFunction(s *Spammer) { s.EvilWallet.ClearAliases(aliases) s.CheckIfAllSent() } + +func BlowballSpammingFunction(s *Spammer) { + clt := s.Clients.GetClient() + // sleep randomly to avoid issuing blocks in different goroutines at once + //nolint:gosec + time.Sleep(time.Duration(rand.Float64()*20) * time.Millisecond) + + centerID, err := createBlowBallCenter(s) + if err != nil { + s.log.Errorf("failed to performe blowball attack", err) + return + } + s.log.Infof("blowball center ID: %s", centerID.ToHex()) + + // wait for the center block to be an old confirmed block + s.log.Infof("wait blowball center to get old...") + time.Sleep(30 * time.Second) + + blowballs := createBlowBall(centerID, s) + + wg := sync.WaitGroup{} + for _, blk := range blowballs { + // send transactions in parallel + wg.Add(1) + go func(clt models.Client, blk *iotago.ProtocolBlock) { + defer wg.Done() + + // sleep randomly to avoid issuing blocks in different goroutines at once + //nolint:gosec + time.Sleep(time.Duration(rand.Float64()*100) * time.Millisecond) + + id, err := clt.PostBlock(blk) + if err != nil { + s.log.Error("ereror to send blowball blocks") + return + } + s.log.Infof("blowball sent, ID: %s", id.ToHex()) + }(clt, blk) + } + wg.Wait() + + s.State.batchPrepared.Add(1) + s.CheckIfAllSent() +} + +func createBlowBallCenter(s *Spammer) (iotago.BlockID, error) { + clt := s.Clients.GetClient() + + centerID := s.PrepareAndPostBlock(&models.PayloadIssuanceData{ + Payload: &iotago.TaggedData{ + Tag: []byte("DS"), + }, + }, s.IssuerAlias, clt) + + timer := time.NewTimer(10 * time.Second) + defer timer.Stop() + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ticker.C: + state := clt.GetBlockConfirmationState(centerID) + if state == "confirmed" { + return centerID, nil + } + case <-timer.C: + return iotago.EmptyBlockID, ierrors.Errorf("failed to confirm center block") + } + } +} + +func createBlowBall(center iotago.BlockID, s *Spammer) []*iotago.ProtocolBlock { + blowBallBlocks := make([]*iotago.ProtocolBlock, 0) + // default to 30, if blowball size is not set + size := lo.Max(s.SpamDetails.BlowballSize, 30) + + for i := 0; i < size; i++ { + blk := createSideBlock(center, s) + blowBallBlocks = append(blowBallBlocks, blk) + } + + return blowBallBlocks +} + +func createSideBlock(parent iotago.BlockID, s *Spammer) *iotago.ProtocolBlock { + // create a new message + clt := s.Clients.GetClient() + + return s.PrepareBlock(&models.PayloadIssuanceData{ + Payload: &iotago.TaggedData{ + Tag: []byte("DS"), + }, + }, s.IssuerAlias, clt, parent) +}