diff --git a/components/restapi/core/accounts.go b/components/restapi/core/accounts.go index 6fd2afd87..7d1e33fb4 100644 --- a/components/restapi/core/accounts.go +++ b/components/restapi/core/accounts.go @@ -36,7 +36,7 @@ func congestionForAccountID(c echo.Context) (*apimodels.CongestionResponse, erro if err != nil { rmcSlot = 0 } - rmc, err := deps.Protocol.CandidateEngineInstance().Ledger.RMCManager().RMC(rmcSlot) + rmc, err := deps.Protocol.MainEngineInstance().Ledger.RMCManager().RMC(rmcSlot) if err != nil { return nil, ierrors.Wrapf(err, "failed to get RMC for slot: %d", rmcSlot) } @@ -141,7 +141,7 @@ func rewardsByOutputID(c echo.Context) (*apimodels.ManaRewardsResponse, error) { utxoOutput, err := deps.Protocol.MainEngineInstance().Ledger.Output(outputID) if err != nil { - return nil, ierrors.Wrapf(err, "failed to get output %s from ledger", outputID) + return nil, ierrors.Wrapf(err, "failed to get output %s from ledger", outputID.ToHex()) } var reward iotago.Mana diff --git a/components/restapi/core/transaction.go b/components/restapi/core/transaction.go index 0ee22c7d4..44575426a 100644 --- a/components/restapi/core/transaction.go +++ b/components/restapi/core/transaction.go @@ -22,15 +22,18 @@ func blockIDByTransactionID(c echo.Context) (iotago.BlockID, error) { func blockIDFromTransactionID(transactionID iotago.TransactionID) (iotago.BlockID, error) { // Get the first output of that transaction (using index 0) - outputID := iotago.OutputID{} - copy(outputID[:], transactionID[:]) + outputID := iotago.OutputIDFromTransactionIDAndIndex(transactionID, 0) - output, err := deps.Protocol.MainEngineInstance().Ledger.Output(outputID) + output, spent, err := deps.Protocol.MainEngineInstance().Ledger.OutputOrSpent(outputID) if err != nil { - return iotago.EmptyBlockID, ierrors.Wrapf(err, "failed to get output: %s", outputID.String()) + return iotago.EmptyBlockID, ierrors.Wrapf(err, "failed to get output: %s", outputID.ToHex()) } - return output.BlockID(), nil + if output != nil { + return output.BlockID(), nil + } + + return spent.BlockID(), nil } func blockByTransactionID(c echo.Context) (*model.Block, error) { diff --git a/go.mod b/go.mod index 06abbdcd7..76a675061 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/iotaledger/hive.go/stringify v0.0.0-20231010133617-cdbd5387e2af github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231011161248-cf0bd6e08811 github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231011154428-257141868dad - github.com/iotaledger/iota.go/v4 v4.0.0-20231011161154-7004432004e1 + github.com/iotaledger/iota.go/v4 v4.0.0-20231013092100-ad2a52b5ac9a github.com/labstack/echo/v4 v4.11.2 github.com/labstack/gommon v0.4.0 github.com/libp2p/go-libp2p v0.30.0 diff --git a/go.sum b/go.sum index 864310299..42d166a1e 100644 --- a/go.sum +++ b/go.sum @@ -311,6 +311,8 @@ github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231011154428-257141868dad h1:TRM9Ek github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231011154428-257141868dad/go.mod h1:plZ0+8yLdDWHedj3SfHUwQtIETD+lcS6M1iEAxcjzJ4= github.com/iotaledger/iota.go/v4 v4.0.0-20231011161154-7004432004e1 h1:mz5E00q1U/LDiUi/wVAbwdhGNKX0dNThaO99Fsyjkgs= github.com/iotaledger/iota.go/v4 v4.0.0-20231011161154-7004432004e1/go.mod h1:XmgOVYZ7805zVEYPwhvqBDVa7XieXRgPrCEGZW35W8k= +github.com/iotaledger/iota.go/v4 v4.0.0-20231013092100-ad2a52b5ac9a h1:S/n3ZTjnnl0IIMCx+S0pu5CZNArO6Z+omiXt6dDmPK4= +github.com/iotaledger/iota.go/v4 v4.0.0-20231013092100-ad2a52b5ac9a/go.mod h1:XmgOVYZ7805zVEYPwhvqBDVa7XieXRgPrCEGZW35W8k= github.com/ipfs/boxo v0.10.0 h1:tdDAxq8jrsbRkYoF+5Rcqyeb91hgWe2hp7iLu7ORZLY= github.com/ipfs/boxo v0.10.0/go.mod h1:Fg+BnfxZ0RPzR0nOodzdIq3A7KgoWAOWsEIImrIQdBM= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= diff --git a/tools/evil-spammer/.gitignore b/tools/evil-spammer/.gitignore index 97be5e00b..301d5d62d 100644 --- a/tools/evil-spammer/.gitignore +++ b/tools/evil-spammer/.gitignore @@ -1,2 +1,3 @@ *.log -*.json \ No newline at end of file +*.json +*.dat \ No newline at end of file diff --git a/tools/evil-spammer/accountwallet/commands.go b/tools/evil-spammer/accountwallet/commands.go new file mode 100644 index 000000000..37a7843cb --- /dev/null +++ b/tools/evil-spammer/accountwallet/commands.go @@ -0,0 +1,40 @@ +package accountwallet + +import ( + "fmt" + + "github.com/iotaledger/hive.go/ierrors" + iotago "github.com/iotaledger/iota.go/v4" +) + +func (a *AccountWallet) CreateAccount(params *CreateAccountParams) (iotago.AccountID, error) { + implicitAccountOutput, privateKey, err := a.getFunds(params.Amount, iotago.AddressImplicitAccountCreation) + if err != nil { + return iotago.EmptyAccountID, ierrors.Wrap(err, "Failed to create account") + } + + accountID := a.registerAccount(params.Alias, implicitAccountOutput.OutputID, a.latestUsedIndex, privateKey) + + fmt.Printf("Created account %s with %d tokens\n", accountID.ToHex(), params.Amount) + + return accountID, nil +} + +func (a *AccountWallet) DestroyAccount(params *DestroyAccountParams) error { + return a.destroyAccount(params.AccountAlias) +} + +func (a *AccountWallet) ListAccount() error { + fmt.Printf("%-10s \t%-33s\n\n", "Alias", "AccountID") + for _, accData := range a.accountsAliases { + fmt.Printf("%-10s \t", accData.Alias) + fmt.Printf("%-33s ", accData.Account.ID().ToHex()) + fmt.Printf("\n") + } + + return nil +} + +func (a *AccountWallet) AllotToAccount(params *AllotAccountParams) error { + return nil +} diff --git a/tools/evil-spammer/accountwallet/config.go b/tools/evil-spammer/accountwallet/config.go new file mode 100644 index 000000000..a363cb8ad --- /dev/null +++ b/tools/evil-spammer/accountwallet/config.go @@ -0,0 +1,219 @@ +package accountwallet + +import ( + "encoding/json" + "os" + + "github.com/iotaledger/hive.go/ds/types" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" +) + +// commands + +type AccountOperation int + +const ( + OperationCreateAccount AccountOperation = iota + OperationConvertAccount + OperationDestroyAccound + OperationAllotAccount + OperationDelegateAccount + OperationStakeAccount + OperationListAccounts + OperationUpdateAccount + + CmdNameCreateAccount = "create" + CmdNameConvertAccount = "convert" + CmdNameDestroyAccount = "destroy" + CmdNameAllotAccount = "allot" + CmdNameDelegateAccount = "delegate" + CmdNameStakeAccount = "stake" + CmdNameListAccounts = "list" + CmdNameUpdateAccount = "update" +) + +func (a AccountOperation) String() string { + return []string{ + CmdNameCreateAccount, + CmdNameConvertAccount, + CmdNameDestroyAccount, + CmdNameAllotAccount, + CmdNameDelegateAccount, + CmdNameStakeAccount, + CmdNameListAccounts, + CmdNameUpdateAccount, + }[a] +} + +func AvailableCommands(cmd string) bool { + availableCommands := map[string]types.Empty{ + CmdNameCreateAccount: types.Void, + CmdNameConvertAccount: types.Void, + CmdNameDestroyAccount: types.Void, + CmdNameAllotAccount: types.Void, + CmdNameDelegateAccount: types.Void, + CmdNameStakeAccount: types.Void, + CmdNameListAccounts: types.Void, + CmdNameUpdateAccount: types.Void, + } + + _, ok := availableCommands[cmd] + return ok +} + +type Configuration struct { + BindAddress string `json:"bindAddress,omitempty"` + AccountStatesFile string `json:"accountStatesFile,omitempty"` + GenesisSeed string `json:"genesisSeed,omitempty"` + BlockIssuerPrivateKey string `json:"blockIssuerPrivateKey,omitempty"` + AccountID string `json:"accountID,omitempty"` +} + +var accountConfigFile = "config.json" + +var ( + dockerAccountConfigJSON = `{ + "bindAddress": "http://localhost:8080", + "accountStatesFile": "wallet.dat", + "genesisSeed": "7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih", + "blockIssuerPrivateKey": "db39d2fde6301d313b108dc9db1ee724d0f405f6fde966bd776365bc5f4a5fb31e4b21eb51dcddf65c20db1065e1f1514658b23a3ddbf48d30c0efc926a9a648", + "accountID": "0x6aee704f25558e8aa7630fed0121da53074188abc423b3c5810f80be4936eb6e"}` +) + +// LoadConfiguration loads the config file. +func LoadConfiguration() *Configuration { + // open config file + config := new(Configuration) + file, err := os.Open(accountConfigFile) + if err != nil { + if !os.IsNotExist(err) { + panic(err) + } + + //nolint:gosec // users should be able to read the file + if err = os.WriteFile(accountConfigFile, []byte(dockerAccountConfigJSON), 0o644); err != nil { + panic(err) + } + if file, err = os.Open(accountConfigFile); err != nil { + panic(err) + } + } + defer file.Close() + + // decode config file + if err = json.NewDecoder(file).Decode(config); err != nil { + panic(err) + } + + return config +} + +func SaveConfiguration(config *Configuration) { + // open config file + file, err := os.Open(accountConfigFile) + if err != nil { + panic(err) + } + defer file.Close() + + jsonConfigs, err := json.MarshalIndent(config, "", " ") + + if err != nil { + log.Errorf("failed to write configs to file %s", err) + } + + //nolint:gosec // users should be able to read the file + if err = os.WriteFile(accountConfigFile, jsonConfigs, 0o644); err != nil { + panic(err) + } +} + +type AccountSubcommands interface { + Type() AccountOperation +} + +type CreateAccountParams struct { + Alias string + Amount uint64 + NoBIF bool + Implicit bool +} + +func (c *CreateAccountParams) Type() AccountOperation { + return OperationCreateAccount +} + +type DestroyAccountParams struct { + AccountAlias string + ExpirySlot uint64 +} + +func (d *DestroyAccountParams) Type() AccountOperation { + return OperationDestroyAccound +} + +type AllotAccountParams struct { + Amount uint64 + To string + From string // if not set we use faucet +} + +func (a *AllotAccountParams) Type() AccountOperation { + return OperationAllotAccount +} + +type ConvertAccountParams struct { + AccountAlias string +} + +func (d *ConvertAccountParams) Type() AccountOperation { + return OperationConvertAccount +} + +type DelegateAccountParams struct { + Amount uint64 + To string + From string // if not set we use faucet +} + +func (a *DelegateAccountParams) Type() AccountOperation { + return OperationDelegateAccount +} + +type StakeAccountParams struct { + Alias string + Amount uint64 + FixedCost uint64 + StartEpoch uint64 + EndEpoch uint64 +} + +func (a *StakeAccountParams) Type() AccountOperation { + return OperationStakeAccount +} + +type UpdateAccountParams struct { + Alias string + BlockIssuerKey string + Mana uint64 + Amount uint64 + ExpirySlot uint64 +} + +func (a *UpdateAccountParams) Type() AccountOperation { + return OperationUpdateAccount +} + +type NoAccountParams struct { + Operation AccountOperation +} + +func (a *NoAccountParams) Type() AccountOperation { + return a.Operation +} + +type StateData struct { + Seed string `serix:"0,mapKey=seed,lengthPrefixType=uint8"` + LastUsedIndex uint64 `serix:"1,mapKey=lastUsedIndex"` + AccountsData []*models.AccountState `serix:"2,mapKey=accounts,lengthPrefixType=uint8"` +} diff --git a/tools/evil-spammer/accountwallet/faucet.go b/tools/evil-spammer/accountwallet/faucet.go new file mode 100644 index 000000000..3a6948142 --- /dev/null +++ b/tools/evil-spammer/accountwallet/faucet.go @@ -0,0 +1,280 @@ +package accountwallet + +import ( + "context" + "sync" + "time" + + "github.com/mr-tron/base58" + + "github.com/iotaledger/hive.go/core/safemath" + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/iota-core/pkg/blockhandler" + "github.com/iotaledger/iota-core/pkg/testsuite/mock" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" + iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/builder" + "github.com/iotaledger/iota.go/v4/nodeclient" + "github.com/iotaledger/iota.go/v4/nodeclient/apimodels" +) + +const ( + FaucetAccountAlias = "faucet" +) + +func (a *AccountWallet) RequestBlockBuiltData(clt *nodeclient.Client, issuerID iotago.AccountID) (*apimodels.CongestionResponse, *apimodels.IssuanceBlockHeaderResponse, iotago.Version, error) { + congestionResp, err := clt.Congestion(context.Background(), issuerID) + if err != nil { + return nil, nil, 0, ierrors.Wrapf(err, "failed to get congestion data for issuer %s", issuerID.ToHex()) + } + + issuerResp, err := clt.BlockIssuance(context.Background()) + if err != nil { + return nil, nil, 0, ierrors.Wrap(err, "failed to get block issuance data") + } + + version := clt.APIForSlot(congestionResp.Slot).Version() + + return congestionResp, issuerResp, version, nil +} + +func (a *AccountWallet) RequestFaucetFunds(clt models.Client, receiveAddr iotago.Address, amount iotago.BaseToken) (*models.Output, error) { + congestionResp, issuerResp, version, err := a.RequestBlockBuiltData(clt.Client(), a.faucet.account.ID()) + if err != nil { + return nil, ierrors.Wrapf(err, "failed to get block built data for issuer %s", a.faucet.account.ID().ToHex()) + } + + signedTx, err := a.faucet.prepareFaucetRequest(receiveAddr, amount, congestionResp.ReferenceManaCost) + if err != nil { + log.Errorf("failed to prepare faucet request: %s", err) + + return nil, err + } + + _, err = a.PostWithBlock(clt, signedTx, a.faucet.account, congestionResp, issuerResp, version) + if err != nil { + log.Errorf("failed to create block: %s", err) + + return nil, err + } + + // set remainder output to be reused by the Faucet wallet + a.faucet.unspentOutput = &models.Output{ + OutputID: iotago.OutputIDFromTransactionIDAndIndex(lo.PanicOnErr(signedTx.Transaction.ID()), 1), + Address: a.faucet.genesisHdWallet.Address(iotago.AddressEd25519).(*iotago.Ed25519Address), + Index: 0, + Balance: signedTx.Transaction.Outputs[1].BaseTokenAmount(), + OutputStruct: signedTx.Transaction.Outputs[1], + } + + return &models.Output{ + OutputID: iotago.OutputIDFromTransactionIDAndIndex(lo.PanicOnErr(signedTx.Transaction.ID()), 0), + Address: receiveAddr, + Index: 0, + Balance: signedTx.Transaction.Outputs[0].BaseTokenAmount(), + OutputStruct: signedTx.Transaction.Outputs[0], + }, 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) + if err != nil { + log.Errorf("failed to create block: %s", err) + + return iotago.EmptyBlockID, err + } + + blockID, err := clt.PostBlock(signedBlock) + if err != nil { + log.Errorf("failed to post block: %s", err) + + return iotago.EmptyBlockID, err + } + + 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) { + issuingTime := time.Now() + issuingSlot := a.client.LatestAPI().TimeProvider().SlotFromTime(issuingTime) + apiForSlot := a.client.APIForSlot(issuingSlot) + + blockBuilder := builder.NewBasicBlockBuilder(apiForSlot) + + commitmentID, err := issuerResp.Commitment.ID() + if err != nil { + return nil, ierrors.Wrap(err, "failed to get commitment id") + } + + blockBuilder.ProtocolVersion(version) + blockBuilder.SlotCommitmentID(commitmentID) + blockBuilder.LatestFinalizedSlot(issuerResp.LatestFinalizedSlot) + blockBuilder.IssuingTime(time.Now()) + blockBuilder.StrongParents(issuerResp.StrongParents) + blockBuilder.WeakParents(issuerResp.WeakParents) + blockBuilder.ShallowLikeParents(issuerResp.ShallowLikeParents) + blockBuilder.MaxBurnedMana(congestionResp.ReferenceManaCost) + + blockBuilder.Payload(payload) + blockBuilder.Sign(issuer.ID(), issuer.PrivateKey()) + + blk, err := blockBuilder.Build() + if err != nil { + return nil, ierrors.Errorf("failed to build block: %w", err) + } + + return blk, nil +} + +type faucetParams struct { + faucetPrivateKey string + faucetAccountID string + genesisSeed string +} + +type faucet struct { + unspentOutput *models.Output + account blockhandler.Account + genesisHdWallet *mock.HDWallet + + clt models.Client + + sync.Mutex +} + +func newFaucet(clt models.Client, faucetParams *faucetParams) (*faucet, error) { + genesisSeed, err := base58.Decode(faucetParams.genesisSeed) + if err != nil { + log.Warnf("failed to decode base58 seed, using the default one: %v", err) + } + faucetAddr := mock.NewHDWallet("", genesisSeed, 0).Address(iotago.AddressEd25519) + + f := &faucet{ + clt: clt, + account: blockhandler.AccountFromParams(faucetParams.faucetAccountID, faucetParams.faucetPrivateKey), + genesisHdWallet: mock.NewHDWallet("", genesisSeed, 0), + } + + faucetUnspentOutput, faucetUnspentOutputID, faucetAmount, err := f.getGenesisOutputFromIndexer(clt, faucetAddr) + if err != nil { + return nil, ierrors.Wrap(err, "failed to get faucet output from indexer") + } + + f.unspentOutput = &models.Output{ + Address: faucetAddr.(*iotago.Ed25519Address), + Index: 0, + OutputID: faucetUnspentOutputID, + Balance: faucetAmount, + OutputStruct: faucetUnspentOutput, + } + + return f, nil +} + +func (f *faucet) getGenesisOutputFromIndexer(clt models.Client, faucetAddr iotago.DirectUnlockableAddress) (iotago.Output, iotago.OutputID, iotago.BaseToken, error) { + indexer, err := clt.Indexer() + if err != nil { + panic(ierrors.Wrap(err, "failed to get indexer")) + } + + results, err := indexer.Outputs(context.Background(), &apimodels.BasicOutputsQuery{ + AddressBech32: faucetAddr.Bech32(iotago.PrefixTestnet), + }) + if err != nil { + return nil, iotago.EmptyOutputID, 0, ierrors.Wrap(err, "failed to prepare faucet unspent outputs indexer request") + } + + var ( + faucetUnspentOutput iotago.Output + faucetUnspentOutputID iotago.OutputID + faucetAmount iotago.BaseToken + ) + for results.Next() { + unspents, err := results.Outputs(context.TODO()) + if err != nil { + return nil, iotago.EmptyOutputID, 0, ierrors.Wrap(err, "failed to get faucet unspent outputs") + } + + faucetUnspentOutput = unspents[0] + faucetAmount = faucetUnspentOutput.BaseTokenAmount() + faucetUnspentOutputID = lo.Return1(results.Response.Items.OutputIDs())[0] + } + + return faucetUnspentOutput, faucetUnspentOutputID, faucetAmount, nil +} + +func (f *faucet) prepareFaucetRequest(receiveAddr iotago.Address, amount iotago.BaseToken, rmc iotago.Mana) (*iotago.SignedTransaction, error) { + remainderAmount, err := safemath.SafeSub(f.unspentOutput.Balance, amount) + if err != nil { + panic(err) + } + + txBuilder, remainderIndex, err := f.createFaucetTransactionNoManaHandling(receiveAddr, amount, remainderAmount) + if err != nil { + return nil, err + } + + rmcAllotedTxBuilder := txBuilder.Clone() + // faucet will allot exact mana to be burnt, rest of the mana is alloted to faucet output remainder + rmcAllotedTxBuilder.AllotRequiredManaAndStoreRemainingManaInOutput(txBuilder.CreationSlot(), rmc, f.account.ID(), remainderIndex) + + var signedTx *iotago.SignedTransaction + signedTx, err = rmcAllotedTxBuilder.Build(f.genesisHdWallet.AddressSigner()) + if err != nil { + log.Infof("WARN: failed to build tx with min required mana allotted, genesis potential mana was not enough, fallback to faucet account") + txBuilder.AllotAllMana(txBuilder.CreationSlot(), f.account.ID()) + if signedTx, err = txBuilder.Build(f.genesisHdWallet.AddressSigner()); err != nil { + return nil, ierrors.Wrapf(err, "failed to build transaction with all mana allotted, after not having enough mana required based on RMC") + } + } + + return signedTx, nil +} + +func (f *faucet) createFaucetTransactionNoManaHandling(receiveAddr iotago.Address, amount iotago.BaseToken, remainderAmount iotago.BaseToken) (*builder.TransactionBuilder, int, error) { + currentTime := time.Now() + currentSlot := f.clt.LatestAPI().TimeProvider().SlotFromTime(currentTime) + + apiForSlot := f.clt.APIForSlot(currentSlot) + txBuilder := builder.NewTransactionBuilder(apiForSlot) + + txBuilder.AddInput(&builder.TxInput{ + UnlockTarget: f.genesisHdWallet.Address(iotago.AddressEd25519).(*iotago.Ed25519Address), + InputID: f.unspentOutput.OutputID, + Input: f.unspentOutput.OutputStruct, + }) + + switch receiveAddr.(type) { + case *iotago.Ed25519Address: + txBuilder.AddOutput(&iotago.BasicOutput{ + Amount: amount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: receiveAddr}, + }, + }) + case *iotago.ImplicitAccountCreationAddress: + log.Infof("creating account %s", receiveAddr) + accOutputBuilder := builder.NewAccountOutputBuilder(receiveAddr, receiveAddr, amount) + output, err := accOutputBuilder.Build() + if err != nil { + log.Errorf("failed to build account output: %s", err) + + return nil, 0, err + } + txBuilder.AddOutput(output) + } + + // remainder output + remainderIndex := 1 + txBuilder.AddOutput(&iotago.BasicOutput{ + Amount: remainderAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: f.genesisHdWallet.Address(iotago.AddressEd25519).(*iotago.Ed25519Address)}, + }, + }) + txBuilder.AddTaggedDataPayload(&iotago.TaggedData{Tag: []byte("Faucet funds"), Data: []byte("to addr" + receiveAddr.String())}) + txBuilder.SetCreationSlot(currentSlot) + + return txBuilder, remainderIndex, nil +} diff --git a/tools/evil-spammer/accountwallet/options.go b/tools/evil-spammer/accountwallet/options.go new file mode 100644 index 000000000..51b28fcd7 --- /dev/null +++ b/tools/evil-spammer/accountwallet/options.go @@ -0,0 +1,24 @@ +package accountwallet + +import ( + "github.com/iotaledger/hive.go/runtime/options" +) + +// WithClientURL sets the client bind address. +func WithClientURL(url string) options.Option[AccountWallet] { + return func(w *AccountWallet) { + w.optsClientBindAddress = url + } +} + +func WithAccountStatesFile(fileName string) options.Option[AccountWallet] { + return func(w *AccountWallet) { + w.optsAccountStatesFile = fileName + } +} + +func WithFaucetAccountParams(params *faucetParams) options.Option[AccountWallet] { + return func(w *AccountWallet) { + w.optsFaucetParams = params + } +} diff --git a/tools/evil-spammer/accountwallet/wallet.go b/tools/evil-spammer/accountwallet/wallet.go new file mode 100644 index 000000000..eaae1e9b9 --- /dev/null +++ b/tools/evil-spammer/accountwallet/wallet.go @@ -0,0 +1,335 @@ +package accountwallet + +import ( + "crypto/ed25519" + "os" + "time" + + "github.com/mr-tron/base58" + + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/runtime/options" + "github.com/iotaledger/hive.go/runtime/timeutil" + "github.com/iotaledger/iota-core/pkg/blockhandler" + "github.com/iotaledger/iota-core/pkg/testsuite/mock" + "github.com/iotaledger/iota-core/tools/evil-spammer/logger" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" + iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/builder" + "github.com/iotaledger/iota.go/v4/tpkg" +) + +var log = logger.New("AccountWallet") + +func Run(config *Configuration) (*AccountWallet, error) { + var opts []options.Option[AccountWallet] + if config.BindAddress != "" { + opts = append(opts, WithClientURL(config.BindAddress)) + } + if config.AccountStatesFile != "" { + opts = append(opts, WithAccountStatesFile(config.AccountStatesFile)) + } + + opts = append(opts, WithFaucetAccountParams(&faucetParams{ + genesisSeed: config.GenesisSeed, + faucetPrivateKey: config.BlockIssuerPrivateKey, + faucetAccountID: config.AccountID, + })) + + wallet := NewAccountWallet(opts...) + + // load wallet + err := wallet.fromAccountStateFile() + if err != nil { + return nil, ierrors.Wrap(err, "failed to load wallet from file") + } + + return wallet, nil +} + +func SaveState(w *AccountWallet) error { + return w.toAccountStateFile() +} + +type AccountWallet struct { + faucet *faucet + seed [32]byte + + accountsAliases map[string]*models.AccountData + + //accountsStatus map[string]models.AccountMetadata + latestUsedIndex uint64 + + client *models.WebClient + + optsClientBindAddress string + optsAccountStatesFile string + optsFaucetParams *faucetParams + optsRequestTimeout time.Duration + optsRequestTicker time.Duration +} + +func NewAccountWallet(opts ...options.Option[AccountWallet]) *AccountWallet { + return options.Apply(&AccountWallet{ + accountsAliases: make(map[string]*models.AccountData), + seed: tpkg.RandEd25519Seed(), + optsRequestTimeout: time.Second * 120, + optsRequestTicker: time.Second * 5, + }, opts, func(w *AccountWallet) { + w.client = models.NewWebClient(w.optsClientBindAddress) + + faucet, err := newFaucet(w.client, w.optsFaucetParams) + if err != nil { + panic(ierrors.Wrap(err, "failed to create faucet")) + } + + w.faucet = faucet + w.accountsAliases[FaucetAccountAlias] = &models.AccountData{ + Alias: FaucetAccountAlias, + Status: models.AccountReady, + OutputID: iotago.EmptyOutputID, + Index: 0, + Account: w.faucet.account, + } + }) +} + +// toAccountStateFile write account states to file. +func (a *AccountWallet) toAccountStateFile() error { + accounts := make([]*models.AccountState, 0) + + for _, acc := range a.accountsAliases { + accounts = append(accounts, models.AccountStateFromAccountData(acc)) + } + + stateBytes, err := a.client.LatestAPI().Encode(&StateData{ + Seed: base58.Encode(a.seed[:]), + LastUsedIndex: a.latestUsedIndex, + AccountsData: accounts, + }) + if err != nil { + return ierrors.Wrap(err, "failed to encode state") + } + + //nolint:gosec // users should be able to read the file + if err = os.WriteFile(a.optsAccountStatesFile, stateBytes, 0o644); err != nil { + return ierrors.Wrap(err, "failed to write account states to file") + } + + return nil +} + +func (a *AccountWallet) fromAccountStateFile() error { + walletStateBytes, err := os.ReadFile(a.optsAccountStatesFile) + if err != nil { + if !os.IsNotExist(err) { + return ierrors.Wrap(err, "failed to read file") + } + return nil + } + + var data StateData + _, err = a.client.LatestAPI().Decode(walletStateBytes, &data) + if err != nil { + return ierrors.Wrap(err, "failed to decode from file") + } + + // copy seeds + decodedSeeds, err := base58.Decode(data.Seed) + if err != nil { + return ierrors.Wrap(err, "failed to decode seed") + } + copy(a.seed[:], decodedSeeds) + + // set latest used index + a.latestUsedIndex = data.LastUsedIndex + + // account data + for _, acc := range data.AccountsData { + a.accountsAliases[acc.Alias] = acc.ToAccountData() + if acc.Alias == FaucetAccountAlias { + a.accountsAliases[acc.Alias].Status = models.AccountReady + } + } + + return nil +} + +func (a *AccountWallet) registerAccount(alias string, outputID iotago.OutputID, index uint64, privKey ed25519.PrivateKey) iotago.AccountID { + accountID := iotago.AccountIDFromOutputID(outputID) + account := blockhandler.NewEd25519Account(accountID, privKey) + + a.accountsAliases[alias] = &models.AccountData{ + Alias: alias, + Account: account, + Status: models.AccountPending, + OutputID: outputID, + Index: index, + } + + return accountID +} + +func (a *AccountWallet) updateAccountStatus(alias string, status models.AccountStatus) (*models.AccountData, bool) { + accData, exists := a.accountsAliases[alias] + if !exists { + return nil, false + } + + if accData.Status == status { + return accData, false + } + + accData.Status = status + a.accountsAliases[alias] = accData + + return accData, true +} + +func (a *AccountWallet) GetReadyAccount(alias string) (*models.AccountData, error) { + accData, exists := a.accountsAliases[alias] + if !exists { + return nil, ierrors.Errorf("account with alias %s does not exist", alias) + } + + // check if account is ready (to be included in a commitment) + ready := a.isAccountReady(accData) + if !ready { + return nil, ierrors.Errorf("account with alias %s is not ready", alias) + } + + accData, _ = a.updateAccountStatus(alias, models.AccountReady) + + return accData, nil +} + +func (a *AccountWallet) GetAccount(alias string) (*models.AccountData, error) { + accData, exists := a.accountsAliases[alias] + if !exists { + return nil, ierrors.Errorf("account with alias %s does not exist", alias) + } + + return accData, nil +} + +func (a *AccountWallet) isAccountReady(accData *models.AccountData) bool { + if accData.Status == models.AccountReady { + return true + } + + creationSlot := accData.OutputID.CreationSlot() + + // wait for the account to be committed + log.Infof("Waiting for account %s to be committed within slot %d...", accData.Alias, creationSlot) + err := a.retry(func() (bool, error) { + resp, err := a.client.GetBlockIssuance() + if err != nil { + return false, err + } + + if resp.Commitment.Slot >= creationSlot { + log.Infof("Slot %d commited, account %s is ready to use", creationSlot, accData.Alias) + return true, nil + } + + return false, nil + }) + + if err != nil { + log.Errorf("failed to get commitment details while waiting %s: %s", accData.Alias, err) + return false + } + + return true +} + +func (a *AccountWallet) getFunds(amount uint64, addressType iotago.AddressType) (*models.Output, ed25519.PrivateKey, error) { + hdWallet := mock.NewHDWallet("", a.seed[:], a.latestUsedIndex+1) + privKey, _ := hdWallet.KeyPair() + receiverAddr := hdWallet.Address(addressType) + createdOutput, err := a.RequestFaucetFunds(a.client, receiverAddr, iotago.BaseToken(amount)) + if err != nil { + return nil, nil, ierrors.Wrap(err, "failed to request funds from Faucet") + } + + a.latestUsedIndex++ + createdOutput.Index = a.latestUsedIndex + + return createdOutput, privKey, nil +} + +func (a *AccountWallet) destroyAccount(alias string) error { + accData, err := a.GetAccount(alias) + if err != nil { + return err + } + hdWallet := mock.NewHDWallet("", a.seed[:], accData.Index) + + issuingTime := time.Now() + issuingSlot := a.client.LatestAPI().TimeProvider().SlotFromTime(issuingTime) + apiForSlot := a.client.APIForSlot(issuingSlot) + + // get output from node + // From TIP42: Indexers and node plugins shall map the account address of the output derived with Account ID to the regular address -> output mapping table, so that given an Account Address, its most recent unspent account output can be retrieved. + // TODO: use correct outputID + accountOutput := a.client.GetOutput(accData.OutputID) + + txBuilder := builder.NewTransactionBuilder(apiForSlot) + txBuilder.AddInput(&builder.TxInput{ + UnlockTarget: a.accountsAliases[alias].Account.ID().ToAddress(), + InputID: accData.OutputID, + Input: accountOutput, + }) + + // send all tokens to faucet + txBuilder.AddOutput(&iotago.BasicOutput{ + Amount: accountOutput.BaseTokenAmount(), + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: a.faucet.genesisHdWallet.Address(iotago.AddressEd25519).(*iotago.Ed25519Address)}, + }, + }) + + tx, err := txBuilder.Build(hdWallet.AddressSigner()) + if err != nil { + return ierrors.Wrapf(err, "failed to build transaction for account alias destruction %s", alias) + } + + congestionResp, issuerResp, version, err := a.RequestBlockBuiltData(a.client.Client(), a.faucet.account.ID()) + if err != nil { + return ierrors.Wrap(err, "failed to request block built data for the faucet account") + } + + blockID, err := a.PostWithBlock(a.client, tx, a.faucet.account, congestionResp, issuerResp, version) + if err != nil { + return ierrors.Wrapf(err, "failed to post block with ID %s", blockID) + } + + // remove account from wallet + delete(a.accountsAliases, alias) + + log.Infof("Account %s has been destroyed", alias) + return nil +} + +func (a *AccountWallet) retry(requestFunc func() (bool, error)) error { + timeout := time.NewTimer(a.optsRequestTimeout) + interval := time.NewTicker(a.optsRequestTicker) + defer timeutil.CleanupTimer(timeout) + defer timeutil.CleanupTicker(interval) + + for { + done, err := requestFunc() + if err != nil { + return err + } + if done { + return nil + } + select { + case <-interval.C: + continue + case <-timeout.C: + return ierrors.New("timeout while trying to request") + } + } +} diff --git a/tools/evil-spammer/basic.go b/tools/evil-spammer/basic.go deleted file mode 100644 index 222af823c..000000000 --- a/tools/evil-spammer/basic.go +++ /dev/null @@ -1,212 +0,0 @@ -package main - -import ( - "sync" - "time" - - "github.com/iotaledger/iota-core/pkg/protocol/snapshotcreator" - "github.com/iotaledger/iota-core/tools/evil-spammer/spammer" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" - iotago "github.com/iotaledger/iota.go/v4" -) - -type CustomSpamParams struct { - ClientURLs []string - SpamTypes []string - Rates []int - Durations []time.Duration - BlkToBeSent []int - TimeUnit time.Duration - DelayBetweenConflicts time.Duration - NSpend int - Scenario wallet.EvilBatch - DeepSpam bool - EnableRateSetter bool - - config *BasicConfig -} - -func CustomSpam(params *CustomSpamParams) { - outputID := iotago.OutputIDFromTransactionIDAndIndex(snapshotcreator.GenesisTransactionID, 0) - if params.config.LastFaucetUnspentOutputID != "" { - outputID, _ = iotago.OutputIDFromHexString(params.config.LastFaucetUnspentOutputID) - } - - w := wallet.NewEvilWallet(wallet.WithClients(params.ClientURLs...), wallet.WithFaucetOutputID(outputID)) - wg := sync.WaitGroup{} - - fundsNeeded := false - for _, st := range params.SpamTypes { - if st != SpammerTypeBlock { - fundsNeeded = true - } - } - if fundsNeeded { - err := w.RequestFreshBigFaucetWallet() - if err != nil { - panic(err) - } - saveConfigsToFile(&BasicConfig{ - LastFaucetUnspentOutputID: w.LastFaucetUnspentOutput().ToHex(), - }) - } - - 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) - - switch sType { - case SpammerTypeBlock: - 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) - if s == nil { - return - } - s.Spam() - }(i) - case SpammerTypeTx: - wg.Add(1) - go func(i int) { - defer wg.Done() - SpamTransaction(w, params.Rates[i], params.TimeUnit, params.Durations[i], params.DeepSpam, params.EnableRateSetter) - }(i) - case SpammerTypeDs: - 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) - }(i) - case SpammerTypeCustom: - 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) - if s == nil { - return - } - s.Spam() - }(i) - case SpammerTypeCommitments: - wg.Add(1) - go func() { - defer wg.Done() - }() - - default: - log.Warn("Spamming type not recognized. Try one of following: tx, ds, blk, custom, commitments") - } - } - - wg.Wait() - log.Info("Basic spamming finished!") -} - -func SpamTransaction(w *wallet.EvilWallet, rate int, timeUnit, duration time.Duration, deepSpam, enableRateSetter bool) { - if w.NumOfClient() < 1 { - printer.NotEnoughClientsWarning(1) - } - - scenarioOptions := []wallet.ScenarioOption{ - wallet.WithScenarioCustomConflicts(wallet.SingleTransactionBatch()), - } - if deepSpam { - outWallet := wallet.NewWallet(wallet.Reuse) - scenarioOptions = append(scenarioOptions, - wallet.WithScenarioDeepSpamEnabled(), - wallet.WithScenarioReuseOutputWallet(outWallet), - wallet.WithScenarioInputWalletForDeepSpam(outWallet), - ) - } - scenarioTx := wallet.NewEvilScenario(scenarioOptions...) - - options := []spammer.Options{ - spammer.WithSpamRate(rate, timeUnit), - spammer.WithSpamDuration(duration), - spammer.WithRateSetter(enableRateSetter), - spammer.WithEvilWallet(w), - spammer.WithEvilScenario(scenarioTx), - } - - s := spammer.NewSpammer(options...) - s.Spam() -} - -func SpamDoubleSpends(w *wallet.EvilWallet, rate, nSpent int, timeUnit, duration, delayBetweenConflicts time.Duration, deepSpam, enableRateSetter bool) { - log.Debugf("Setting up double spend spammer with rate: %d, time unit: %s, and duration: %s.", rate, timeUnit.String(), duration.String()) - if w.NumOfClient() < 2 { - printer.NotEnoughClientsWarning(2) - } - - scenarioOptions := []wallet.ScenarioOption{ - wallet.WithScenarioCustomConflicts(wallet.NSpendBatch(nSpent)), - } - if deepSpam { - outWallet := wallet.NewWallet(wallet.Reuse) - scenarioOptions = append(scenarioOptions, - wallet.WithScenarioDeepSpamEnabled(), - wallet.WithScenarioReuseOutputWallet(outWallet), - wallet.WithScenarioInputWalletForDeepSpam(outWallet), - ) - } - scenarioDs := wallet.NewEvilScenario(scenarioOptions...) - options := []spammer.Options{ - spammer.WithSpamRate(rate, timeUnit), - spammer.WithSpamDuration(duration), - spammer.WithEvilWallet(w), - spammer.WithRateSetter(enableRateSetter), - spammer.WithTimeDelayForDoubleSpend(delayBetweenConflicts), - spammer.WithEvilScenario(scenarioDs), - } - - s := spammer.NewSpammer(options...) - s.Spam() -} - -func SpamNestedConflicts(w *wallet.EvilWallet, rate int, timeUnit, duration time.Duration, conflictBatch wallet.EvilBatch, deepSpam, reuseOutputs, enableRateSetter bool) *spammer.Spammer { - scenarioOptions := []wallet.ScenarioOption{ - wallet.WithScenarioCustomConflicts(conflictBatch), - } - if deepSpam { - outWallet := wallet.NewWallet(wallet.Reuse) - scenarioOptions = append(scenarioOptions, - wallet.WithScenarioDeepSpamEnabled(), - wallet.WithScenarioReuseOutputWallet(outWallet), - wallet.WithScenarioInputWalletForDeepSpam(outWallet), - ) - } else if reuseOutputs { - outWallet := wallet.NewWallet(wallet.Reuse) - scenarioOptions = append(scenarioOptions, wallet.WithScenarioReuseOutputWallet(outWallet)) - } - scenario := wallet.NewEvilScenario(scenarioOptions...) - if scenario.NumOfClientsNeeded > w.NumOfClient() { - printer.NotEnoughClientsWarning(scenario.NumOfClientsNeeded) - } - - options := []spammer.Options{ - spammer.WithSpamRate(rate, timeUnit), - spammer.WithSpamDuration(duration), - spammer.WithEvilWallet(w), - spammer.WithRateSetter(enableRateSetter), - spammer.WithEvilScenario(scenario), - } - - return spammer.NewSpammer(options...) -} - -func SpamBlocks(w *wallet.EvilWallet, rate int, timeUnit, duration time.Duration, numBlkToSend int, enableRateSetter bool) *spammer.Spammer { - if w.NumOfClient() < 1 { - printer.NotEnoughClientsWarning(1) - } - - options := []spammer.Options{ - spammer.WithSpamRate(rate, timeUnit), - spammer.WithSpamDuration(duration), - spammer.WithBatchesSent(numBlkToSend), - spammer.WithRateSetter(enableRateSetter), - spammer.WithEvilWallet(w), - spammer.WithSpammingFunc(spammer.DataSpammingFunction), - } - - return spammer.NewSpammer(options...) -} diff --git a/tools/evil-spammer/config.go b/tools/evil-spammer/config.go index 3bd2c1f34..c8fc6e93d 100644 --- a/tools/evil-spammer/config.go +++ b/tools/evil-spammer/config.go @@ -3,33 +3,39 @@ package main import ( "time" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/accountwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/programs" + "github.com/iotaledger/iota-core/tools/evil-spammer/spammer" ) // Nodes used during the test, use at least two nodes to be able to doublespend. var ( // urls = []string{"http://bootstrap-01.feature.shimmer.iota.cafe:8080", "http://vanilla-01.feature.shimmer.iota.cafe:8080", "http://drng-01.feature.shimmer.iota.cafe:8080"} - // urls = []string{"http://localhost:8080", "http://localhost:8090", "http://localhost:8070", "http://localhost:8040"} - urls = []string{} + urls = []string{"http://localhost:8080"} //, "http://localhost:8090", "http://localhost:8070", "http://localhost:8040"} + //urls = []string{} ) var ( - Script = "basic" + Script = "basic" + Subcommand = "" - customSpamParams = CustomSpamParams{ + customSpamParams = programs.CustomSpamParams{ ClientURLs: urls, - SpamTypes: []string{SpammerTypeBlock}, + SpamTypes: []string{spammer.TypeBlock}, Rates: []int{1}, Durations: []time.Duration{time.Second * 20}, BlkToBeSent: []int{0}, TimeUnit: time.Second, DelayBetweenConflicts: 0, NSpend: 2, - Scenario: wallet.Scenario1(), + Scenario: evilwallet.Scenario1(), DeepSpam: false, EnableRateSetter: false, + AccountAlias: accountwallet.FaucetAccountAlias, } - quickTestParams = QuickTestParams{ + + quickTestParams = programs.QuickTestParams{ ClientURLs: urls, Rate: 100, Duration: time.Second * 30, @@ -38,6 +44,8 @@ var ( EnableRateSetter: false, } + accountsSubcommandsFlags []accountwallet.AccountSubcommands + //nolint:godot // commitmentsSpamParams = CommitmentsSpamParams{ // Rate: 1, @@ -45,7 +53,7 @@ var ( // TimeUnit: time.Second, // NetworkAlias: "docker", // SpammerAlias: "peer_master", - // ValidAlias: "faucet", + // ValidAlias: accountwallet.FaucetAccountAlias, // CommitmentType: "latest", // ForkAfter: 10, // } diff --git a/tools/evil-spammer/wallet/aliasmanager.go b/tools/evil-spammer/evilwallet/aliasmanager.go similarity index 71% rename from tools/evil-spammer/wallet/aliasmanager.go rename to tools/evil-spammer/evilwallet/aliasmanager.go index c653c10b0..284e89f6d 100644 --- a/tools/evil-spammer/wallet/aliasmanager.go +++ b/tools/evil-spammer/evilwallet/aliasmanager.go @@ -1,18 +1,19 @@ -package wallet +package evilwallet import ( "go.uber.org/atomic" "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/runtime/syncutils" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" ) // region AliasManager ///////////////////////////////////////////////////////////////////////////////////////////////// // AliasManager is the manager for output aliases. type AliasManager struct { - outputMap map[string]*Output - inputMap map[string]*Output + outputMap map[string]*models.Output + inputMap map[string]*models.Output outputAliasCount *atomic.Uint64 mu syncutils.RWMutex @@ -21,14 +22,14 @@ type AliasManager struct { // NewAliasManager creates and returns a new AliasManager. func NewAliasManager() *AliasManager { return &AliasManager{ - outputMap: make(map[string]*Output), - inputMap: make(map[string]*Output), + outputMap: make(map[string]*models.Output), + inputMap: make(map[string]*models.Output), outputAliasCount: atomic.NewUint64(0), } } // AddOutputAlias maps the given outputAliasName to output, if there's duplicate outputAliasName, it will be overwritten. -func (a *AliasManager) AddOutputAlias(output *Output, aliasName string) { +func (a *AliasManager) AddOutputAlias(output *models.Output, aliasName string) { a.mu.Lock() defer a.mu.Unlock() @@ -36,7 +37,7 @@ func (a *AliasManager) AddOutputAlias(output *Output, aliasName string) { } // AddInputAlias adds an input alias. -func (a *AliasManager) AddInputAlias(input *Output, aliasName string) { +func (a *AliasManager) AddInputAlias(input *models.Output, aliasName string) { a.mu.Lock() defer a.mu.Unlock() @@ -44,7 +45,7 @@ func (a *AliasManager) AddInputAlias(input *Output, aliasName string) { } // GetInput returns the input for the alias specified. -func (a *AliasManager) GetInput(aliasName string) (*Output, bool) { +func (a *AliasManager) GetInput(aliasName string) (*models.Output, bool) { a.mu.RLock() defer a.mu.RUnlock() in, ok := a.inputMap[aliasName] @@ -53,7 +54,7 @@ func (a *AliasManager) GetInput(aliasName string) (*Output, bool) { } // GetOutput returns the output for the alias specified. -func (a *AliasManager) GetOutput(aliasName string) *Output { +func (a *AliasManager) GetOutput(aliasName string) *models.Output { a.mu.RLock() defer a.mu.RUnlock() @@ -65,8 +66,8 @@ func (a *AliasManager) ClearAllAliases() { a.mu.Lock() defer a.mu.Unlock() - a.inputMap = make(map[string]*Output) - a.outputMap = make(map[string]*Output) + a.inputMap = make(map[string]*models.Output) + a.outputMap = make(map[string]*models.Output) } // ClearAliases clears provided aliases. @@ -83,7 +84,7 @@ func (a *AliasManager) ClearAliases(aliases ScenarioAlias) { } // AddOutputAliases batch adds the outputs their respective aliases. -func (a *AliasManager) AddOutputAliases(outputs []*Output, aliases []string) error { +func (a *AliasManager) AddOutputAliases(outputs []*models.Output, aliases []string) error { if len(outputs) != len(aliases) { return ierrors.New("mismatch outputs and aliases length") } @@ -95,7 +96,7 @@ func (a *AliasManager) AddOutputAliases(outputs []*Output, aliases []string) err } // AddInputAliases batch adds the inputs their respective aliases. -func (a *AliasManager) AddInputAliases(inputs []*Output, aliases []string) error { +func (a *AliasManager) AddInputAliases(inputs []*models.Output, aliases []string) error { if len(inputs) != len(aliases) { return ierrors.New("mismatch outputs and aliases length") } diff --git a/tools/evil-spammer/wallet/customscenarios.go b/tools/evil-spammer/evilwallet/customscenarios.go similarity index 99% rename from tools/evil-spammer/wallet/customscenarios.go rename to tools/evil-spammer/evilwallet/customscenarios.go index 0f5599544..e26142746 100644 --- a/tools/evil-spammer/wallet/customscenarios.go +++ b/tools/evil-spammer/evilwallet/customscenarios.go @@ -1,4 +1,4 @@ -package wallet +package evilwallet import ( "strconv" diff --git a/tools/evil-spammer/wallet/evilscenario.go b/tools/evil-spammer/evilwallet/evilscenario.go similarity index 92% rename from tools/evil-spammer/wallet/evilscenario.go rename to tools/evil-spammer/evilwallet/evilscenario.go index bd7c04805..00f15d968 100644 --- a/tools/evil-spammer/wallet/evilscenario.go +++ b/tools/evil-spammer/evilwallet/evilscenario.go @@ -1,4 +1,4 @@ -package wallet +package evilwallet import ( "fmt" @@ -8,6 +8,7 @@ import ( "go.uber.org/atomic" "github.com/iotaledger/hive.go/ds/types" + iotago "github.com/iotaledger/iota.go/v4" ) // The custom conflict in spammer can be provided like this: @@ -41,6 +42,8 @@ type EvilScenario struct { ConflictBatch EvilBatch // determines whether outputs of the batch should be reused during the spam to create deep UTXO tree structure. Reuse bool + // specifies the output type of the spam, if not provided, defaults to BasicOutput + OutputType iotago.OutputType // if provided, the outputs from the spam will be saved into this wallet, accepted types of wallet: Reuse, RestrictedReuse. // if type == Reuse, then wallet is available for reuse spamming scenarios that did not provide RestrictedWallet. OutputWallet *Wallet @@ -48,7 +51,8 @@ type EvilScenario struct { // if not provided evil wallet will use Reuse wallet if any is available. Accepts only RestrictedReuse wallet type. RestrictedInputWallet *Wallet // used together with scenario ID to create a prefix for distinct batch alias creation - BatchesCreated *atomic.Uint64 + BatchesCreated *atomic.Uint64 + // used to determine how many clients are needed to run this scenario, some double spends need more than one client to pass the filter NumOfClientsNeeded int } @@ -56,6 +60,7 @@ func NewEvilScenario(options ...ScenarioOption) *EvilScenario { scenario := &EvilScenario{ ConflictBatch: SingleTransactionBatch(), Reuse: false, + OutputType: iotago.OutputBasic, OutputWallet: NewWallet(), BatchesCreated: atomic.NewUint64(0), } diff --git a/tools/evil-spammer/wallet/evilwallet.go b/tools/evil-spammer/evilwallet/evilwallet.go similarity index 68% rename from tools/evil-spammer/wallet/evilwallet.go rename to tools/evil-spammer/evilwallet/evilwallet.go index 9501972ac..007059cce 100644 --- a/tools/evil-spammer/wallet/evilwallet.go +++ b/tools/evil-spammer/evilwallet/evilwallet.go @@ -1,19 +1,23 @@ -package wallet +package evilwallet import ( - "fmt" - "log" "sync" "time" - "github.com/mr-tron/base58" + "github.com/ethereum/go-ethereum/log" "github.com/iotaledger/hive.go/ds/types" "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/hive.go/logger" "github.com/iotaledger/hive.go/runtime/options" + "github.com/iotaledger/iota-core/pkg/blockhandler" + "github.com/iotaledger/iota-core/tools/evil-spammer/accountwallet" + evillogger "github.com/iotaledger/iota-core/tools/evil-spammer/logger" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" iotago "github.com/iotaledger/iota.go/v4" "github.com/iotaledger/iota.go/v4/builder" + "github.com/iotaledger/iota.go/v4/nodeclient/apimodels" "github.com/iotaledger/iota.go/v4/tpkg" ) @@ -22,28 +26,15 @@ const ( FaucetRequestSplitNumber = 100 faucetTokensPerRequest iotago.BaseToken = 1_000_000 - waitForConfirmation = 150 * time.Second - waitForSolidification = 150 * time.Second + waitForConfirmation = 15 * time.Second + waitForSolidification = 10 * time.Second - awaitConfirmationSleep = 3 * time.Second + awaitConfirmationSleep = 2 * time.Second awaitSolidificationSleep = time.Millisecond * 500 - - WaitForTxSolid = 150 * time.Second ) var ( defaultClientsURLs = []string{"http://localhost:8080", "http://localhost:8090"} - - genesisTransactionID = iotago.TransactionIDRepresentingData(0, []byte("genesis")) - - dockerFaucetSeed = func() []byte { - genesisSeed, err := base58.Decode("7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih") - if err != nil { - log.Fatal(ierrors.Errorf("failed to decode base58 seed, using the default one: %w", err)) - } - - return genesisSeed - } ) // region EvilWallet /////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -53,56 +44,27 @@ type EvilWallet struct { // faucet is the wallet of faucet faucet *Wallet wallets *Wallets - connector Connector + accWallet *accountwallet.AccountWallet + connector models.Connector outputManager *OutputManager aliasManager *AliasManager - optFaucetSeed []byte - optFaucetUnspentOutputID iotago.OutputID - optsClientURLs []string + optsClientURLs []string + log *logger.Logger } // NewEvilWallet creates an EvilWallet instance. func NewEvilWallet(opts ...options.Option[EvilWallet]) *EvilWallet { return options.Apply(&EvilWallet{ - wallets: NewWallets(), - aliasManager: NewAliasManager(), - optFaucetSeed: dockerFaucetSeed(), - optFaucetUnspentOutputID: iotago.OutputIDFromTransactionIDAndIndex(genesisTransactionID, 0), - optsClientURLs: defaultClientsURLs, + wallets: NewWallets(), + aliasManager: NewAliasManager(), + optsClientURLs: defaultClientsURLs, + log: evillogger.New("EvilWallet"), }, opts, func(w *EvilWallet) { - connector := NewWebClients(w.optsClientURLs) + connector := models.NewWebClients(w.optsClientURLs) w.connector = connector + w.outputManager = NewOutputManager(connector, w.wallets, w.log) - clt := w.connector.GetClient() - - w.outputManager = NewOutputManager(connector, w.wallets) - - w.faucet = NewWallet() - w.faucet.seed = [32]byte(w.optFaucetSeed) - - // get faucet output and amount - var faucetAmount iotago.BaseToken - - faucetOutput := clt.GetOutput(w.optFaucetUnspentOutputID) - if faucetOutput != nil { - faucetAmount = faucetOutput.BaseTokenAmount() - } else { - // use the genesis output ID instead, if we relaunch the docker network - w.optFaucetUnspentOutputID = iotago.OutputIDFromTransactionIDAndIndex(genesisTransactionID, 0) - faucetOutput = clt.GetOutput(w.optFaucetUnspentOutputID) - if faucetOutput != nil { - faucetAmount = faucetOutput.BaseTokenAmount() - } - } - - w.faucet.AddUnspentOutput(&Output{ - Address: w.faucet.AddressOnIndex(0), - Index: 0, - OutputID: w.optFaucetUnspentOutputID, - Balance: faucetAmount, - OutputStruct: faucetOutput, - }) }) } @@ -124,12 +86,12 @@ func (e *EvilWallet) NewWallet(wType ...WalletType) *Wallet { } // GetClients returns the given number of clients. -func (e *EvilWallet) GetClients(num int) []Client { +func (e *EvilWallet) GetClients(num int) []models.Client { return e.connector.GetClients(num) } // Connector give access to the EvilWallet connector. -func (e *EvilWallet) Connector() Connector { +func (e *EvilWallet) Connector() models.Connector { return e.connector } @@ -150,6 +112,28 @@ func (e *EvilWallet) RemoveClient(clientURL string) { e.connector.RemoveClient(clientURL) } +func (e *EvilWallet) GetAccount(alias string) (blockhandler.Account, error) { + account, err := e.accWallet.GetAccount(alias) + if err != nil { + return nil, err + } + + return account.Account, nil +} + +func (e *EvilWallet) PrepareAndPostBlock(clt models.Client, payload iotago.Payload, congestionResp *apimodels.CongestionResponse, issuer blockhandler.Account) (iotago.BlockID, error) { + congestionResp, issuerResp, version, err := e.accWallet.RequestBlockBuiltData(clt.Client(), issuer.ID()) + if err != nil { + return iotago.EmptyBlockID, ierrors.Wrapf(err, "failed to get block built data for issuer %s", issuer.ID().ToHex()) + } + blockID, err := e.accWallet.PostWithBlock(clt, payload, issuer, congestionResp, issuerResp, version) + if err != nil { + return iotago.EmptyBlockID, err + } + + return blockID, nil +} + // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// // region EvilWallet Faucet Requests /////////////////////////////////////////////////////////////////////////////////// @@ -169,6 +153,8 @@ func (e *EvilWallet) RequestFundsFromFaucet(options ...FaucetRequestOption) (ini e.aliasManager.AddInputAlias(output, buildOptions.outputAliasName) } + log.Debug("Funds requested succesfully") + return } @@ -205,8 +191,7 @@ func (e *EvilWallet) RequestFreshBigFaucetWallet() error { receiveWallet := e.NewWallet(Fresh) txIDs := make(iotago.TransactionIDs, 0) - // TODO: calculate the exact number of required funds so we don't run out after a while - for i := 0; i < 300; i++ { + for i := 0; i < 1; i++ { txID, err := e.requestAndSplitFaucetFunds(initWallet, receiveWallet) if err != nil { return ierrors.Wrap(err, "failed to request big funds from faucet") @@ -244,107 +229,73 @@ func (e *EvilWallet) requestAndSplitFaucetFunds(initWallet, receiveWallet *Walle if err != nil { return iotago.EmptyTransactionID, err } + + e.log.Debugf("Faucet funds received, continue spliting output: %s", splitOutput.OutputID.ToHex()) // first split 1 to FaucetRequestSplitNumber outputs return e.splitOutputs(splitOutput, initWallet, receiveWallet) } -func (e *EvilWallet) requestFaucetFunds(wallet *Wallet) (outputID *Output, err error) { +func (e *EvilWallet) requestFaucetFunds(wallet *Wallet) (outputID *models.Output, err error) { receiveAddr := wallet.AddressOnIndex(0) clt := e.connector.GetClient() - faucetAddr := e.faucet.AddressOnIndex(0) - unspentFaucet := e.faucet.UnspentOutput(faucetAddr.String()) - if unspentFaucet.OutputStruct == nil { - clt := e.connector.GetClient() - faucetOutput := clt.GetOutput(e.optFaucetUnspentOutputID) - if faucetOutput == nil { - panic("no valid faucet unspent output") - } - unspentFaucet.OutputStruct = faucetOutput - } - remainderAmount := unspentFaucet.Balance - faucetTokensPerRequest - - txBuilder := builder.NewTransactionBuilder(clt.CurrentAPI()) - - txBuilder.AddInput(&builder.TxInput{ - UnlockTarget: faucetAddr, - InputID: unspentFaucet.OutputID, - Input: unspentFaucet.OutputStruct, - }) - - // receiver output - txBuilder.AddOutput(&iotago.BasicOutput{ - Amount: faucetTokensPerRequest, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: receiveAddr}, - }, - }) - - // remainder output - txBuilder.AddOutput(&iotago.BasicOutput{ - Amount: remainderAmount, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: faucetAddr}, - }, - }) - - txBuilder.AddTaggedDataPayload(&iotago.TaggedData{Tag: []byte("faucet funds"), Data: []byte("to addr" + receiveAddr.String())}) - txBuilder.SetCreationSlot(clt.CurrentAPI().TimeProvider().SlotFromTime(time.Now())) - - signedTx, err := txBuilder.Build(e.faucet.AddressSigner(faucetAddr)) - if err != nil { - return nil, err - } - - // send transaction - _, err = clt.PostTransaction(signedTx) + output, err := e.accWallet.RequestFaucetFunds(clt, receiveAddr, faucetTokensPerRequest) if err != nil { - return nil, err + return nil, ierrors.Wrap(err, "failed to request funds from faucet") } - // requested output to split and use in spammer - output := e.outputManager.CreateOutputFromAddress(wallet, receiveAddr, faucetTokensPerRequest, iotago.OutputIDFromTransactionIDAndIndex(lo.PanicOnErr(signedTx.Transaction.ID()), 0), signedTx.Transaction.Outputs[0]) - - // set remainder output to be reused by the faucet wallet - e.faucet.AddUnspentOutput(&Output{ - OutputID: iotago.OutputIDFromTransactionIDAndIndex(lo.PanicOnErr(signedTx.Transaction.ID()), 1), - Address: faucetAddr, - Index: 0, - Balance: signedTx.Transaction.Outputs[1].BaseTokenAmount(), - OutputStruct: signedTx.Transaction.Outputs[1], - }) + // update wallet with newly created output + e.outputManager.createOutputFromAddress(wallet, receiveAddr, faucetTokensPerRequest, output.OutputID, output.OutputStruct) return output, nil } // splitOutputs splits faucet input to 100 outputs. -func (e *EvilWallet) splitOutputs(splitOutput *Output, inputWallet, outputWallet *Wallet) (iotago.TransactionID, error) { +func (e *EvilWallet) splitOutputs(splitOutput *models.Output, inputWallet, outputWallet *Wallet) (iotago.TransactionID, error) { if inputWallet.IsEmpty() { return iotago.EmptyTransactionID, ierrors.New("inputWallet is empty") } input, outputs := e.handleInputOutputDuringSplitOutputs(splitOutput, FaucetRequestSplitNumber, outputWallet) - signedTx, err := e.CreateTransaction(WithInputs(input), WithOutputs(outputs), WithIssuer(inputWallet), WithOutputWallet(outputWallet)) + faucetAccount, err := e.accWallet.GetAccount(accountwallet.FaucetAccountAlias) if err != nil { return iotago.EmptyTransactionID, err } + txData, err := e.CreateTransaction( + WithInputs(input), + WithOutputs(outputs), + WithInputWallet(inputWallet), + WithOutputWallet(outputWallet), + WithIssuanceStrategy(models.AllotmentStrategyAll, faucetAccount.Account.ID()), + ) - _, err = e.connector.GetClient().PostTransaction(signedTx) if err != nil { - fmt.Println(err) return iotago.EmptyTransactionID, err } - return lo.PanicOnErr(signedTx.Transaction.ID()), nil + _, err = e.PrepareAndPostBlock(e.connector.GetClient(), txData.Payload, txData.CongestionResponse, faucetAccount.Account) + if err != nil { + return iotago.TransactionID{}, err + } + + if txData.Payload.PayloadType() != iotago.PayloadSignedTransaction { + return iotago.EmptyTransactionID, ierrors.New("payload type is not signed transaction") + } + + txID := lo.PanicOnErr(txData.Payload.(*iotago.SignedTransaction).Transaction.ID()) + + e.log.Debugf("Splitting output %s finished with tx: %s", splitOutput.OutputID.ToHex(), txID.ToHex()) + + return txID, nil } -func (e *EvilWallet) handleInputOutputDuringSplitOutputs(splitOutput *Output, splitNumber int, receiveWallet *Wallet) (input *Output, outputs []*OutputOption) { +func (e *EvilWallet) handleInputOutputDuringSplitOutputs(splitOutput *models.Output, splitNumber int, receiveWallet *Wallet) (input *models.Output, outputs []*OutputOption) { input = splitOutput balances := SplitBalanceEqually(splitNumber, input.Balance) for _, bal := range balances { - outputs = append(outputs, &OutputOption{amount: bal, address: receiveWallet.Address()}) + outputs = append(outputs, &OutputOption{amount: bal, address: receiveWallet.Address(), outputType: iotago.OutputBasic}) } return @@ -364,57 +315,27 @@ func (e *EvilWallet) ClearAllAliases() { e.aliasManager.ClearAllAliases() } -func (e *EvilWallet) PrepareCustomConflicts(conflictsMaps []ConflictSlice) (conflictBatch [][]*iotago.SignedTransaction, err error) { +func (e *EvilWallet) PrepareCustomConflicts(conflictsMaps []ConflictSlice) (conflictBatch [][]*models.PayloadIssuanceData, err error) { for _, conflictMap := range conflictsMaps { - var txs []*iotago.SignedTransaction + var txsData []*models.PayloadIssuanceData for _, conflictOptions := range conflictMap { - tx, err2 := e.CreateTransaction(conflictOptions...) + txData, err2 := e.CreateTransaction(conflictOptions...) if err2 != nil { return nil, err2 } - txs = append(txs, tx) + txsData = append(txsData, txData) } - conflictBatch = append(conflictBatch, txs) + conflictBatch = append(conflictBatch, txsData) } - return -} - -// SendCustomConflicts sends transactions with the given conflictsMaps. -func (e *EvilWallet) SendCustomConflicts(conflictsMaps []ConflictSlice) (err error) { - conflictBatch, err := e.PrepareCustomConflicts(conflictsMaps) - if err != nil { - return err - } - for _, txs := range conflictBatch { - clients := e.connector.GetClients(len(txs)) - if len(txs) > len(clients) { - return ierrors.New("insufficient clients to send conflicts") - } - - // send transactions in parallel - wg := sync.WaitGroup{} - for i, tx := range txs { - wg.Add(1) - go func(clt Client, signedTx *iotago.SignedTransaction) { - defer wg.Done() - _, _ = clt.PostTransaction(signedTx) - }(clients[i], tx) - } - wg.Wait() - - // wait until transactions are solid - time.Sleep(WaitForTxSolid) - } - - return + return conflictBatch, nil } // CreateTransaction creates a transaction based on provided options. If no input wallet is provided, the next non-empty faucet wallet is used. // Inputs of the transaction are determined in three ways: // 1 - inputs are provided directly without associated alias, 2- alias is provided, and input is already stored in an alias manager, // 3 - alias is provided, and there are no inputs assigned in Alias manager, so aliases are assigned to next ready inputs from input wallet. -func (e *EvilWallet) CreateTransaction(options ...Option) (signedTx *iotago.SignedTransaction, err error) { +func (e *EvilWallet) CreateTransaction(options ...Option) (*models.PayloadIssuanceData, error) { buildOptions, err := NewOptions(options...) if err != nil { return nil, err @@ -445,22 +366,40 @@ func (e *EvilWallet) CreateTransaction(options ...Option) (signedTx *iotago.Sign } } - signedTx, err = e.makeTransaction(inputs, outputs, buildOptions.inputWallet) + var congestionResp *apimodels.CongestionResponse + // request congestion endpoint if allotment strategy configured + if buildOptions.allotmentStrategy == models.AllotmentStrategyMinCost { + congestionResp, err = e.connector.GetClient().GetCongestion(buildOptions.issuerAccountID) + if err != nil { + return nil, err + } + } + + signedTx, err := e.makeTransaction(inputs, outputs, buildOptions.inputWallet, congestionResp, buildOptions.allotmentStrategy, buildOptions.issuerAccountID) if err != nil { return nil, err } + txData := &models.PayloadIssuanceData{ + Payload: signedTx, + CongestionResponse: congestionResp, + } e.addOutputsToOutputManager(signedTx, buildOptions.outputWallet, tempWallet, tempAddresses) e.registerOutputAliases(signedTx, addrAliasMap) - return + return txData, nil } // 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 { + if o.UnlockConditionSet().Address() == nil { + continue + } + + // register UnlockConditionAddress only (skip account outputs) addr := o.UnlockConditionSet().Address().Address - out := &Output{ + out := &models.Output{ OutputID: iotago.OutputIDFromTransactionIDAndIndex(lo.PanicOnErr(signedTx.Transaction.ID()), uint16(idx)), Address: addr, Balance: o.BaseTokenAmount(), @@ -506,6 +445,9 @@ func (e *EvilWallet) registerOutputAliases(signedTx *iotago.SignedTransaction, a for idx := range signedTx.Transaction.Outputs { id := iotago.OutputIDFromTransactionIDAndIndex(lo.PanicOnErr(signedTx.Transaction.ID()), uint16(idx)) out := e.outputManager.GetOutput(id) + if out == nil { + continue + } // register output alias e.aliasManager.AddOutputAlias(out, addrAliasMap[out.Address.String()]) @@ -515,7 +457,7 @@ func (e *EvilWallet) registerOutputAliases(signedTx *iotago.SignedTransaction, a } } -func (e *EvilWallet) prepareInputs(buildOptions *Options) (inputs []*Output, err error) { +func (e *EvilWallet) prepareInputs(buildOptions *Options) (inputs []*models.Output, err error) { if buildOptions.areInputsProvidedWithoutAliases() { inputs = append(inputs, buildOptions.inputs...) @@ -547,7 +489,7 @@ func (e *EvilWallet) prepareOutputs(buildOptions *Options, tempWallet *Wallet) ( // matchInputsWithAliases gets input from the alias manager. if input was not assigned to an alias before, // it assigns a new Fresh faucet output. -func (e *EvilWallet) matchInputsWithAliases(buildOptions *Options) (inputs []*Output, err error) { +func (e *EvilWallet) matchInputsWithAliases(buildOptions *Options) (inputs []*models.Output, err error) { // get inputs by alias for inputAlias := range buildOptions.aliasInputs { in, ok := e.aliasManager.GetInput(inputAlias) @@ -616,12 +558,15 @@ func (e *EvilWallet) matchOutputsWithAliases(buildOptions *Options, tempWallet * tempAddresses[addr.String()] = types.Void } - outputs = append(outputs, &iotago.BasicOutput{ - Amount: output.BaseTokenAmount(), - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: addr}, - }, - }) + switch output.Type() { + case iotago.OutputBasic: + outputBuilder := builder.NewBasicOutputBuilder(addr, output.BaseTokenAmount()) + outputs = append(outputs, outputBuilder.MustBuild()) + case iotago.OutputAccount: + outputBuilder := builder.NewAccountOutputBuilder(addr, addr, output.BaseTokenAmount()) + outputs = append(outputs, outputBuilder.MustBuild()) + } + addrAliasMap[addr.String()] = alias } @@ -696,9 +641,16 @@ func (e *EvilWallet) updateOutputBalances(buildOptions *Options) (err error) { } balances := SplitBalanceEqually(len(buildOptions.outputs)+len(buildOptions.aliasOutputs), totalBalance) i := 0 - for out := range buildOptions.aliasOutputs { - buildOptions.aliasOutputs[out] = &iotago.BasicOutput{ - Amount: balances[i], + for out, output := range buildOptions.aliasOutputs { + switch output.Type() { + case iotago.OutputBasic: + buildOptions.aliasOutputs[out] = &iotago.BasicOutput{ + Amount: balances[i], + } + case iotago.OutputAccount: + buildOptions.aliasOutputs[out] = &iotago.AccountOutput{ + Amount: balances[i], + } } i++ } @@ -707,10 +659,13 @@ func (e *EvilWallet) updateOutputBalances(buildOptions *Options) (err error) { return } -func (e *EvilWallet) makeTransaction(inputs []*Output, outputs iotago.Outputs[iotago.Output], w *Wallet) (tx *iotago.SignedTransaction, err error) { +func (e *EvilWallet) makeTransaction(inputs []*models.Output, outputs iotago.Outputs[iotago.Output], w *Wallet, congestionResponse *apimodels.CongestionResponse, allotmentStrategy models.AllotmentStrategy, issuerAccountID iotago.AccountID) (tx *iotago.SignedTransaction, err error) { clt := e.Connector().GetClient() + currentTime := time.Now() + targetSlot := clt.LatestAPI().TimeProvider().SlotFromTime(currentTime) + targetAPI := clt.APIForSlot(targetSlot) - txBuilder := builder.NewTransactionBuilder(clt.CurrentAPI()) + txBuilder := builder.NewTransactionBuilder(targetAPI) for _, input := range inputs { txBuilder.AddInput(&builder.TxInput{UnlockTarget: input.Address, InputID: input.OutputID, Input: input.OutputStruct}) @@ -736,23 +691,55 @@ func (e *EvilWallet) makeTransaction(inputs []*Output, outputs iotago.Outputs[io inputPrivateKey, _ := wallet.KeyPair(index) walletKeys[i] = iotago.AddressKeys{Address: addr, Keys: inputPrivateKey} } - txBuilder.SetCreationSlot(clt.CurrentAPI().TimeProvider().SlotFromTime(time.Now())) + + txBuilder.SetCreationSlot(targetSlot) + // no allotment strategy + if congestionResponse == nil { + return txBuilder.Build(iotago.NewInMemoryAddressSigner(walletKeys...)) + } + switch allotmentStrategy { + case models.AllotmentStrategyAll: + txBuilder.AllotAllMana(targetSlot, issuerAccountID) + case models.AllotmentStrategyMinCost: + txBuilder.AllotRequiredManaAndStoreRemainingManaInOutput(targetSlot, congestionResponse.ReferenceManaCost, issuerAccountID, 0) + } return txBuilder.Build(iotago.NewInMemoryAddressSigner(walletKeys...)) } -func (e *EvilWallet) PrepareCustomConflictsSpam(scenario *EvilScenario) (signedTxs [][]*iotago.SignedTransaction, allAliases ScenarioAlias, err error) { - conflicts, allAliases := e.prepareConflictSliceForScenario(scenario) - signedTxs, err = e.PrepareCustomConflicts(conflicts) +func (e *EvilWallet) PrepareCustomConflictsSpam(scenario *EvilScenario, strategy *models.IssuancePaymentStrategy) (txsData [][]*models.PayloadIssuanceData, allAliases ScenarioAlias, err error) { + conflicts, allAliases := e.prepareConflictSliceForScenario(scenario, strategy) + txsData, err = e.PrepareCustomConflicts(conflicts) - return + return txsData, allAliases, err } -func (e *EvilWallet) prepareConflictSliceForScenario(scenario *EvilScenario) (conflictSlice []ConflictSlice, allAliases ScenarioAlias) { +func (e *EvilWallet) PrepareAccountSpam(scenario *EvilScenario, strategy *models.IssuancePaymentStrategy) (*models.PayloadIssuanceData, ScenarioAlias, error) { + accountSpamOptions, allAliases := e.prepareFlatOptionsForAccountScenario(scenario, strategy) + + txData, err := e.CreateTransaction(accountSpamOptions...) + + return txData, allAliases, err +} + +func (e *EvilWallet) evaluateIssuanceStrategy(strategy *models.IssuancePaymentStrategy) (models.AllotmentStrategy, iotago.AccountID) { + var issuerAccountID iotago.AccountID + if strategy.AllotmentStrategy != models.AllotmentStrategyNone { + // get issuer accountID + accData, err := e.accWallet.GetAccount(strategy.IssuerAlias) + if err != nil { + panic("could not get issuer accountID while preparing conflicts") + } + issuerAccountID = accData.Account.ID() + } + return strategy.AllotmentStrategy, issuerAccountID +} + +func (e *EvilWallet) prepareConflictSliceForScenario(scenario *EvilScenario, strategy *models.IssuancePaymentStrategy) (conflictSlice []ConflictSlice, allAliases ScenarioAlias) { genOutputOptions := func(aliases []string) []*OutputOption { outputOptions := make([]*OutputOption, 0) for _, o := range aliases { - outputOptions = append(outputOptions, &OutputOption{aliasName: o}) + outputOptions = append(outputOptions, &OutputOption{aliasName: o, outputType: iotago.OutputBasic}) } return outputOptions @@ -770,11 +757,12 @@ func (e *EvilWallet) prepareConflictSliceForScenario(scenario *EvilScenario) (co option = append(option, WithOutputWallet(scenario.OutputWallet)) } if scenario.RestrictedInputWallet != nil { - option = append(option, WithIssuer(scenario.RestrictedInputWallet)) + option = append(option, WithInputWallet(scenario.RestrictedInputWallet)) } if scenario.Reuse { option = append(option, WithReuseOutputs()) } + option = append(option, WithIssuanceStrategy(e.evaluateIssuanceStrategy(strategy))) conflicts = append(conflicts, option) } conflictSlice = append(conflictSlice, conflicts) @@ -783,6 +771,38 @@ func (e *EvilWallet) prepareConflictSliceForScenario(scenario *EvilScenario) (co return } +func (e *EvilWallet) prepareFlatOptionsForAccountScenario(scenario *EvilScenario, strategy *models.IssuancePaymentStrategy) ([]Option, ScenarioAlias) { + // we do not care about batchedOutputs, because we do not support saving account spam result in evil wallet for now + prefixedBatch, allAliases, _ := scenario.ConflictBatchWithPrefix() + if len(prefixedBatch) != 1 { + panic("invalid scenario, cannot prepare flat option structure with deep scenario, EvilBatch should have only one element") + } + evilBatch := prefixedBatch[0] + if len(evilBatch) != 1 { + panic("invalid scenario, cannot prepare flat option structure with deep scenario, EvilBatch should have only one element") + } + + genOutputOptions := func(aliases []string) []*OutputOption { + outputOptions := make([]*OutputOption, 0) + for _, o := range aliases { + outputOptions = append(outputOptions, &OutputOption{ + aliasName: o, + outputType: iotago.OutputAccount, + }) + } + + return outputOptions + } + scenarioAlias := evilBatch[0] + outs := genOutputOptions(scenarioAlias.Outputs) + + return []Option{ + WithInputs(scenarioAlias.Inputs), + WithOutputs(outs), + WithIssuanceStrategy(e.evaluateIssuanceStrategy(strategy)), + }, allAliases +} + // AwaitInputsSolidity waits for all inputs to be solid for client clt. // func (e *EvilWallet) AwaitInputsSolidity(inputs devnetvm.Inputs, clt Client) (allSolid bool) { // awaitSolid := make([]string, 0) @@ -813,20 +833,14 @@ func (e *EvilWallet) SetTxOutputsSolid(outputs iotago.OutputIDs, clientID string // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// -func WithFaucetSeed(seed []byte) options.Option[EvilWallet] { - return func(opts *EvilWallet) { - copy(opts.optFaucetSeed[:], seed[:]) - } -} - -func WithFaucetOutputID(id iotago.OutputID) options.Option[EvilWallet] { +func WithClients(urls ...string) options.Option[EvilWallet] { return func(opts *EvilWallet) { - opts.optFaucetUnspentOutputID = id + opts.optsClientURLs = urls } } -func WithClients(urls ...string) options.Option[EvilWallet] { +func WithAccountsWallet(wallet *accountwallet.AccountWallet) options.Option[EvilWallet] { return func(opts *EvilWallet) { - opts.optsClientURLs = urls + opts.accWallet = wallet } } diff --git a/tools/evil-spammer/wallet/options.go b/tools/evil-spammer/evilwallet/options.go similarity index 79% rename from tools/evil-spammer/wallet/options.go rename to tools/evil-spammer/evilwallet/options.go index a59259d2b..7b0b581f8 100644 --- a/tools/evil-spammer/wallet/options.go +++ b/tools/evil-spammer/evilwallet/options.go @@ -1,12 +1,13 @@ -package wallet +package evilwallet import ( - "fmt" "time" "github.com/iotaledger/hive.go/ds/types" "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/builder" ) // region Options /////////////////////////////////////////////////////////////////////////// @@ -14,7 +15,7 @@ import ( // Options is a struct that represents a collection of options that can be set when creating a block. type Options struct { aliasInputs map[string]types.Empty - inputs []*Output + inputs []*models.Output aliasOutputs map[string]iotago.Output outputs []iotago.Output inputWallet *Wallet @@ -22,21 +23,27 @@ type Options struct { outputBatchAliases map[string]types.Empty reuse bool issuingTime time.Time + allotmentStrategy models.AllotmentStrategy + issuerAccountID iotago.AccountID + // maps input alias to desired output type, used to create account output types + specialOutputTypes map[string]iotago.OutputType } type OutputOption struct { - aliasName string - amount iotago.BaseToken - address *iotago.Ed25519Address + aliasName string + amount iotago.BaseToken + address *iotago.Ed25519Address + outputType iotago.OutputType } // NewOptions is the constructor for the tx creation. func NewOptions(options ...Option) (option *Options, err error) { option = &Options{ - aliasInputs: make(map[string]types.Empty), - inputs: make([]*Output, 0), - aliasOutputs: make(map[string]iotago.Output), - outputs: make([]iotago.Output, 0), + aliasInputs: make(map[string]types.Empty), + inputs: make([]*models.Output, 0), + aliasOutputs: make(map[string]iotago.Output), + outputs: make([]iotago.Output, 0), + specialOutputTypes: make(map[string]iotago.OutputType), } for _, opt := range options { @@ -127,66 +134,46 @@ func WithInputs(inputs interface{}) Option { for _, input := range in { options.aliasInputs[input] = types.Void } - case *Output: + case *models.Output: options.inputs = append(options.inputs, in) - case []*Output: + case []*models.Output: options.inputs = append(options.inputs, in...) } } } -// WithOutput returns an Option that is used to define a non-colored Output for the Transaction in the Block. -func WithOutput(output *OutputOption) Option { +// WithOutputs returns an Option that is used to define a non-colored Outputs for the Transaction in the Block. +func WithOutputs(outputsOptions []*OutputOption) Option { return func(options *Options) { - if output.amount == 0 || output.address == nil { - fmt.Println("output invalid") - return - } + for _, outputOptions := range outputsOptions { + var output iotago.Output + switch outputOptions.outputType { + case iotago.OutputBasic: + outputBuilder := builder.NewBasicOutputBuilder(outputOptions.address, outputOptions.amount) + output = outputBuilder.MustBuild() + case iotago.OutputAccount: + outputBuilder := builder.NewAccountOutputBuilder(outputOptions.address, outputOptions.address, outputOptions.amount) + output = outputBuilder.MustBuild() + } - if output.aliasName != "" { - fmt.Println(output.aliasName) - options.aliasOutputs[output.aliasName] = &iotago.BasicOutput{ - Amount: output.amount, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: output.address}, - }, + if outputOptions.aliasName != "" { + options.aliasOutputs[outputOptions.aliasName] = output + } else { + options.outputs = append(options.outputs, output) } - } else { - options.outputs = append(options.outputs, &iotago.BasicOutput{ - Amount: output.amount, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: output.address}, - }, - }) } } } -// WithOutputs returns an Option that is used to define a non-colored Outputs for the Transaction in the Block. -func WithOutputs(outputs []*OutputOption) Option { +func WithIssuanceStrategy(strategy models.AllotmentStrategy, issuerID iotago.AccountID) Option { return func(options *Options) { - for _, output := range outputs { - if output.aliasName != "" { - options.aliasOutputs[output.aliasName] = &iotago.BasicOutput{ - Amount: output.amount, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: output.address}, - }, - } - } else { - options.outputs = append(options.outputs, &iotago.BasicOutput{ - Amount: output.amount, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: output.address}, - }, - }) - } - } + options.allotmentStrategy = strategy + options.issuerAccountID = issuerID } } -// WithIssuer returns a BlockOption that is used to define the inputWallet of the Block. -func WithIssuer(issuer *Wallet) Option { +// WithInputWallet returns a BlockOption that is used to define the inputWallet of the Block. +func WithInputWallet(issuer *Wallet) Option { return func(options *Options) { options.inputWallet = issuer } @@ -299,4 +286,10 @@ func WithScenarioInputWalletForDeepSpam(wallet *Wallet) ScenarioOption { } } +func WithCreateAccounts() ScenarioOption { + return func(options *EvilScenario) { + options.OutputType = iotago.OutputAccount + } +} + // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tools/evil-spammer/wallet/output_manager.go b/tools/evil-spammer/evilwallet/output_manager.go similarity index 88% rename from tools/evil-spammer/wallet/output_manager.go rename to tools/evil-spammer/evilwallet/output_manager.go index 559c8521f..e86ac7448 100644 --- a/tools/evil-spammer/wallet/output_manager.go +++ b/tools/evil-spammer/evilwallet/output_manager.go @@ -1,4 +1,4 @@ -package wallet +package evilwallet import ( "sync" @@ -7,36 +7,19 @@ import ( "github.com/iotaledger/hive.go/ds/types" "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/logger" "github.com/iotaledger/hive.go/runtime/syncutils" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" iotago "github.com/iotaledger/iota.go/v4" ) -var ( - awaitOutputToBeConfirmed = 150 * time.Second +const ( + awaitOutputToBeConfirmed = 10 * time.Second ) -// Input contains details of an input. -type Input struct { - OutputID iotago.OutputID - Address iotago.Address -} - -// Output contains details of an output ID. -type Output struct { - OutputID iotago.OutputID - Address iotago.Address - Index uint64 - Balance iotago.BaseToken - - OutputStruct iotago.Output -} - -// Outputs is a list of Output. -type Outputs []*Output - // OutputManager keeps track of the output statuses. type OutputManager struct { - connector Connector + connector models.Connector wallets *Wallets outputIDWalletMap map[string]*Wallet @@ -44,17 +27,20 @@ type OutputManager struct { // stores solid outputs per node issuerSolidOutIDMap map[string]map[iotago.OutputID]types.Empty + log *logger.Logger + syncutils.RWMutex } // NewOutputManager creates an OutputManager instance. -func NewOutputManager(connector Connector, wallets *Wallets) *OutputManager { +func NewOutputManager(connector models.Connector, wallets *Wallets, log *logger.Logger) *OutputManager { return &OutputManager{ connector: connector, wallets: wallets, outputIDWalletMap: make(map[string]*Wallet), outputIDAddrMap: make(map[string]string), issuerSolidOutIDMap: make(map[string]map[iotago.OutputID]types.Empty), + log: log, } } @@ -140,11 +126,11 @@ func (o *OutputManager) Track(outputIDs ...iotago.OutputID) (allConfirmed bool) return !unconfirmedOutputFound.Load() } -// CreateOutputFromAddress creates output, retrieves outputID, and adds it to the wallet. +// createOutputFromAddress creates output, retrieves outputID, and adds it to the wallet. // Provided address should be generated from provided wallet. Considers only first output found on address. -func (o *OutputManager) CreateOutputFromAddress(w *Wallet, addr *iotago.Ed25519Address, balance iotago.BaseToken, outputID iotago.OutputID, outputStruct iotago.Output) *Output { +func (o *OutputManager) createOutputFromAddress(w *Wallet, addr *iotago.Ed25519Address, balance iotago.BaseToken, outputID iotago.OutputID, outputStruct iotago.Output) *models.Output { index := w.AddrIndexMap(addr.String()) - out := &Output{ + out := &models.Output{ Address: addr, Index: index, OutputID: outputID, @@ -159,9 +145,9 @@ func (o *OutputManager) CreateOutputFromAddress(w *Wallet, addr *iotago.Ed25519A } // AddOutput adds existing output from wallet w to the OutputManager. -func (o *OutputManager) AddOutput(w *Wallet, output *Output) *Output { +func (o *OutputManager) AddOutput(w *Wallet, output *models.Output) *models.Output { idx := w.AddrIndexMap(output.Address.String()) - out := &Output{ + out := &models.Output{ Address: output.Address, Index: idx, OutputID: output.OutputID, @@ -177,7 +163,7 @@ func (o *OutputManager) AddOutput(w *Wallet, output *Output) *Output { // GetOutput returns the Output of the given outputID. // Firstly checks if output can be retrieved by outputManager from wallet, if not does an API call. -func (o *OutputManager) GetOutput(outputID iotago.OutputID) (output *Output) { +func (o *OutputManager) GetOutput(outputID iotago.OutputID) (output *models.Output) { output = o.getOutputFromWallet(outputID) // get output info via web api @@ -193,7 +179,7 @@ func (o *OutputManager) GetOutput(outputID iotago.OutputID) (output *Output) { return nil } - output = &Output{ + output = &models.Output{ OutputID: outputID, Address: basicOutput.UnlockConditionSet().Address().Address, Balance: basicOutput.BaseTokenAmount(), @@ -204,7 +190,7 @@ func (o *OutputManager) GetOutput(outputID iotago.OutputID) (output *Output) { return output } -func (o *OutputManager) getOutputFromWallet(outputID iotago.OutputID) (output *Output) { +func (o *OutputManager) getOutputFromWallet(outputID iotago.OutputID) (output *models.Output) { o.RLock() defer o.RUnlock() w, ok := o.outputIDWalletMap[outputID.ToHex()] @@ -289,6 +275,8 @@ func (o *OutputManager) AwaitTransactionsConfirmation(txIDs ...iotago.Transactio wg := sync.WaitGroup{} semaphore := make(chan bool, 1) + o.log.Debugf("Awaiting confirmation of %d transactions", len(txIDs)) + for _, txID := range txIDs { wg.Add(1) go func(txID iotago.TransactionID) { @@ -312,8 +300,9 @@ func (o *OutputManager) AwaitTransactionToBeAccepted(txID iotago.TransactionID, clt := o.connector.GetClient() var accepted bool for ; time.Since(s) < waitFor; time.Sleep(awaitConfirmationSleep) { - // TODO: need to change to pending for now - if confirmationState := clt.GetTransactionConfirmationState(txID); confirmationState == "confirmed" || confirmationState == "finalized" { + confirmationState := clt.GetTransactionConfirmationState(txID) + o.log.Debugf("Tx %s confirmationState: %s", txID.ToHex(), confirmationState) + if confirmationState == "confirmed" || confirmationState == "finalized" { accepted = true break } @@ -322,11 +311,13 @@ func (o *OutputManager) AwaitTransactionToBeAccepted(txID iotago.TransactionID, return ierrors.Errorf("transaction %s not accepted in time", txID) } + o.log.Debugf("Transaction %s accepted", txID) + return nil } // AwaitOutputToBeSolid awaits for solidification of a single output by provided clt. -func (o *OutputManager) AwaitOutputToBeSolid(outID iotago.OutputID, clt Client, waitFor time.Duration) error { +func (o *OutputManager) AwaitOutputToBeSolid(outID iotago.OutputID, clt models.Client, waitFor time.Duration) error { s := time.Now() var solid bool @@ -350,7 +341,7 @@ func (o *OutputManager) AwaitOutputToBeSolid(outID iotago.OutputID, clt Client, } // AwaitOutputsToBeSolid awaits for all provided outputs are solid for a provided client. -func (o *OutputManager) AwaitOutputsToBeSolid(outputs iotago.OutputIDs, clt Client, maxGoroutines int) (allSolid bool) { +func (o *OutputManager) AwaitOutputsToBeSolid(outputs iotago.OutputIDs, clt models.Client, maxGoroutines int) (allSolid bool) { wg := sync.WaitGroup{} semaphore := make(chan bool, maxGoroutines) allSolid = true diff --git a/tools/evil-spammer/wallet/utils.go b/tools/evil-spammer/evilwallet/utils.go similarity index 98% rename from tools/evil-spammer/wallet/utils.go rename to tools/evil-spammer/evilwallet/utils.go index eb080d749..733d86987 100644 --- a/tools/evil-spammer/wallet/utils.go +++ b/tools/evil-spammer/evilwallet/utils.go @@ -1,4 +1,4 @@ -package wallet +package evilwallet import iotago "github.com/iotaledger/iota.go/v4" diff --git a/tools/evil-spammer/wallet/wallet.go b/tools/evil-spammer/evilwallet/wallet.go similarity index 91% rename from tools/evil-spammer/wallet/wallet.go rename to tools/evil-spammer/evilwallet/wallet.go index c1c024a81..5b97d86f8 100644 --- a/tools/evil-spammer/wallet/wallet.go +++ b/tools/evil-spammer/evilwallet/wallet.go @@ -1,4 +1,4 @@ -package wallet +package evilwallet import ( "crypto/ed25519" @@ -8,6 +8,7 @@ import ( "github.com/iotaledger/hive.go/ds/types" "github.com/iotaledger/hive.go/runtime/syncutils" "github.com/iotaledger/iota-core/pkg/testsuite/mock" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" iotago "github.com/iotaledger/iota.go/v4" "github.com/iotaledger/iota.go/v4/tpkg" ) @@ -18,7 +19,7 @@ import ( type Wallet struct { ID walletID walletType WalletType - unspentOutputs map[string]*Output // maps addr to its unspentOutput + unspentOutputs map[string]*models.Output // maps addr to its unspentOutput indexAddrMap map[uint64]string addrIndexMap map[string]uint64 inputTransactions map[string]types.Empty @@ -44,7 +45,7 @@ func NewWallet(wType ...WalletType) *Wallet { walletType: walletType, ID: -1, seed: tpkg.RandEd25519Seed(), - unspentOutputs: make(map[string]*Output), + unspentOutputs: make(map[string]*models.Output), indexAddrMap: make(map[uint64]string), addrIndexMap: make(map[string]uint64), inputTransactions: make(map[string]types.Empty), @@ -80,7 +81,7 @@ func (w *Wallet) Address() *iotago.Ed25519Address { return addr } -// Address returns a new and unused address of a given wallet. +// AddressOnIndex returns a new and unused address of a given wallet. func (w *Wallet) AddressOnIndex(index uint64) *iotago.Ed25519Address { w.Lock() defer w.Unlock() @@ -93,7 +94,7 @@ func (w *Wallet) AddressOnIndex(index uint64) *iotago.Ed25519Address { } // UnspentOutput returns the unspent output on the address. -func (w *Wallet) UnspentOutput(addr string) *Output { +func (w *Wallet) UnspentOutput(addr string) *models.Output { w.RLock() defer w.RUnlock() @@ -101,10 +102,10 @@ func (w *Wallet) UnspentOutput(addr string) *Output { } // UnspentOutputs returns all unspent outputs on the wallet. -func (w *Wallet) UnspentOutputs() (outputs map[string]*Output) { +func (w *Wallet) UnspentOutputs() (outputs map[string]*models.Output) { w.RLock() defer w.RUnlock() - outputs = make(map[string]*Output) + outputs = make(map[string]*models.Output) for addr, outs := range w.unspentOutputs { outputs[addr] = outs } @@ -129,7 +130,7 @@ func (w *Wallet) AddrIndexMap(address string) uint64 { } // AddUnspentOutput adds an unspentOutput of a given wallet. -func (w *Wallet) AddUnspentOutput(output *Output) { +func (w *Wallet) AddUnspentOutput(output *models.Output) { w.Lock() defer w.Unlock() w.unspentOutputs[output.Address.String()] = output @@ -200,7 +201,7 @@ func (w *Wallet) GetReuseAddress() string { } // GetUnspentOutput returns an unspent output on the oldest address ordered by index. -func (w *Wallet) GetUnspentOutput() *Output { +func (w *Wallet) GetUnspentOutput() *models.Output { switch w.walletType { case Reuse: addr := w.GetReuseAddress() diff --git a/tools/evil-spammer/wallet/wallets.go b/tools/evil-spammer/evilwallet/wallets.go similarity index 97% rename from tools/evil-spammer/wallet/wallets.go rename to tools/evil-spammer/evilwallet/wallets.go index bf9fe191c..31e5c0366 100644 --- a/tools/evil-spammer/wallet/wallets.go +++ b/tools/evil-spammer/evilwallet/wallets.go @@ -1,10 +1,11 @@ -package wallet +package evilwallet import ( "go.uber.org/atomic" "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/runtime/syncutils" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" ) type walletID int @@ -146,7 +147,7 @@ func (w *Wallets) addReuseWallet(wallet *Wallet) { } // GetUnspentOutput gets first found unspent output for a given walletType. -func (w *Wallets) GetUnspentOutput(wallet *Wallet) *Output { +func (w *Wallets) GetUnspentOutput(wallet *Wallet) *models.Output { if wallet == nil { return nil } diff --git a/tools/evil-spammer/go.mod b/tools/evil-spammer/go.mod index 2381fe759..30cc8c60c 100644 --- a/tools/evil-spammer/go.mod +++ b/tools/evil-spammer/go.mod @@ -8,7 +8,9 @@ replace github.com/iotaledger/iota-core/tools/genesis-snapshot => ../genesis-sna require ( github.com/AlecAivazis/survey/v2 v2.3.7 + github.com/ethereum/go-ethereum v1.13.2 github.com/iotaledger/hive.go/app v0.0.0-20231010133617-cdbd5387e2af + github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231010133617-cdbd5387e2af github.com/iotaledger/hive.go/crypto v0.0.0-20231010133617-cdbd5387e2af github.com/iotaledger/hive.go/ds v0.0.0-20231010133617-cdbd5387e2af github.com/iotaledger/hive.go/ierrors v0.0.0-20231010133617-cdbd5387e2af @@ -17,7 +19,7 @@ require ( github.com/iotaledger/hive.go/runtime v0.0.0-20231010133617-cdbd5387e2af github.com/iotaledger/iota-core v0.0.0-00010101000000-000000000000 github.com/iotaledger/iota-core/tools/genesis-snapshot v0.0.0-00010101000000-000000000000 - github.com/iotaledger/iota.go/v4 v4.0.0-20231011161154-7004432004e1 + github.com/iotaledger/iota.go/v4 v4.0.0-20231013092100-ad2a52b5ac9a github.com/mr-tron/base58 v1.2.0 go.uber.org/atomic v1.11.0 ) @@ -28,9 +30,9 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect - github.com/ethereum/go-ethereum v1.13.2 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-stack/stack v1.8.1 // indirect github.com/google/uuid v1.3.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/holiman/uint256 v1.2.3 // indirect @@ -38,7 +40,6 @@ require ( github.com/iotaledger/grocksdb v1.7.5-0.20230220105546-5162e18885c7 // indirect github.com/iotaledger/hive.go/ads v0.0.0-20231010133617-cdbd5387e2af // indirect github.com/iotaledger/hive.go/constraints v0.0.0-20231010133617-cdbd5387e2af // indirect - github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231010133617-cdbd5387e2af // indirect github.com/iotaledger/hive.go/kvstore v0.0.0-20231010133617-cdbd5387e2af // indirect github.com/iotaledger/hive.go/log v0.0.0-20231010133617-cdbd5387e2af // indirect github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231010133617-cdbd5387e2af // indirect diff --git a/tools/evil-spammer/go.sum b/tools/evil-spammer/go.sum index c5109f6b5..db0f63d48 100644 --- a/tools/evil-spammer/go.sum +++ b/tools/evil-spammer/go.sum @@ -82,6 +82,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -197,8 +199,8 @@ github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231010133617-cdbd538 github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231010133617-cdbd5387e2af/go.mod h1:IJgaaxbgKCsNat18jlJJEAxCY2oVYR3F30B+M4vJ89I= github.com/iotaledger/hive.go/stringify v0.0.0-20231010133617-cdbd5387e2af h1:2/8In9gw03NW1hL4qyXuFYFoWZScHmyZtYUG0kHPmo4= github.com/iotaledger/hive.go/stringify v0.0.0-20231010133617-cdbd5387e2af/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= -github.com/iotaledger/iota.go/v4 v4.0.0-20231011161154-7004432004e1 h1:mz5E00q1U/LDiUi/wVAbwdhGNKX0dNThaO99Fsyjkgs= -github.com/iotaledger/iota.go/v4 v4.0.0-20231011161154-7004432004e1/go.mod h1:XmgOVYZ7805zVEYPwhvqBDVa7XieXRgPrCEGZW35W8k= +github.com/iotaledger/iota.go/v4 v4.0.0-20231013092100-ad2a52b5ac9a h1:S/n3ZTjnnl0IIMCx+S0pu5CZNArO6Z+omiXt6dDmPK4= +github.com/iotaledger/iota.go/v4 v4.0.0-20231013092100-ad2a52b5ac9a/go.mod h1:XmgOVYZ7805zVEYPwhvqBDVa7XieXRgPrCEGZW35W8k= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= diff --git a/tools/evil-spammer/interactive.go b/tools/evil-spammer/interactive/interactive.go similarity index 94% rename from tools/evil-spammer/interactive.go rename to tools/evil-spammer/interactive/interactive.go index 3bd43c694..620f1c9b7 100644 --- a/tools/evil-spammer/interactive.go +++ b/tools/evil-spammer/interactive/interactive.go @@ -1,4 +1,4 @@ -package main +package interactive import ( "encoding/json" @@ -15,8 +15,9 @@ import ( "github.com/iotaledger/hive.go/ds/types" "github.com/iotaledger/hive.go/runtime/syncutils" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/programs" "github.com/iotaledger/iota-core/tools/evil-spammer/spammer" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" "github.com/iotaledger/iota.go/v4/nodeclient" ) @@ -25,17 +26,12 @@ const ( maxConcurrentSpams = 5 lastSpamsShowed = 15 timeFormat = "2006/01/02 15:04:05" + configFilename = "interactive_config.json" ) const ( AnswerEnable = "enable" AnswerDisable = "disable" - - SpammerTypeBlock = "blk" - SpammerTypeTx = "tx" - SpammerTypeDs = "ds" - SpammerTypeCustom = "custom" - SpammerTypeCommitments = "commitments" ) var ( @@ -73,7 +69,7 @@ var configJSON = fmt.Sprintf(`{ "autoRequestingEnabled": false, "autoRequestingAmount": "100", "useRateSetter": true -}`, SpammerTypeTx) +}`, spammer.TypeTx) var defaultConfig = InteractiveConfig{ clientURLs: map[string]types.Empty{ @@ -85,7 +81,7 @@ var defaultConfig = InteractiveConfig{ timeUnit: time.Second, Deep: false, Reuse: true, - Scenario: SpammerTypeTx, + Scenario: spammer.TypeTx, AutoRequesting: false, AutoRequestingAmount: "100", UseRateSetter: true, @@ -143,7 +139,7 @@ const ( ) var ( - scenarios = []string{SpammerTypeBlock, SpammerTypeTx, SpammerTypeDs, "conflict-circle", "guava", "orange", "mango", "pear", "lemon", "banana", "kiwi", "peace"} + scenarios = []string{spammer.TypeBlock, spammer.TypeTx, spammer.TypeDs, "conflict-circle", "guava", "orange", "mango", "pear", "lemon", "banana", "kiwi", "peace"} confirms = []string{AnswerEnable, AnswerDisable} outputNumbers = []string{"100", "1000", "5000", "cancel"} timeUnits = []string{mpm, mps} @@ -196,7 +192,7 @@ func configure(mode *Mode) { // region Mode ///////////////////////////////////////////////////////////////////////////////////////////////////////// type Mode struct { - evilWallet *wallet.EvilWallet + evilWallet *evilwallet.EvilWallet shutdown chan types.Empty mainMenu chan types.Empty spamFinished chan int @@ -220,7 +216,7 @@ type Mode struct { func NewInteractiveMode() *Mode { return &Mode{ - evilWallet: wallet.NewEvilWallet(), + evilWallet: evilwallet.NewEvilWallet(), action: make(chan action), shutdown: make(chan types.Empty), mainMenu: make(chan types.Empty), @@ -303,7 +299,7 @@ func (m *Mode) onMenuAction() { } func (m *Mode) prepareFundsIfNeeded() { - if m.evilWallet.UnspentOutputsLeft(wallet.Fresh) < minSpamOutputs { + if m.evilWallet.UnspentOutputsLeft(evilwallet.Fresh) < minSpamOutputs { if !m.preparingFunds && m.Config.AutoRequesting { m.preparingFunds = true go func() { @@ -441,7 +437,7 @@ func (m *Mode) areEnoughFundsAvailable() bool { outputsNeeded = int(float64(m.Config.Rate) * m.Config.duration.Minutes()) } - return m.evilWallet.UnspentOutputsLeft(wallet.Fresh) < outputsNeeded && m.Config.Scenario != SpammerTypeBlock + return m.evilWallet.UnspentOutputsLeft(evilwallet.Fresh) < outputsNeeded && m.Config.Scenario != spammer.TypeBlock } func (m *Mode) startSpam() { @@ -449,11 +445,11 @@ func (m *Mode) startSpam() { defer m.spamMutex.Unlock() var s *spammer.Spammer - if m.Config.Scenario == SpammerTypeBlock { - s = SpamBlocks(m.evilWallet, m.Config.Rate, time.Second, m.Config.duration, 0, m.Config.UseRateSetter) + if m.Config.Scenario == spammer.TypeBlock { + s = programs.SpamBlocks(m.evilWallet, m.Config.Rate, time.Second, m.Config.duration, 0, m.Config.UseRateSetter, "") } else { - scenario, _ := wallet.GetScenario(m.Config.Scenario) - s = SpamNestedConflicts(m.evilWallet, m.Config.Rate, time.Second, m.Config.duration, scenario, m.Config.Deep, m.Config.Reuse, m.Config.UseRateSetter) + 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, "") if s == nil { return } @@ -698,7 +694,7 @@ func (m *Mode) summarizeSpam(id int) { func (m *Mode) updateSentStatistic(s *spammer.Spammer, id int) { blkSent := s.BlocksSent() scenariosCreated := s.BatchesPrepared() - if m.spammerLog.SpamDetails(id).Scenario == SpammerTypeBlock { + if m.spammerLog.SpamDetails(id).Scenario == spammer.TypeBlock { m.blkSent.Add(blkSent) } else { m.txSent.Add(blkSent) @@ -709,7 +705,7 @@ func (m *Mode) updateSentStatistic(s *spammer.Spammer, id int) { // load the config file. func (m *Mode) loadConfig() { // open config file - file, err := os.Open("config.json") + file, err := os.Open(configFilename) if err != nil { if !os.IsNotExist(err) { panic(err) diff --git a/tools/evil-spammer/menu.go b/tools/evil-spammer/interactive/menu.go similarity index 98% rename from tools/evil-spammer/menu.go rename to tools/evil-spammer/interactive/menu.go index f9b478b85..3f91c8c00 100644 --- a/tools/evil-spammer/menu.go +++ b/tools/evil-spammer/interactive/menu.go @@ -1,11 +1,11 @@ -package main +package interactive import ( "fmt" "os" "time" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" ) // region Printer ///////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -63,8 +63,8 @@ func (p *Printer) printBanner() { func (p *Printer) EvilWalletStatus() { p.PrintTopLine() p.Println(p.colorString("Evil Wallet status:", "cyan"), level2) - p.PrintlnPoint(fmt.Sprintf("Available faucet outputs: %d", p.mode.evilWallet.UnspentOutputsLeft(wallet.Fresh)), level2) - p.PrintlnPoint(fmt.Sprintf("Available reuse outputs: %d", p.mode.evilWallet.UnspentOutputsLeft(wallet.Reuse)), level2) + p.PrintlnPoint(fmt.Sprintf("Available faucet outputs: %d", p.mode.evilWallet.UnspentOutputsLeft(evilwallet.Fresh)), level2) + p.PrintlnPoint(fmt.Sprintf("Available reuse outputs: %d", p.mode.evilWallet.UnspentOutputsLeft(evilwallet.Reuse)), level2) p.PrintlnPoint(fmt.Sprintf("Spammed blocks: %d", p.mode.blkSent.Load()), level2) p.PrintlnPoint(fmt.Sprintf("Spammed transactions: %d", p.mode.txSent.Load()), level2) p.PrintlnPoint(fmt.Sprintf("Spammed scenario batches: %d", p.mode.scenariosSent.Load()), level2) diff --git a/tools/evil-spammer/survey.go b/tools/evil-spammer/interactive/survey.go similarity index 99% rename from tools/evil-spammer/survey.go rename to tools/evil-spammer/interactive/survey.go index 824c5239b..04d0530b4 100644 --- a/tools/evil-spammer/survey.go +++ b/tools/evil-spammer/interactive/survey.go @@ -1,4 +1,4 @@ -package main +package interactive import ( "strconv" diff --git a/tools/evil-spammer/logger/logger.go b/tools/evil-spammer/logger/logger.go index 276850a01..20ce465a3 100644 --- a/tools/evil-spammer/logger/logger.go +++ b/tools/evil-spammer/logger/logger.go @@ -12,7 +12,7 @@ var New = logger.NewLogger func init() { config := configuration.New() - err := config.Set(logger.ConfigurationKeyOutputPaths, []string{"evil-spammer.log"}) + err := config.Set(logger.ConfigurationKeyOutputPaths, []string{"evil-spammer.log", "stdout"}) if err != nil { fmt.Println(err) return diff --git a/tools/evil-spammer/main.go b/tools/evil-spammer/main.go index 99193c3bb..3306fe387 100644 --- a/tools/evil-spammer/main.go +++ b/tools/evil-spammer/main.go @@ -1,16 +1,13 @@ package main import ( - "encoding/json" "flag" "fmt" - "os" - "strconv" - "strings" - "time" + "github.com/iotaledger/iota-core/tools/evil-spammer/accountwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/interactive" "github.com/iotaledger/iota-core/tools/evil-spammer/logger" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/programs" ) var ( @@ -25,240 +22,92 @@ func main() { fmt.Println("Usage of the Evil Spammer tool, provide the first argument for the selected mode:\n" + "'interactive' - enters the interactive mode.\n" + "'basic' - can be parametrized with additional flags to run one time spammer. Run 'evil-wallet basic -h' for the list of possible flags.\n" + - "'quick' - runs simple stress test: tx spam -> blk spam -> ds spam. Run 'evil-wallet quick -h' for the list of possible flags.\n" + - "'commitments' - runs spammer for commitments. Run 'evil-wallet commitments -h' for the list of possible flags.") + "'accounts' - tool for account creation and transition. Run 'evil-wallet accounts -h' for the list of possible flags.\n" + + "'quick' - runs simple stress test: tx spam -> blk spam -> ds spam. Run 'evil-wallet quick -h' for the list of possible flags.") return } + // init account wallet + var accWallet *accountwallet.AccountWallet + var err error + if Script == "basic" || Script == "accounts" { + // read config here + config := accountwallet.LoadConfiguration() + // load wallet + accWallet, err = accountwallet.Run(config) + if err != nil { + log.Warn(err) + return + } + // save wallet and latest faucet output + defer func() { + err = accountwallet.SaveState(accWallet) + if err != nil { + log.Errorf("Error while saving wallet state: %v", err) + } + accountwallet.SaveConfiguration(config) + + }() + } // run selected test scenario switch Script { case "interactive": - Run() + interactive.Run() case "basic": - CustomSpam(&customSpamParams) + programs.CustomSpam(&customSpamParams, accWallet) + case "accounts": + accountsSubcommands(accWallet, accountsSubcommandsFlags) case "quick": - QuickTest(&quickTestParams) + programs.QuickTest(&quickTestParams) // case SpammerTypeCommitments: // CommitmentsSpam(&commitmentsSpamParams) default: - log.Warnf("Unknown parameter for script, possible values: basic, quick, commitments") - } -} - -func parseFlags() (help bool) { - if len(os.Args) <= 1 { - return true - } - script := os.Args[1] - - Script = script - log.Infof("script %s", Script) - - switch Script { - case "basic": - parseBasicSpamFlags() - case "quick": - parseQuickTestFlags() - // case SpammerTypeCommitments: - // parseCommitmentsSpamFlags() + log.Warnf("Unknown parameter for script, possible values: interactive, basic, accounts, quick") } - if Script == "help" || Script == "-h" || Script == "--help" { - return true - } - - return } -func parseOptionFlagSet(flagSet *flag.FlagSet) { - err := flagSet.Parse(os.Args[2:]) - if err != nil { - log.Errorf("Cannot parse first `script` parameter") - return +func accountsSubcommands(wallet *accountwallet.AccountWallet, subcommands []accountwallet.AccountSubcommands) { + for _, sub := range subcommands { + accountsSubcommand(wallet, sub) } } -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") - 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'.") - 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.") - nSpend := optionFlagSet.Int("nSpend", customSpamParams.NSpend, "Number of outputs to be spent in n-spends spammer for the spammer type needs to be set to 'ds'. Default value is 2 for double-spend.") - - parseOptionFlagSet(optionFlagSet) +func accountsSubcommand(wallet *accountwallet.AccountWallet, sub accountwallet.AccountSubcommands) { + switch sub.Type() { + case accountwallet.OperationCreateAccount: + log.Infof("Run subcommand: %s, with parametetr set: %v", accountwallet.OperationCreateAccount.String(), sub) + params := sub.(*accountwallet.CreateAccountParams) + accountID, err := wallet.CreateAccount(params) + if err != nil { + log.Errorf("Error creating account: %v", err) - if *urls != "" { - parsedUrls := parseCommaSepString(*urls) - quickTestParams.ClientURLs = parsedUrls - customSpamParams.ClientURLs = parsedUrls - } - if *spamTypes != "" { - parsedSpamTypes := parseCommaSepString(*spamTypes) - customSpamParams.SpamTypes = parsedSpamTypes - } - if *rate != "" { - parsedRates := parseCommaSepInt(*rate) - customSpamParams.Rates = parsedRates - } - if *duration != "" { - parsedDurations := parseDurations(*duration) - customSpamParams.Durations = parsedDurations - } - if *blkNum != "" { - parsedBlkNums := parseCommaSepInt(*blkNum) - customSpamParams.BlkToBeSent = parsedBlkNums - } - if *scenario != "" { - conflictBatch, ok := wallet.GetScenario(*scenario) - if ok { - customSpamParams.Scenario = conflictBatch + return } - } - - customSpamParams.NSpend = *nSpend - customSpamParams.DeepSpam = *deepSpam - customSpamParams.TimeUnit = *timeunit - customSpamParams.DelayBetweenConflicts = *delayBetweenConflicts - - // 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)) - } - - customSpamParams.config = loadBasicConfig() -} - -func parseQuickTestFlags() { - urls := optionFlagSet.String("urls", "", "API urls for clients used in test separated with commas") - rate := optionFlagSet.Int("rate", quickTestParams.Rate, "The spamming rate") - duration := optionFlagSet.Duration("duration", quickTestParams.Duration, "Duration of the spam. 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'.") - timeunit := optionFlagSet.Duration("tu", quickTestParams.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", quickTestParams.DelayBetweenConflicts, "delayBetweenConflicts - Time delay between conflicts in double spend spamming") - verifyLedger := optionFlagSet.Bool("verify", quickTestParams.VerifyLedger, "Set to true if verify ledger script should be run at the end of the test") - - parseOptionFlagSet(optionFlagSet) - - if *urls != "" { - parsedUrls := parseCommaSepString(*urls) - quickTestParams.ClientURLs = parsedUrls - } - quickTestParams.Rate = *rate - quickTestParams.Duration = *duration - quickTestParams.TimeUnit = *timeunit - quickTestParams.DelayBetweenConflicts = *delayBetweenConflicts - quickTestParams.VerifyLedger = *verifyLedger -} - -// func parseCommitmentsSpamFlags() { -// commitmentType := optionFlagSet.String("type", commitmentsSpamParams.CommitmentType, "Type of commitment spam. Possible values: 'latest' - valid commitment spam, 'random' - completely new, invalid cahin, 'fork' - forked chain, combine with 'forkAfter' parameter.") -// rate := optionFlagSet.Int("rate", commitmentsSpamParams.Rate, "Commitment spam rate") -// duration := optionFlagSet.Duration("duration", commitmentsSpamParams.Duration, "Duration of the spam. 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'.") -// timeUnit := optionFlagSet.Duration("tu", commitmentsSpamParams.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'.") -// networkAlias := optionFlagSet.String("network", commitmentsSpamParams.NetworkAlias, "Network alias for the test. Check your keys-config.json file for possible values.") -// identityAlias := optionFlagSet.String("spammerAlias", commitmentsSpamParams.SpammerAlias, "Identity alias for the node identity and its private keys that will be used to spam. Check your keys-config.json file for possible values.") -// validAlias := optionFlagSet.String("validAlias", commitmentsSpamParams.ValidAlias, "Identity alias for the honest node and its private keys, will be used to request valid commitment and block data. Check your keys-config.json file for possible values.") -// forkAfter := optionFlagSet.Int("forkAfter", commitmentsSpamParams.Rate, "Indicates how many slots after spammer startup should fork be placed in the created commitment chain. Works only for 'fork' commitment spam type.") - -// parseOptionFlagSet(optionFlagSet) - -// commitmentsSpamParams.CommitmentType = *commitmentType -// commitmentsSpamParams.Rate = *rate -// commitmentsSpamParams.Duration = *duration -// commitmentsSpamParams.TimeUnit = *timeUnit -// commitmentsSpamParams.NetworkAlias = *networkAlias -// commitmentsSpamParams.SpammerAlias = *identityAlias -// commitmentsSpamParams.ValidAlias = *validAlias -// commitmentsSpamParams.ForkAfter = *forkAfter -// } - -func parseCommaSepString(urls string) []string { - split := strings.Split(urls, ",") - - 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 -} - -type BasicConfig struct { - LastFaucetUnspentOutputID string `json:"lastFaucetUnspentOutputId"` -} - -var basicConfigJSON = `{ - "lastFaucetUnspentOutputId": "" -}` - -var basicConfigFile = "basic_config.json" - -// load the config file. -func loadBasicConfig() *BasicConfig { - // open config file - config := new(BasicConfig) - file, err := os.Open(basicConfigFile) - if err != nil { - if !os.IsNotExist(err) { - panic(err) + log.Infof("Created account %s with %d tokens", accountID, params.Amount) + case accountwallet.OperationDestroyAccound: + log.Infof("Run subcommand: %s, with parametetr set: %v", accountwallet.OperationDestroyAccound, sub) + params := sub.(*accountwallet.DestroyAccountParams) + err := wallet.DestroyAccount(params) + if err != nil { + log.Errorf("Error destroying account: %v", err) + + return } + case accountwallet.OperationListAccounts: + err := wallet.ListAccount() + if err != nil { + log.Errorf("Error listing accounts: %v", err) - //nolint:gosec // users should be able to read the file - if err = os.WriteFile(basicConfigFile, []byte(basicConfigJSON), 0o644); err != nil { - panic(err) + return } - if file, err = os.Open(basicConfigFile); err != nil { - panic(err) + case accountwallet.OperationAllotAccount: + log.Infof("Run subcommand: %s, with parametetr set: %v", accountwallet.OperationAllotAccount, sub) + params := sub.(*accountwallet.AllotAccountParams) + err := wallet.AllotToAccount(params) + if err != nil { + log.Errorf("Error allotting account: %v", err) + + return } } - defer file.Close() - - // decode config file - if err = json.NewDecoder(file).Decode(config); err != nil { - panic(err) - } - - return config -} - -func saveConfigsToFile(config *BasicConfig) { - // open config file - file, err := os.Open(basicConfigFile) - if err != nil { - panic(err) - } - defer file.Close() - - jsonConfigs, err := json.MarshalIndent(config, "", " ") - - if err != nil { - log.Errorf("failed to write configs to file %s", err) - } - - //nolint:gosec // users should be able to read the file - if err = os.WriteFile(basicConfigFile, jsonConfigs, 0o644); err != nil { - panic(err) - } } diff --git a/tools/evil-spammer/wallet/connector.go b/tools/evil-spammer/models/connector.go similarity index 89% rename from tools/evil-spammer/wallet/connector.go rename to tools/evil-spammer/models/connector.go index 88447959b..c15462ca0 100644 --- a/tools/evil-spammer/wallet/connector.go +++ b/tools/evil-spammer/models/connector.go @@ -1,4 +1,4 @@ -package wallet +package models import ( "context" @@ -159,10 +159,12 @@ func (c *WebClients) RemoveClient(url string) { } type Client interface { + Client() *nodeclient.Client + Indexer() (nodeclient.IndexerClient, error) // URL returns a client API url. URL() (cltID string) - // PostTransaction sends a transaction to the Tangle via a given client. - PostTransaction(tx *iotago.SignedTransaction) (iotago.BlockID, error) + // PostBlock sends a block to the Tangle via a given client. + 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) // GetTransactionConfirmationState returns the AcceptanceState of a given transaction ID. @@ -175,6 +177,8 @@ type Client interface { GetTransaction(txID iotago.TransactionID) (resp *iotago.SignedTransaction, err error) // GetBlockIssuance returns the latest commitment and data needed to create a new block. GetBlockIssuance() (resp *apimodels.IssuanceBlockHeaderResponse, err error) + // GetCongestion returns congestion data such as rmc or issuing readiness. + GetCongestion(id iotago.AccountID) (resp *apimodels.CongestionResponse, err error) iotago.APIProvider } @@ -185,6 +189,14 @@ type WebClient struct { url string } +func (c *WebClient) Client() *nodeclient.Client { + return c.client +} + +func (c *WebClient) Indexer() (nodeclient.IndexerClient, error) { + return c.client.Indexer(context.Background()) +} + func (c *WebClient) APIForVersion(version iotago.Version) (iotago.API, error) { return c.client.APIForVersion(version) } @@ -219,29 +231,13 @@ func NewWebClient(url string, opts ...options.Option[WebClient]) *WebClient { }) } -// PostTransaction sends a transaction to the Tangle via a given client. -func (c *WebClient) PostTransaction(tx *iotago.SignedTransaction) (blockID iotago.BlockID, err error) { - blockBuilder := builder.NewBasicBlockBuilder(c.client.CurrentAPI()) - - blockBuilder.Payload(tx) - blockBuilder.IssuingTime(time.Time{}) - - blk, err := blockBuilder.Build() - if err != nil { - return iotago.EmptyBlockID, err - } - - id, err := c.client.SubmitBlock(context.Background(), blk) - if err != nil { - return - } - - return id, nil +func (c *WebClient) PostBlock(block *iotago.ProtocolBlock) (blockID iotago.BlockID, err error) { + return c.client.SubmitBlock(context.Background(), block) } // PostData sends the given data (payload) by creating a block in the backend. func (c *WebClient) PostData(data []byte) (blkID string, err error) { - blockBuilder := builder.NewBasicBlockBuilder(c.client.CurrentAPI()) + blockBuilder := builder.NewBasicBlockBuilder(c.client.LatestAPI()) blockBuilder.IssuingTime(time.Time{}) blockBuilder.Payload(&iotago.TaggedData{ @@ -313,3 +309,7 @@ func (c *WebClient) GetBlockIssuance() (resp *apimodels.IssuanceBlockHeaderRespo return } + +func (c *WebClient) GetCongestion(accountID iotago.AccountID) (resp *apimodels.CongestionResponse, err error) { + return c.client.Congestion(context.Background(), accountID) +} diff --git a/tools/evil-spammer/models/output.go b/tools/evil-spammer/models/output.go new file mode 100644 index 000000000..539da3a19 --- /dev/null +++ b/tools/evil-spammer/models/output.go @@ -0,0 +1,88 @@ +package models + +import ( + "crypto/ed25519" + + "github.com/iotaledger/iota-core/pkg/blockhandler" + iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/nodeclient/apimodels" +) + +// Input contains details of an input. +type Input struct { + OutputID iotago.OutputID + Address iotago.Address +} + +// Output contains details of an output ID. +type Output struct { + OutputID iotago.OutputID + Address iotago.Address + Index uint64 + Balance iotago.BaseToken + + OutputStruct iotago.Output +} + +// Outputs is a list of Output. +type Outputs []*Output + +type AccountStatus uint8 + +const ( + AccountPending AccountStatus = iota + AccountReady +) + +type AccountData struct { + Alias string + Status AccountStatus + Account blockhandler.Account + OutputID iotago.OutputID + Index uint64 +} + +type AccountState struct { + Alias string `serix:"0,lengthPrefixType=uint8"` + AccountID iotago.AccountID `serix:"2"` + PrivateKey ed25519.PrivateKey `serix:"3,lengthPrefixType=uint8"` + OutputID iotago.OutputID `serix:"4"` + Index uint64 `serix:"5"` +} + +func AccountStateFromAccountData(acc *AccountData) *AccountState { + return &AccountState{ + Alias: acc.Alias, + AccountID: acc.Account.ID(), + PrivateKey: acc.Account.PrivateKey(), + OutputID: acc.OutputID, + Index: acc.Index, + } +} + +func (a *AccountState) ToAccountData() *AccountData { + return &AccountData{ + Alias: a.Alias, + Account: blockhandler.NewEd25519Account(a.AccountID, a.PrivateKey), + OutputID: a.OutputID, + Index: a.Index, + } +} + +type PayloadIssuanceData struct { + Payload iotago.Payload + CongestionResponse *apimodels.CongestionResponse +} + +type AllotmentStrategy uint8 + +const ( + AllotmentStrategyNone AllotmentStrategy = iota + AllotmentStrategyMinCost + AllotmentStrategyAll +) + +type IssuancePaymentStrategy struct { + AllotmentStrategy AllotmentStrategy + IssuerAlias string +} diff --git a/tools/evil-spammer/parse.go b/tools/evil-spammer/parse.go new file mode 100644 index 000000000..b62b85292 --- /dev/null +++ b/tools/evil-spammer/parse.go @@ -0,0 +1,494 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/iota-core/tools/evil-spammer/accountwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" +) + +func parseFlags() (help bool) { + if len(os.Args) <= 1 { + return true + } + script := os.Args[1] + + Script = script + log.Infof("script %s", Script) + + switch Script { + case "basic": + parseBasicSpamFlags() + case "accounts": + // pass subcommands + subcommands := make([]string, 0) + if len(os.Args) > 2 { + subcommands = os.Args[2:] + } + splitedCmds := readSubcommandsAndFlagSets(subcommands) + accountsSubcommandsFlags = parseAccountTestFlags(splitedCmds) + + case "quick": + parseQuickTestFlags() + // case SpammerTypeCommitments: + // parseCommitmentsSpamFlags() + } + if Script == "help" || Script == "-h" || Script == "--help" { + return true + } + + return +} + +func parseOptionFlagSet(flagSet *flag.FlagSet, args ...[]string) { + commands := os.Args[2:] + if len(args) > 0 { + commands = args[0] + } + err := flagSet.Parse(commands) + if err != nil { + log.Errorf("Cannot parse first `script` parameter") + return + } +} + +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") + 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'.") + 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.") + nSpend := optionFlagSet.Int("nSpend", customSpamParams.NSpend, "Number of outputs to be spent in n-spends spammer for the spammer type needs to be set to 'ds'. Default value is 2 for double-spend.") + account := optionFlagSet.String("account", "", "Account alias to be used for the spam. Account should be created first with accounts tool.") + + parseOptionFlagSet(optionFlagSet) + + if *urls != "" { + parsedUrls := parseCommaSepString(*urls) + quickTestParams.ClientURLs = parsedUrls + customSpamParams.ClientURLs = parsedUrls + } + if *spamTypes != "" { + parsedSpamTypes := parseCommaSepString(*spamTypes) + customSpamParams.SpamTypes = parsedSpamTypes + } + if *rate != "" { + parsedRates := parseCommaSepInt(*rate) + customSpamParams.Rates = parsedRates + } + if *duration != "" { + parsedDurations := parseDurations(*duration) + customSpamParams.Durations = parsedDurations + } + if *blkNum != "" { + parsedBlkNums := parseCommaSepInt(*blkNum) + customSpamParams.BlkToBeSent = parsedBlkNums + } + if *scenario != "" { + conflictBatch, ok := evilwallet.GetScenario(*scenario) + if ok { + customSpamParams.Scenario = conflictBatch + } + } + + customSpamParams.NSpend = *nSpend + customSpamParams.DeepSpam = *deepSpam + customSpamParams.TimeUnit = *timeunit + customSpamParams.DelayBetweenConflicts = *delayBetweenConflicts + 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)) + } +} + +func parseQuickTestFlags() { + urls := optionFlagSet.String("urls", "", "API urls for clients used in test separated with commas") + rate := optionFlagSet.Int("rate", quickTestParams.Rate, "The spamming rate") + duration := optionFlagSet.Duration("duration", quickTestParams.Duration, "Duration of the spam. 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'.") + timeunit := optionFlagSet.Duration("tu", quickTestParams.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", quickTestParams.DelayBetweenConflicts, "delayBetweenConflicts - Time delay between conflicts in double spend spamming") + verifyLedger := optionFlagSet.Bool("verify", quickTestParams.VerifyLedger, "Set to true if verify ledger script should be run at the end of the test") + + parseOptionFlagSet(optionFlagSet) + + if *urls != "" { + parsedUrls := parseCommaSepString(*urls) + quickTestParams.ClientURLs = parsedUrls + } + quickTestParams.Rate = *rate + quickTestParams.Duration = *duration + quickTestParams.TimeUnit = *timeunit + quickTestParams.DelayBetweenConflicts = *delayBetweenConflicts + quickTestParams.VerifyLedger = *verifyLedger +} + +// readSubcommandsAndFlagSets splits the subcommands on multiple flag sets. +func readSubcommandsAndFlagSets(subcommands []string) [][]string { + prevSplitIndex := 0 + subcommandsSplit := make([][]string, 0) + if len(subcommands) == 0 { + return nil + } + + // mainCmd := make([]string, 0) + for index := 0; index < len(subcommands); index++ { + validCommand := accountwallet.AvailableCommands(subcommands[index]) + + if !strings.HasPrefix(subcommands[index], "--") && validCommand { + if index != 0 { + subcommandsSplit = append(subcommandsSplit, subcommands[prevSplitIndex:index]) + } + prevSplitIndex = index + } + } + subcommandsSplit = append(subcommandsSplit, subcommands[prevSplitIndex:]) + + return subcommandsSplit +} + +func parseAccountTestFlags(splitedCmds [][]string) []accountwallet.AccountSubcommands { + parsedCmds := make([]accountwallet.AccountSubcommands, 0) + + for _, cmds := range splitedCmds { + switch cmds[0] { + case "create": + createAccountParams, err := parseCreateAccountFlags(cmds[1:]) + if err != nil { + continue + } + + parsedCmds = append(parsedCmds, createAccountParams) + case "convert": + convertAccountParams, err := parseConvertAccountFlags(cmds[1:]) + if err != nil { + continue + } + + parsedCmds = append(parsedCmds, convertAccountParams) + case "destroy": + destroyAccountParams, err := parseDestroyAccountFlags(cmds[1:]) + if err != nil { + continue + } + + parsedCmds = append(parsedCmds, destroyAccountParams) + case "allot": + allotAccountParams, err := parseAllotAccountFlags(cmds[1:]) + if err != nil { + continue + } + + parsedCmds = append(parsedCmds, allotAccountParams) + case "delegate": + delegatingAccountParams, err := parseDelegateAccountFlags(cmds[1:]) + if err != nil { + continue + } + + parsedCmds = append(parsedCmds, delegatingAccountParams) + case "stake": + stakingAccountParams, err := parseStakeAccountFlags(cmds[1:]) + if err != nil { + continue + } + + parsedCmds = append(parsedCmds, stakingAccountParams) + case "update": + updateAccountParams, err := parseUpdateAccountFlags(cmds[1:]) + if err != nil { + continue + } + + parsedCmds = append(parsedCmds, updateAccountParams) + case "list": + parsedCmds = append(parsedCmds, &accountwallet.NoAccountParams{ + Operation: accountwallet.OperationListAccounts, + }) + default: + accountUsage() + return nil + } + } + + return parsedCmds +} + +func accountUsage() { + fmt.Println("Usage for accounts [COMMAND] [FLAGS], multiple commands can be chained together.") + fmt.Printf("COMMAND: %s\n", accountwallet.CmdNameCreateAccount) + parseCreateAccountFlags(nil) + + fmt.Printf("COMMAND: %s\n", accountwallet.CmdNameConvertAccount) + parseConvertAccountFlags(nil) + + fmt.Printf("COMMAND: %s\n", accountwallet.CmdNameDestroyAccount) + parseDestroyAccountFlags(nil) + + fmt.Printf("COMMAND: %s\n", accountwallet.CmdNameAllotAccount) + parseAllotAccountFlags(nil) + + fmt.Printf("COMMAND: %s\n", accountwallet.CmdNameDelegateAccount) + parseDelegateAccountFlags(nil) + + fmt.Printf("COMMAND: %s\n", accountwallet.CmdNameStakeAccount) + parseStakeAccountFlags(nil) +} + +func parseCreateAccountFlags(subcommands []string) (*accountwallet.CreateAccountParams, error) { + flagSet := flag.NewFlagSet("create", flag.ExitOnError) + alias := flagSet.String("alias", "", "The alias name of new created account") + amount := flagSet.Int64("amount", 1000, "The amount to be transfered to the new account") + noBIF := flagSet.Bool("noBIF", false, "Create account without Block Issuer Feature") + implicit := flagSet.Bool("implicit", false, "Create an implicit account") + + if subcommands == nil { + flagSet.Usage() + + return nil, ierrors.Errorf("no subcommands") + } + + log.Infof("Parsing create account flags, subcommands: %v", subcommands) + err := flagSet.Parse(subcommands) + if err != nil { + log.Errorf("Cannot parse first `script` parameter") + + return nil, ierrors.Wrap(err, "cannot parse first `script` parameter") + } + + return &accountwallet.CreateAccountParams{ + Alias: *alias, + Amount: uint64(*amount), + NoBIF: *noBIF, + Implicit: *implicit, + }, nil +} + +func parseConvertAccountFlags(subcommands []string) (*accountwallet.ConvertAccountParams, error) { + flagSet := flag.NewFlagSet("convert", flag.ExitOnError) + alias := flagSet.String("alias", "", "The implicit account to be converted to full account") + + if subcommands == nil { + flagSet.Usage() + + return nil, ierrors.Errorf("no subcommands") + } + + log.Infof("Parsing convert account flags, subcommands: %v", subcommands) + err := flagSet.Parse(subcommands) + if err != nil { + log.Errorf("Cannot parse first `script` parameter") + + return nil, ierrors.Wrap(err, "cannot parse first `script` parameter") + } + + return &accountwallet.ConvertAccountParams{ + AccountAlias: *alias, + }, nil +} + +func parseDestroyAccountFlags(subcommands []string) (*accountwallet.DestroyAccountParams, error) { + flagSet := flag.NewFlagSet("destroy", flag.ExitOnError) + alias := flagSet.String("alias", "", "The alias name of the account to be destroyed") + expirySlot := flagSet.Int64("expirySlot", 0, "The expiry slot of the account to be destroyed") + + if subcommands == nil { + flagSet.Usage() + + return nil, ierrors.Errorf("no subcommands") + } + + log.Infof("Parsing destroy account flags, subcommands: %v", subcommands) + err := flagSet.Parse(subcommands) + if err != nil { + log.Errorf("Cannot parse first `script` parameter") + + return nil, ierrors.Wrap(err, "cannot parse first `script` parameter") + } + + return &accountwallet.DestroyAccountParams{ + AccountAlias: *alias, + ExpirySlot: uint64(*expirySlot), + }, nil +} + +func parseAllotAccountFlags(subcommands []string) (*accountwallet.AllotAccountParams, error) { + flagSet := flag.NewFlagSet("allot", flag.ExitOnError) + from := flagSet.String("from", "", "The alias name of the account to allot mana from") + to := flagSet.String("to", "", "The alias of the account to allot mana to") + amount := flagSet.Int64("amount", 1000, "The amount of mana to allot") + + if subcommands == nil { + flagSet.Usage() + + return nil, ierrors.Errorf("no subcommands") + } + + log.Infof("Parsing allot account flags, subcommands: %v", subcommands) + err := flagSet.Parse(subcommands) + if err != nil { + log.Errorf("Cannot parse first `script` parameter") + + return nil, ierrors.Wrap(err, "cannot parse first `script` parameter") + } + + return &accountwallet.AllotAccountParams{ + From: *from, + To: *to, + Amount: uint64(*amount), + }, nil +} + +func parseStakeAccountFlags(subcommands []string) (*accountwallet.StakeAccountParams, error) { + flagSet := flag.NewFlagSet("stake", flag.ExitOnError) + alias := flagSet.String("alias", "", "The alias name of the account to stake") + amount := flagSet.Int64("amount", 100, "The amount of tokens to stake") + fixedCost := flagSet.Int64("fixedCost", 0, "The fixed cost of the account to stake") + startEpoch := flagSet.Int64("startEpoch", 0, "The start epoch of the account to stake") + endEpoch := flagSet.Int64("endEpoch", 0, "The end epoch of the account to stake") + + if subcommands == nil { + flagSet.Usage() + + return nil, ierrors.Errorf("no subcommands") + } + + log.Infof("Parsing staking account flags, subcommands: %v", subcommands) + err := flagSet.Parse(subcommands) + if err != nil { + log.Errorf("Cannot parse first `script` parameter") + + return nil, ierrors.Wrap(err, "cannot parse first `script` parameter") + } + + return &accountwallet.StakeAccountParams{ + Alias: *alias, + Amount: uint64(*amount), + FixedCost: uint64(*fixedCost), + StartEpoch: uint64(*startEpoch), + EndEpoch: uint64(*endEpoch), + }, nil +} + +func parseDelegateAccountFlags(subcommands []string) (*accountwallet.DelegateAccountParams, error) { + flagSet := flag.NewFlagSet("delegate", flag.ExitOnError) + from := flagSet.String("from", "", "The alias name of the account to delegate mana from") + to := flagSet.String("to", "", "The alias of the account to delegate mana to") + amount := flagSet.Int64("amount", 100, "The amount of mana to delegate") + + if subcommands == nil { + flagSet.Usage() + + return nil, ierrors.Errorf("no subcommands") + } + + log.Infof("Parsing delegate account flags, subcommands: %v", subcommands) + err := flagSet.Parse(subcommands) + if err != nil { + log.Errorf("Cannot parse first `script` parameter") + + return nil, ierrors.Wrap(err, "cannot parse first `script` parameter") + } + + return &accountwallet.DelegateAccountParams{ + From: *from, + To: *to, + Amount: uint64(*amount), + }, nil +} + +func parseUpdateAccountFlags(subcommands []string) (*accountwallet.UpdateAccountParams, error) { + flagSet := flag.NewFlagSet("update", flag.ExitOnError) + alias := flagSet.String("alias", "", "The alias name of the account to update") + bik := flagSet.String("bik", "", "The block issuer key (in hex) to add") + amount := flagSet.Int64("addamount", 100, "The amount of token to add") + mana := flagSet.Int64("addmana", 100, "The amount of mana to add") + expirySlot := flagSet.Int64("expirySlot", 0, "Update the expiry slot of the account") + + if subcommands == nil { + flagSet.Usage() + + return nil, ierrors.Errorf("no subcommands") + } + + log.Infof("Parsing update account flags, subcommands: %v", subcommands) + err := flagSet.Parse(subcommands) + if err != nil { + log.Errorf("Cannot parse first `script` parameter") + + return nil, ierrors.Wrap(err, "cannot parse first `script` parameter") + } + + return &accountwallet.UpdateAccountParams{ + Alias: *alias, + BlockIssuerKey: *bik, + Amount: uint64(*amount), + Mana: uint64(*mana), + ExpirySlot: uint64(*expirySlot), + }, nil +} + +// func parseCommitmentsSpamFlags() { +// commitmentType := optionFlagSet.String("type", commitmentsSpamParams.CommitmentType, "Type of commitment spam. Possible values: 'latest' - valid commitment spam, 'random' - completely new, invalid cahin, 'fork' - forked chain, combine with 'forkAfter' parameter.") +// rate := optionFlagSet.Int("rate", commitmentsSpamParams.Rate, "Commitment spam rate") +// duration := optionFlagSet.Duration("duration", commitmentsSpamParams.Duration, "Duration of the spam. 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'.") +// timeUnit := optionFlagSet.Duration("tu", commitmentsSpamParams.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'.") +// networkAlias := optionFlagSet.String("network", commitmentsSpamParams.NetworkAlias, "Network alias for the test. Check your keys-config.json file for possible values.") +// identityAlias := optionFlagSet.String("spammerAlias", commitmentsSpamParams.SpammerAlias, "Identity alias for the node identity and its private keys that will be used to spam. Check your keys-config.json file for possible values.") +// validAlias := optionFlagSet.String("validAlias", commitmentsSpamParams.ValidAlias, "Identity alias for the honest node and its private keys, will be used to request valid commitment and block data. Check your keys-config.json file for possible values.") +// forkAfter := optionFlagSet.Int("forkAfter", commitmentsSpamParams.Rate, "Indicates how many slots after spammer startup should fork be placed in the created commitment chain. Works only for 'fork' commitment spam type.") + +// parseOptionFlagSet(optionFlagSet) + +// commitmentsSpamParams.CommitmentType = *commitmentType +// commitmentsSpamParams.Rate = *rate +// commitmentsSpamParams.Duration = *duration +// commitmentsSpamParams.TimeUnit = *timeUnit +// commitmentsSpamParams.NetworkAlias = *networkAlias +// commitmentsSpamParams.SpammerAlias = *identityAlias +// commitmentsSpamParams.ValidAlias = *validAlias +// commitmentsSpamParams.ForkAfter = *forkAfter +// } + +func parseCommaSepString(urls string) []string { + split := strings.Split(urls, ",") + + 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/tools/evil-spammer/commitments.go b/tools/evil-spammer/programs/commitments.go similarity index 98% rename from tools/evil-spammer/commitments.go rename to tools/evil-spammer/programs/commitments.go index b97dc205b..5083b8552 100644 --- a/tools/evil-spammer/commitments.go +++ b/tools/evil-spammer/programs/commitments.go @@ -1,4 +1,4 @@ -package main +package programs // import ( // "time" diff --git a/tools/evil-spammer/programs/params.go b/tools/evil-spammer/programs/params.go new file mode 100644 index 000000000..f0e1d2070 --- /dev/null +++ b/tools/evil-spammer/programs/params.go @@ -0,0 +1,22 @@ +package programs + +import ( + "time" + + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" +) + +type CustomSpamParams struct { + ClientURLs []string + SpamTypes []string + Rates []int + Durations []time.Duration + BlkToBeSent []int + TimeUnit time.Duration + DelayBetweenConflicts time.Duration + NSpend int + Scenario evilwallet.EvilBatch + DeepSpam bool + EnableRateSetter bool + AccountAlias string +} diff --git a/tools/evil-spammer/quick-test.go b/tools/evil-spammer/programs/quick-test.go similarity index 84% rename from tools/evil-spammer/quick-test.go rename to tools/evil-spammer/programs/quick-test.go index 14a7e4b1f..e17725594 100644 --- a/tools/evil-spammer/quick-test.go +++ b/tools/evil-spammer/programs/quick-test.go @@ -1,10 +1,10 @@ -package main +package programs import ( "time" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" "github.com/iotaledger/iota-core/tools/evil-spammer/spammer" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" ) type QuickTestParams struct { @@ -19,7 +19,7 @@ type QuickTestParams struct { // QuickTest runs short spamming periods with stable mps. func QuickTest(params *QuickTestParams) { - evilWallet := wallet.NewEvilWallet(wallet.WithClients(params.ClientURLs...)) + evilWallet := evilwallet.NewEvilWallet(evilwallet.WithClients(params.ClientURLs...)) counter := spammer.NewErrorCount() log.Info("Starting quick test") @@ -41,8 +41,8 @@ func QuickTest(params *QuickTestParams) { spammer.WithSpammingFunc(spammer.DataSpammingFunction), ) - dsScenario := wallet.NewEvilScenario( - wallet.WithScenarioCustomConflicts(wallet.NSpendBatch(2)), + dsScenario := evilwallet.NewEvilScenario( + evilwallet.WithScenarioCustomConflicts(evilwallet.NSpendBatch(2)), ) //nolint:gocritic // we want a copy here diff --git a/tools/evil-spammer/programs/spammers.go b/tools/evil-spammer/programs/spammers.go new file mode 100644 index 000000000..b8ca4845f --- /dev/null +++ b/tools/evil-spammer/programs/spammers.go @@ -0,0 +1,230 @@ +package programs + +import ( + "sync" + "time" + + "github.com/iotaledger/iota-core/tools/evil-spammer/accountwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/logger" + "github.com/iotaledger/iota-core/tools/evil-spammer/spammer" +) + +var log = logger.New("customSpam") + +func CustomSpam(params *CustomSpamParams, accWallet *accountwallet.AccountWallet) { + w := evilwallet.NewEvilWallet(evilwallet.WithClients(params.ClientURLs...), evilwallet.WithAccountsWallet(accWallet)) + wg := sync.WaitGroup{} + + // funds are requested fro all spam types except SpammerTypeBlock + fundsNeeded := false + for _, st := range params.SpamTypes { + if st != spammer.TypeBlock { + fundsNeeded = true + } + } + if fundsNeeded { + err := w.RequestFreshBigFaucetWallet() + if err != nil { + panic(err) + } + } + + 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) + + 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.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.TypeCommitments: + wg.Add(1) + go func() { + defer wg.Done() + }() + case spammer.TypeAccounts: + wg.Add(1) + go func() { + defer wg.Done() + + s := SpamAccounts(w, params.Rates[i], params.TimeUnit, params.Durations[i], 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!") +} + +func SpamTransaction(w *evilwallet.EvilWallet, rate int, timeUnit, duration time.Duration, deepSpam, enableRateSetter bool, accountAlias string) { + if w.NumOfClient() < 1 { + log.Infof("Warning: At least one client is needed to spam.") + } + + scenarioOptions := []evilwallet.ScenarioOption{ + evilwallet.WithScenarioCustomConflicts(evilwallet.SingleTransactionBatch()), + } + if deepSpam { + outWallet := evilwallet.NewWallet(evilwallet.Reuse) + scenarioOptions = append(scenarioOptions, + evilwallet.WithScenarioDeepSpamEnabled(), + evilwallet.WithScenarioReuseOutputWallet(outWallet), + evilwallet.WithScenarioInputWalletForDeepSpam(outWallet), + ) + } + scenarioTx := evilwallet.NewEvilScenario(scenarioOptions...) + + options := []spammer.Options{ + spammer.WithSpamRate(rate, timeUnit), + spammer.WithSpamDuration(duration), + spammer.WithRateSetter(enableRateSetter), + spammer.WithEvilWallet(w), + spammer.WithEvilScenario(scenarioTx), + spammer.WithAccountAlias(accountAlias), + } + + s := spammer.NewSpammer(options...) + s.Spam() +} + +func SpamDoubleSpends(w *evilwallet.EvilWallet, rate, nSpent int, timeUnit, duration, delayBetweenConflicts time.Duration, deepSpam, enableRateSetter bool, accountAlias string) { + log.Debugf("Setting up double spend spammer with rate: %d, time unit: %s, and duration: %s.", rate, timeUnit.String(), duration.String()) + if w.NumOfClient() < 2 { + log.Infof("Warning: At least two client are needed to spam, and %d was provided", w.NumOfClient()) + } + + scenarioOptions := []evilwallet.ScenarioOption{ + evilwallet.WithScenarioCustomConflicts(evilwallet.NSpendBatch(nSpent)), + } + if deepSpam { + outWallet := evilwallet.NewWallet(evilwallet.Reuse) + scenarioOptions = append(scenarioOptions, + evilwallet.WithScenarioDeepSpamEnabled(), + evilwallet.WithScenarioReuseOutputWallet(outWallet), + evilwallet.WithScenarioInputWalletForDeepSpam(outWallet), + ) + } + scenarioDs := evilwallet.NewEvilScenario(scenarioOptions...) + options := []spammer.Options{ + spammer.WithSpamRate(rate, timeUnit), + spammer.WithSpamDuration(duration), + spammer.WithEvilWallet(w), + spammer.WithRateSetter(enableRateSetter), + spammer.WithTimeDelayForDoubleSpend(delayBetweenConflicts), + spammer.WithEvilScenario(scenarioDs), + spammer.WithAccountAlias(accountAlias), + } + + s := spammer.NewSpammer(options...) + s.Spam() +} + +func SpamNestedConflicts(w *evilwallet.EvilWallet, rate int, timeUnit, duration time.Duration, conflictBatch evilwallet.EvilBatch, deepSpam, reuseOutputs, enableRateSetter bool, accountAlias string) *spammer.Spammer { + scenarioOptions := []evilwallet.ScenarioOption{ + evilwallet.WithScenarioCustomConflicts(conflictBatch), + } + if deepSpam { + outWallet := evilwallet.NewWallet(evilwallet.Reuse) + scenarioOptions = append(scenarioOptions, + evilwallet.WithScenarioDeepSpamEnabled(), + evilwallet.WithScenarioReuseOutputWallet(outWallet), + evilwallet.WithScenarioInputWalletForDeepSpam(outWallet), + ) + } else if reuseOutputs { + outWallet := evilwallet.NewWallet(evilwallet.Reuse) + scenarioOptions = append(scenarioOptions, evilwallet.WithScenarioReuseOutputWallet(outWallet)) + } + scenario := evilwallet.NewEvilScenario(scenarioOptions...) + if scenario.NumOfClientsNeeded > w.NumOfClient() { + log.Infof("Warning: At least %d client are needed to spam, and %d was provided", scenario.NumOfClientsNeeded, w.NumOfClient()) + } + + options := []spammer.Options{ + spammer.WithSpamRate(rate, timeUnit), + spammer.WithSpamDuration(duration), + spammer.WithEvilWallet(w), + spammer.WithRateSetter(enableRateSetter), + spammer.WithEvilScenario(scenario), + spammer.WithAccountAlias(accountAlias), + } + + return spammer.NewSpammer(options...) +} + +func SpamBlocks(w *evilwallet.EvilWallet, rate int, timeUnit, duration time.Duration, numBlkToSend int, enableRateSetter bool, accountAlias string) *spammer.Spammer { + if w.NumOfClient() < 1 { + log.Infof("Warning: At least one client is needed to spam.") + } + + options := []spammer.Options{ + spammer.WithSpamRate(rate, timeUnit), + spammer.WithSpamDuration(duration), + spammer.WithBatchesSent(numBlkToSend), + spammer.WithRateSetter(enableRateSetter), + spammer.WithEvilWallet(w), + spammer.WithSpammingFunc(spammer.DataSpammingFunction), + spammer.WithAccountAlias(accountAlias), + } + + return spammer.NewSpammer(options...) +} + +func SpamAccounts(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.") + } + scenarioOptions := []evilwallet.ScenarioOption{ + evilwallet.WithScenarioCustomConflicts(evilwallet.SingleTransactionBatch()), + evilwallet.WithCreateAccounts(), + } + + scenarioAccount := evilwallet.NewEvilScenario(scenarioOptions...) + + options := []spammer.Options{ + spammer.WithSpamRate(rate, timeUnit), + spammer.WithSpamDuration(duration), + spammer.WithRateSetter(enableRateSetter), + spammer.WithEvilWallet(w), + spammer.WithSpammingFunc(spammer.AccountSpammingFunction), + spammer.WithEvilScenario(scenarioAccount), + spammer.WithAccountAlias(accountAlias), + } + + return spammer.NewSpammer(options...) +} diff --git a/tools/evil-spammer/spammer/errors.go b/tools/evil-spammer/spammer/errors.go index 95f8a5b91..4c056ffaa 100644 --- a/tools/evil-spammer/spammer/errors.go +++ b/tools/evil-spammer/spammer/errors.go @@ -10,14 +10,17 @@ import ( ) var ( - ErrFailPostTransaction = ierrors.New("failed to post transaction") + ErrFailPostBlock = ierrors.New("failed to post block") ErrFailSendDataBlock = ierrors.New("failed to send a data block") ErrFailGetReferences = ierrors.New("failed to get references") ErrTransactionIsNil = ierrors.New("provided transaction is nil") + ErrTransactionInvalid = ierrors.New("provided transaction is invalid") + ErrPayloadIsNil = ierrors.New("provided payload is nil") ErrFailToPrepareBatch = ierrors.New("custom conflict batch could not be prepared") ErrInsufficientClients = ierrors.New("insufficient clients to send conflicts") ErrInputsNotSolid = ierrors.New("not all inputs are solid") ErrFailPrepareBlock = ierrors.New("failed to prepare block") + ErrFailGetAccount = ierrors.New("failed to get account from the account wallet") ) // ErrorCounter counts errors that appeared during the spam, diff --git a/tools/evil-spammer/spammer/options.go b/tools/evil-spammer/spammer/options.go index ec45ce8ca..0077ab1af 100644 --- a/tools/evil-spammer/spammer/options.go +++ b/tools/evil-spammer/spammer/options.go @@ -3,7 +3,8 @@ package spammer import ( "time" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" ) type Options func(*Spammer) @@ -60,6 +61,13 @@ func WithSpammingFunc(spammerFunc func(s *Spammer)) Options { } } +// WithAccountAlias sets the alias of the account that will be used to pay with mana for sent blocks. +func WithAccountAlias(alias string) Options { + return func(s *Spammer) { + s.IssuerAlias = alias + } +} + // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // region Spammer EvilWallet options /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -85,14 +93,14 @@ func WithBatchesSent(maxBatchesSent int) Options { } // WithEvilWallet provides evil wallet instance, that will handle all spam logic according to provided EvilScenario. -func WithEvilWallet(initWallets *wallet.EvilWallet) Options { +func WithEvilWallet(initWallets *evilwallet.EvilWallet) Options { return func(s *Spammer) { s.EvilWallet = initWallets } } // WithEvilScenario provides initWallet of spammer, if omitted spammer will prepare funds based on maxBlkSent parameter. -func WithEvilScenario(scenario *wallet.EvilScenario) Options { +func WithEvilScenario(scenario *evilwallet.EvilScenario) Options { return func(s *Spammer) { s.EvilScenario = scenario } @@ -118,7 +126,7 @@ func WithNumberOfSpends(n int) Options { func WithClientURL(clientURL string) Options { return func(s *Spammer) { - s.Clients = wallet.NewWebClients([]string{clientURL}) + s.Clients = models.NewWebClients([]string{clientURL}) } } diff --git a/tools/evil-spammer/spammer/spammer.go b/tools/evil-spammer/spammer/spammer.go index 8e86ee7a6..5037556f6 100644 --- a/tools/evil-spammer/spammer/spammer.go +++ b/tools/evil-spammer/spammer/spammer.go @@ -9,16 +9,25 @@ import ( appLogger "github.com/iotaledger/hive.go/app/logger" "github.com/iotaledger/hive.go/ds/types" "github.com/iotaledger/hive.go/ierrors" - "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/logger" "github.com/iotaledger/iota-core/pkg/protocol/snapshotcreator" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" "github.com/iotaledger/iota-core/tools/genesis-snapshot/presets" iotago "github.com/iotaledger/iota.go/v4" ) +const ( + TypeBlock = "blk" + TypeTx = "tx" + TypeDs = "ds" + TypeCustom = "custom" + TypeCommitments = "commitments" + TypeAccounts = "accounts" +) + // region Spammer ////////////////////////////////////////////////////////////////////////////////////////////////////// -// + //nolint:revive type SpammerFunc func(*Spammer) @@ -49,12 +58,13 @@ type Spammer struct { State *State UseRateSetter bool SpamType SpamType - Clients wallet.Connector - EvilWallet *wallet.EvilWallet - EvilScenario *wallet.EvilScenario + Clients models.Connector + EvilWallet *evilwallet.EvilWallet + EvilScenario *evilwallet.EvilScenario IdentityManager *IdentityManager // CommitmentManager *CommitmentManager - ErrCounter *ErrorCounter + ErrCounter *ErrorCounter + IssuerAlias string log Logger api iotago.API @@ -83,7 +93,7 @@ func NewSpammer(options ...Options) *Spammer { spamFunc: CustomConflictSpammingFunc, State: state, SpamType: SpamEvilWallet, - EvilScenario: wallet.NewEvilScenario(), + EvilScenario: evilwallet.NewEvilScenario(), IdentityManager: NewIdentityManager(), // CommitmentManager: NewCommitmentManager(), UseRateSetter: true, @@ -119,7 +129,7 @@ func (s *Spammer) setup() { switch s.SpamType { case SpamEvilWallet: if s.EvilWallet == nil { - s.EvilWallet = wallet.NewEvilWallet() + s.EvilWallet = evilwallet.NewEvilWallet() } s.Clients = s.EvilWallet.Connector() // case SpamCommitments: @@ -222,50 +232,54 @@ func (s *Spammer) StopSpamming() { s.shutdown <- types.Void } -// PostTransaction use provided client to issue a transaction. It chooses API method based on Spammer options. Counts errors, -// counts transactions and provides debug logs. -func (s *Spammer) PostTransaction(signedTx *iotago.SignedTransaction, clt wallet.Client) { - if signedTx == nil { - s.log.Debug(ErrTransactionIsNil) - s.ErrCounter.CountError(ErrTransactionIsNil) +func (s *Spammer) PrepareAndPostBlock(txData *models.PayloadIssuanceData, issuerAlias string, clt models.Client) { + if txData.Payload == nil { + s.log.Debug(ErrPayloadIsNil) + s.ErrCounter.CountError(ErrPayloadIsNil) return } + 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())) - txID := lo.PanicOnErr(signedTx.Transaction.ID()) - allSolid := s.handleSolidityForReuseOutputs(clt, signedTx) - if !allSolid { - s.log.Debug(ErrInputsNotSolid) - s.ErrCounter.CountError(ierrors.Wrapf(ErrInputsNotSolid, "txID: %s", txID.ToHex())) + return + } + 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 } - blockID, err := clt.PostTransaction(signedTx) + if txData.Payload.PayloadType() != iotago.PayloadSignedTransaction { + return + } + + signedTx := txData.Payload.(*iotago.SignedTransaction) + + txID, err := signedTx.Transaction.ID() if err != nil { - s.log.Debug(ierrors.Wrapf(ErrFailPostTransaction, err.Error())) - s.ErrCounter.CountError(ierrors.Wrapf(ErrFailPostTransaction, err.Error())) + s.log.Debug(ierrors.Wrapf(ErrTransactionInvalid, err.Error())) + s.ErrCounter.CountError(ierrors.Wrapf(ErrTransactionInvalid, err.Error())) return } - if s.EvilScenario.OutputWallet.Type() == wallet.Reuse { - var outputIDs iotago.OutputIDs - for index := range signedTx.Transaction.Outputs { - outputIDs = append(outputIDs, iotago.OutputIDFromTransactionIDAndIndex(txID, uint16(index))) + + // reuse outputs + if txData.Payload.PayloadType() == iotago.PayloadSignedTransaction { + if s.EvilScenario.OutputWallet.Type() == evilwallet.Reuse { + var outputIDs iotago.OutputIDs + for index := range signedTx.Transaction.Outputs { + outputIDs = append(outputIDs, iotago.OutputIDFromTransactionIDAndIndex(txID, uint16(index))) + } + s.EvilWallet.SetTxOutputsSolid(outputIDs, clt.URL()) } - s.EvilWallet.SetTxOutputsSolid(outputIDs, clt.URL()) } - count := s.State.txSent.Add(1) - s.log.Debugf("%s: Last transaction sent, ID: %s, txCount: %d", blockID.ToHex(), txID.ToHex(), count) -} - -func (s *Spammer) handleSolidityForReuseOutputs(_ wallet.Client, _ *iotago.SignedTransaction) (ok bool) { - // ok = s.EvilWallet.AwaitInputsSolidity(tx.SignedTransaction().Inputs(), clt) - // if s.EvilScenario.OutputWallet.Type() == wallet.Reuse { - // s.EvilWallet.AddReuseOutputsToThePool(tx.SignedTransaction().Outputs()) - // } - return true + s.log.Debugf("Last block sent, ID: %s, txCount: %d", blockID.ToHex(), count) } // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/tools/evil-spammer/spammer/spamming_functions.go b/tools/evil-spammer/spammer/spamming_functions.go index 7eca19a76..2462c07ca 100644 --- a/tools/evil-spammer/spammer/spamming_functions.go +++ b/tools/evil-spammer/spammer/spamming_functions.go @@ -6,7 +6,7 @@ import ( "time" "github.com/iotaledger/hive.go/ierrors" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/models" iotago "github.com/iotaledger/iota.go/v4" ) @@ -15,52 +15,48 @@ func DataSpammingFunction(s *Spammer) { // sleep randomly to avoid issuing blocks in different goroutines at once //nolint:gosec time.Sleep(time.Duration(rand.Float64()*20) * time.Millisecond) - // if err := wallet.RateSetterSleep(clt, s.UseRateSetter); err != nil { - // s.ErrCounter.CountError(err) - // } - blkID, err := clt.PostData([]byte("SPAM")) - if err != nil { - s.ErrCounter.CountError(ErrFailSendDataBlock) - s.log.Error(err) - } - count := s.State.txSent.Add(1) - if count%int64(s.SpamDetails.Rate*2) == 0 { - s.log.Debugf("Last sent block, ID: %s; blkCount: %d", blkID, count) - } + s.PrepareAndPostBlock(&models.PayloadIssuanceData{ + Payload: &iotago.TaggedData{ + Tag: []byte("SPAM"), + }, + }, s.IssuerAlias, clt) + s.State.batchPrepared.Add(1) s.CheckIfAllSent() } func CustomConflictSpammingFunc(s *Spammer) { - conflictBatch, aliases, err := s.EvilWallet.PrepareCustomConflictsSpam(s.EvilScenario) + conflictBatch, aliases, err := s.EvilWallet.PrepareCustomConflictsSpam(s.EvilScenario, &models.IssuancePaymentStrategy{ + AllotmentStrategy: models.AllotmentStrategyAll, + IssuerAlias: s.IssuerAlias, + }) + if err != nil { s.log.Debugf(ierrors.Wrap(ErrFailToPrepareBatch, err.Error()).Error()) s.ErrCounter.CountError(ierrors.Wrap(ErrFailToPrepareBatch, err.Error())) } - for _, txs := range conflictBatch { - clients := s.Clients.GetClients(len(txs)) - if len(txs) > len(clients) { + for _, txsData := range conflictBatch { + clients := s.Clients.GetClients(len(txsData)) + if len(txsData) > len(clients) { s.log.Debug(ErrFailToPrepareBatch) s.ErrCounter.CountError(ErrInsufficientClients) } // send transactions in parallel wg := sync.WaitGroup{} - for i, tx := range txs { + for i, txData := range txsData { wg.Add(1) - go func(clt wallet.Client, tx *iotago.SignedTransaction) { + go func(clt models.Client, tx *models.PayloadIssuanceData) { 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) - // if err = wallet.RateSetterSleep(clt, s.UseRateSetter); err != nil { - // s.ErrCounter.CountError(err) - // } - s.PostTransaction(tx, clt) - }(clients[i], tx) + + s.PrepareAndPostBlock(tx, s.IssuerAlias, clt) + }(clients[i], txData) } wg.Wait() } @@ -69,6 +65,24 @@ func CustomConflictSpammingFunc(s *Spammer) { s.CheckIfAllSent() } +func AccountSpammingFunction(s *Spammer) { + clt := s.Clients.GetClient() + // update scenario + txData, aliases, err := s.EvilWallet.PrepareAccountSpam(s.EvilScenario, &models.IssuancePaymentStrategy{ + AllotmentStrategy: models.AllotmentStrategyAll, + IssuerAlias: s.IssuerAlias, + }) + if err != nil { + s.log.Debugf(ierrors.Wrap(ErrFailToPrepareBatch, err.Error()).Error()) + s.ErrCounter.CountError(ierrors.Wrap(ErrFailToPrepareBatch, err.Error())) + } + s.PrepareAndPostBlock(txData, s.IssuerAlias, clt) + + s.State.batchPrepared.Add(1) + s.EvilWallet.ClearAliases(aliases) + s.CheckIfAllSent() +} + // func CommitmentsSpammingFunction(s *Spammer) { // clt := s.Clients.GetClient() // p := payload.NewGenericDataPayload([]byte("SPAM")) diff --git a/tools/evil-spammer/spammer/utils.go b/tools/evil-spammer/spammer/utils.go index c395d023b..997945723 100644 --- a/tools/evil-spammer/spammer/utils.go +++ b/tools/evil-spammer/spammer/utils.go @@ -3,12 +3,12 @@ package spammer import ( "time" - "github.com/iotaledger/iota-core/tools/evil-spammer/wallet" + "github.com/iotaledger/iota-core/tools/evil-spammer/evilwallet" ) // BigWalletsNeeded calculates how many big wallets needs to be prepared for a spam based on provided spam details. func BigWalletsNeeded(rate int, timeUnit, duration time.Duration) int { - bigWalletSize := wallet.FaucetRequestSplitNumber * wallet.FaucetRequestSplitNumber + bigWalletSize := evilwallet.FaucetRequestSplitNumber * evilwallet.FaucetRequestSplitNumber outputsNeeded := rate * int(duration/timeUnit) walletsNeeded := outputsNeeded/bigWalletSize + 1