diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfbb3caf..816757a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to this project will be documented in this file. +## [0.2.2] - 15.12.2019 + +### Added + + - TangleMonitor Plugin + - Spammer Plugin + - More detailed log messages at shutdown + +### Fixed + + - Do not expose passwords from config file at startup + - Duplicated neighbors + +### Config file changes + +New settings: +```json + "monitor": { + "tanglemonitorpath": "tanglemonitor/frontend", + "domain": "", + "host": "127.0.0.1" + }, + "spammer": { + "address": "HORNET99INTEGRATED99SPAMMER999999999999999999999999999999999999999999999999999999", + "depth": 3, + "message": "Spamming with HORNET tipselect", + "tag": "HORNET99INTEGRATED99SPAMMER", + "tpsratelimit": 0.1, + "workers": 1 + }, + "zmq": { + "host": "127.0.0.1", + } +``` + + ## [0.2.1] - 13.12.2019 ### Added diff --git a/README.md b/README.md index 6a659dda2..7169e7472 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,56 @@ This way, HORNET is easier to install and runs on low-end devices. - Download the latest HORNET snapshot from [dbfiles.iota.org](https://dbfiles.iota.org/mainnet/hornet/latest-export.gz.bin) - Run HORNET: `./hornet -c config` +--- + +### Available plugins + +#### TangleMonitor + +- Download the latest TangleMonitor source code +```bash +git clone https://github.com/unioproject/tanglemonitor.git +``` +- Modify the `config.json` to fit your needs + - `"tanglemonitorpath"` has to point to the frontend folder of the TangleMonitor source code + - Add `"Monitor"` to `"enableplugins"` + - Change `"host"` to `"0.0.0.0"` if you want to access TangleMonitor from anywhere +```json + "monitor": { + "tanglemonitorpath": "tanglemonitor/frontend", + "domain": "", + "host": "127.0.0.1" + }, + "node": { + "disableplugins": [], + "enableplugins": ["Monitor"], + "loglevel": 3 + }, +``` + +#### Spammer + +- Modify the `config.json` to fit your needs + - Change `"address"`, `"message"`, `"tag"` and `"tpsratelimit"` + - Add `"Spammer"` to `"enableplugins"` +```json + "spammer": { + "address": "HORNET99INTEGRATED99SPAMMER999999999999999999999999999999999999999999999999999999", + "depth": 3, + "message": "Spamming with HORNET tipselect", + "tag": "HORNET99INTEGRATED99SPAMMER", + "tpsratelimit": 0.1, + "workers": 1 + }, + "node": { + "disableplugins": [], + "enableplugins": ["Spammer"], + "loglevel": 3 + }, +``` + +--- + ### Docker -See [Docker](DOCKER.md) +- See [Docker](DOCKER.md) diff --git a/config.json b/config.json index 3eb8de942..25e5c1efa 100644 --- a/config.json +++ b/config.json @@ -139,6 +139,14 @@ "protocol": { "mwm": 14 }, + "spammer": { + "address": "HORNET99INTEGRATED99SPAMMER999999999999999999999999999999999999999999999999999999", + "depth": 3, + "message": "Spamming with HORNET tipselect", + "tag": "HORNET99INTEGRATED99SPAMMER", + "tpsratelimit": 0.1, + "workers": 1 + }, "tipsel": { "belowmaxdepthtransactionlimit": 20000, "maxdepth": 15 diff --git a/go.mod b/go.mod index 8ebb67852..09b23163d 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/googollee/go-socket.io v1.4.3-0.20191204093753-683f8725b6d0 github.com/gorilla/websocket v1.4.1 github.com/iotaledger/goshimmer v0.0.0-20191125182838-26e99d88bddb - github.com/iotaledger/hive.go v0.0.0-20191206213336-9f6cd83f0501 + github.com/iotaledger/hive.go v0.0.0-20191215095341-f16d8a5ea757 github.com/iotaledger/iota.go v1.0.0-beta.12 github.com/labstack/echo v3.3.10+incompatible github.com/mitchellh/mapstructure v1.1.2 diff --git a/go.sum b/go.sum index dff16bec5..736d86a26 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,9 @@ github.com/iotaledger/goshimmer v0.0.0-20191125182838-26e99d88bddb/go.mod h1:Mpv github.com/iotaledger/hive.go v0.0.0-20191113184748-b545de9170d9/go.mod h1:Ks2y/bEyfvb7nUA7l69a+8Epsv16UlGev0BvxggHius= github.com/iotaledger/hive.go v0.0.0-20191206213336-9f6cd83f0501 h1:m9bX1o4r269lC8PiLnvwnJVLM9SQIBaSYZCdwJgo0tU= github.com/iotaledger/hive.go v0.0.0-20191206213336-9f6cd83f0501/go.mod h1:7iqun29a1x0lymTrn0UJ3Z/yy0sUzUpoOZ1OYMrYN20= +github.com/iotaledger/hive.go v0.0.0-20191208004610-567900b261bd h1:hh8iusLOBylWNHeJNiZv6atCbO7vzjCLlg53pNAMkFk= +github.com/iotaledger/hive.go v0.0.0-20191215095341-f16d8a5ea757 h1:D9zQRp4dqPErNbJHa4uX5UREFlDs9WOq8j5yJo+/Szw= +github.com/iotaledger/hive.go v0.0.0-20191215095341-f16d8a5ea757/go.mod h1:7iqun29a1x0lymTrn0UJ3Z/yy0sUzUpoOZ1OYMrYN20= github.com/iotaledger/iota.go v1.0.0-beta.9/go.mod h1:F6WBmYd98mVjAmmPVYhnxg8NNIWCjjH8VWT9qvv3Rc8= github.com/iotaledger/iota.go v1.0.0-beta.12 h1:p6Pk3N8tmb6Kaj8F45O5XVJQfH5qQYqpvUnXiSMrtiE= github.com/iotaledger/iota.go v1.0.0-beta.12/go.mod h1:F6WBmYd98mVjAmmPVYhnxg8NNIWCjjH8VWT9qvv3Rc8= diff --git a/main.go b/main.go index 5a9d41458..526babb08 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/gohornet/hornet/plugins/monitor" "github.com/gohornet/hornet/plugins/snapshot" "github.com/gohornet/hornet/plugins/spa" + "github.com/gohornet/hornet/plugins/spammer" "github.com/gohornet/hornet/plugins/tangle" "github.com/gohornet/hornet/plugins/tipselection" "github.com/gohornet/hornet/plugins/webapi" @@ -37,5 +38,6 @@ func main() { spa.PLUGIN, zeromq.PLUGIN, monitor.PLUGIN, + spammer.PLUGIN, ) } diff --git a/plugins/cli/plugin.go b/plugins/cli/plugin.go index 1b37fcdb8..bfcd04248 100644 --- a/plugins/cli/plugin.go +++ b/plugins/cli/plugin.go @@ -13,7 +13,7 @@ import ( var ( // AppVersion version number - AppVersion = "0.2.1" + AppVersion = "0.2.2" // AppName app code name AppName = "HORNET" @@ -58,7 +58,10 @@ func configure(ctx *node.Plugin) { ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ` + "\n\n") - parameter.FetchConfig(true) + ignoreSettingsAtPrint := []string{} + ignoreSettingsAtPrint = append(ignoreSettingsAtPrint, "api.auth.password") + ignoreSettingsAtPrint = append(ignoreSettingsAtPrint, "dashboard.basic_auth.password") + parameter.FetchConfig(true, ignoreSettingsAtPrint) parseParameters() ctx.Node.Logger.Infof("Using profile '%s'", profile.GetProfile().Name) ctx.Node.Logger.Info("Loading plugins ...") diff --git a/plugins/spammer/bundle.go b/plugins/spammer/bundle.go new file mode 100644 index 000000000..2668ce462 --- /dev/null +++ b/plugins/spammer/bundle.go @@ -0,0 +1,112 @@ +package spammer + +import ( + "fmt" + "time" + + "github.com/iotaledger/iota.go/bundle" + "github.com/iotaledger/iota.go/consts" + "github.com/iotaledger/iota.go/converter" + "github.com/iotaledger/iota.go/kerl" + "github.com/iotaledger/iota.go/trinary" +) + +func integerToAscii(number int) string { + alphabet := "9ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + result := "" + for index := 0; index < 7; index++ { + pos := number % 27 + number /= 27 + result = string(alphabet[pos]) + result + } + return result +} + +// We don't need to care about the M-Bug in the spammer => much faster without +func finalizeInsecure(bundle bundle.Bundle) (bundle.Bundle, error) { + var valueTrits = make([]trinary.Trits, len(bundle)) + var timestampTrits = make([]trinary.Trits, len(bundle)) + var currentIndexTrits = make([]trinary.Trits, len(bundle)) + var obsoleteTagTrits = make([]trinary.Trits, len(bundle)) + var lastIndexTrits = trinary.PadTrits(trinary.IntToTrits(int64(bundle[0].LastIndex)), 27) + + for i := range bundle { + valueTrits[i] = trinary.PadTrits(trinary.IntToTrits(bundle[i].Value), 81) + timestampTrits[i] = trinary.PadTrits(trinary.IntToTrits(int64(bundle[i].Timestamp)), 27) + currentIndexTrits[i] = trinary.PadTrits(trinary.IntToTrits(int64(bundle[i].CurrentIndex)), 27) + obsoleteTagTrits[i] = trinary.PadTrits(trinary.MustTrytesToTrits(bundle[i].ObsoleteTag), 81) + } + + var bundleHash trinary.Hash + + k := kerl.NewKerl() + + for i := 0; i < len(bundle); i++ { + relevantTritsForBundleHash := trinary.MustTrytesToTrits( + bundle[i].Address + + trinary.MustTritsToTrytes(valueTrits[i]) + + trinary.MustTritsToTrytes(obsoleteTagTrits[i]) + + trinary.MustTritsToTrytes(timestampTrits[i]) + + trinary.MustTritsToTrytes(currentIndexTrits[i]) + + trinary.MustTritsToTrytes(lastIndexTrits), + ) + k.Absorb(relevantTritsForBundleHash) + } + + bundleHashTrits, err := k.Squeeze(consts.HashTrinarySize) + if err != nil { + return nil, err + } + bundleHash = trinary.MustTritsToTrytes(bundleHashTrits) + + // set the computed bundle hash on each tx in the bundle + for i := range bundle { + tx := &bundle[i] + tx.Bundle = bundleHash + } + + return bundle, nil +} + +func createBundle(address string, msg string, tagSubstring string, txCount int, additionalMesssage ...string) (bundle.Bundle, error) { + + tag, err := trinary.NewTrytes(tagSubstring + integerToAscii(txCount)) + if err != nil { + return nil, fmt.Errorf("NewTrytes: %v", err.Error()) + } + now := time.Now() + + messageString := msg + fmt.Sprintf("\nCount: %06d", txCount) + messageString += fmt.Sprintf("\nTimestamp: %s", now.Format(time.RFC3339)) + if len(additionalMesssage) > 0 { + messageString = fmt.Sprintf("%v\n%v", messageString, additionalMesssage[0]) + } + + message, err := converter.ASCIIToTrytes(messageString) + if err != nil { + return nil, fmt.Errorf("ASCIIToTrytes: %v", err.Error()) + } + + timestamp := uint64(now.UnixNano() / int64(time.Millisecond)) + + var b bundle.Bundle + + outEntry := bundle.BundleEntry{ + Address: address, + Value: 0, + Tag: tag, + Timestamp: timestamp, + Length: uint64(1), + SignatureMessageFragments: []trinary.Trytes{trinary.Pad(message, consts.SignatureMessageFragmentSizeInTrytes)}, + } + b = bundle.AddEntry(b, outEntry) + + // finalize bundle by adding the bundle hash + b, err = finalizeInsecure(b) + if err != nil { + return nil, fmt.Errorf("Bundle.Finalize: %v", err.Error()) + } + + return b, nil +} diff --git a/plugins/spammer/parameters.go b/plugins/spammer/parameters.go new file mode 100644 index 000000000..7951b3756 --- /dev/null +++ b/plugins/spammer/parameters.go @@ -0,0 +1,12 @@ +package spammer + +import flag "github.com/spf13/pflag" + +func init() { + flag.String("spammer.address", "HORNET99INTEGRATED99SPAMMER999999999999999999999999999999999999999999999999999999", "Tx Address") + flag.String("spammer.message", "Spamming with HORNET tipselect", "Message of the Tx") + flag.String("spammer.tag", "HORNET99INTEGRATED99SPAMMER", "Tag of the Tx") + flag.Uint("spammer.depth", 3, "Depth of the random walker") + flag.Float64("spammer.tpsRateLimit", 0.10, "Rate limit for the spam (0 = no limit)") + flag.Uint("spammer.workers", 1, "How many spammers should run in parallel") +} diff --git a/plugins/spammer/plugin.go b/plugins/spammer/plugin.go new file mode 100644 index 000000000..b82fe2bfd --- /dev/null +++ b/plugins/spammer/plugin.go @@ -0,0 +1,92 @@ +package spammer + +import ( + "runtime" + "time" + + "github.com/gohornet/hornet/packages/logger" + "github.com/gohornet/hornet/packages/model/tangle" + "github.com/gohornet/hornet/packages/node" + "github.com/gohornet/hornet/packages/shutdown" + "github.com/gohornet/hornet/packages/timeutil" + daemon "github.com/iotaledger/hive.go/daemon/ordered" + "github.com/iotaledger/hive.go/parameter" + "github.com/iotaledger/iota.go/consts" + "github.com/iotaledger/iota.go/trinary" +) + +var ( + PLUGIN = node.NewPlugin("Spammer", node.Disabled, configure, run) + log = logger.NewLogger("Spammer") + + address string + message string + tagSubstring string + depth uint + rateLimit float64 + mwm int + spammerWorkerCount int +) + +func configure(plugin *node.Plugin) { + + address = trinary.Pad(parameter.NodeConfig.GetString("spammer.address"), consts.AddressTrinarySize/3)[:consts.AddressTrinarySize/3] + message = parameter.NodeConfig.GetString("spammer.message") + tagSubstring = trinary.Pad(parameter.NodeConfig.GetString("spammer.tag"), consts.TagTrinarySize/3)[:consts.TagTrinarySize/3] + depth = parameter.NodeConfig.GetUint("spammer.depth") + rateLimit = parameter.NodeConfig.GetFloat64("spammer.tpsRateLimit") + mwm = parameter.NodeConfig.GetInt("protocol.mwm") + spammerWorkerCount = int(parameter.NodeConfig.GetUint("spammer.workers")) + + if spammerWorkerCount >= runtime.NumCPU() { + spammerWorkerCount = runtime.NumCPU() - 1 + } + if spammerWorkerCount < 1 { + spammerWorkerCount = 1 + } + + if int64(rateLimit) != 0 { + rateLimitChannelSize := int64(rateLimit) * 2 + if rateLimitChannelSize < 2 { + rateLimitChannelSize = 2 + } + rateLimitChannel = make(chan struct{}, rateLimitChannelSize) + + // create a background worker that fills rateLimitChannel every second + daemon.BackgroundWorker("Spammer rate limit channel", func(shutdownSignal <-chan struct{}) { + timeutil.Ticker(func() { + select { + case rateLimitChannel <- struct{}{}: + default: + // Channel full + } + }, time.Duration(int64(float64(time.Second)/rateLimit)), shutdownSignal) + }, shutdown.ShutdownPrioritySpammer) + } + + if len(tagSubstring) > 20 { + tagSubstring = string([]rune(tagSubstring)[:20]) + } +} + +func run(plugin *node.Plugin) { + + for i := 0; i < spammerWorkerCount; i++ { + daemon.BackgroundWorker("Spammer", func(shutdownSignal <-chan struct{}) { + log.Infof("Starting Spammer %d... done", i) + + for { + select { + case <-shutdownSignal: + log.Infof("Stopping Spammer %d...", i) + log.Infof("Stopping Spammer %d... done", i) + return + default: + if tangle.IsNodeSynced() { + doSpam(shutdownSignal) + } + } + } + }, shutdown.ShutdownPrioritySpammer) + } +} diff --git a/plugins/spammer/spammer.go b/plugins/spammer/spammer.go new file mode 100644 index 000000000..5dbdf862e --- /dev/null +++ b/plugins/spammer/spammer.go @@ -0,0 +1,125 @@ +package spammer + +import ( + "fmt" + "time" + + "github.com/iotaledger/iota.go/bundle" + "github.com/iotaledger/iota.go/consts" + "github.com/iotaledger/iota.go/pow" + "github.com/iotaledger/iota.go/transaction" + "github.com/iotaledger/iota.go/trinary" + + "github.com/gohornet/hornet/packages/compressed" + "github.com/gohornet/hornet/packages/curl" + "github.com/gohornet/hornet/packages/model/hornet" + "github.com/gohornet/hornet/plugins/gossip" + "github.com/gohornet/hornet/plugins/tipselection" +) + +var ( + _, powFunc = pow.GetFastestProofOfWorkImpl() + rateLimitChannel chan struct{} + txCount = 0 +) + +func doSpam(shutdownSignal <-chan struct{}) { + + if int64(rateLimit) != 0 { + select { + case <-shutdownSignal: + return + case <-rateLimitChannel: + } + } + + timeGTTA := time.Now() + tips, _, err := tipselection.SelectTips(depth, nil) + if err != nil { + return + } + durationGTTA := time.Since(timeGTTA) + + txCount++ + infoMsg := fmt.Sprintf("gTTA took %v (depth=%v)", durationGTTA.Truncate(time.Millisecond), depth) + b, err := createBundle(address, message, tagSubstring, txCount, infoMsg) + if err != nil { + return + } + + err = doPow(b, tips[0], tips[1], mwm) + if err != nil { + return + } + + for _, tx := range b { + err = broadcastTransaction(&tx) + if err != nil { + return + } + } +} + +// transactionHash makes a transaction hash from the given transaction. +func transactionHash(t *transaction.Transaction) trinary.Hash { + trits, _ := transaction.TransactionToTrits(t) + hashTrits := curl.CURLP81.Hash(trits) + return trinary.MustTritsToTrytes(hashTrits) +} + +func doPow(b bundle.Bundle, trunk trinary.Hash, branch trinary.Hash, mwm int) error { + var prev trinary.Hash + + for i := len(b) - 1; i >= 0; i-- { + switch { + case i == len(b)-1: + // Last tx in the bundle + b[i].TrunkTransaction = trunk + b[i].BranchTransaction = branch + default: + b[i].TrunkTransaction = prev + b[i].BranchTransaction = trunk + } + + b[i].AttachmentTimestamp = time.Now().UnixNano() / int64(time.Millisecond) + b[i].AttachmentTimestampLowerBound = consts.LowerBoundAttachmentTimestamp + b[i].AttachmentTimestampUpperBound = consts.UpperBoundAttachmentTimestamp + + trytes, err := transaction.TransactionToTrytes(&b[i]) + if err != nil { + return err + } + + nonce, err := powFunc(trytes, mwm) + if err != nil { + return err + } + + b[i].Nonce = nonce + + // set new transaction hash + b[i].Hash = transactionHash(&b[i]) + prev = b[i].Hash + } + return nil +} + +func broadcastTransaction(tx *transaction.Transaction) error { + + if !transaction.HasValidNonce(tx, uint64(mwm)) { + return consts.ErrInvalidTransactionHash + } + + txTrits, err := transaction.TransactionToTrits(tx) + if err != nil { + return err + } + + txBytesTruncated := compressed.TruncateTx(trinary.TritsToBytes(txTrits)) + hornetTx := hornet.NewTransactionFromAPI(tx, txBytesTruncated) + + gossip.Events.ReceivedTransaction.Trigger(hornetTx) + gossip.BroadcastTransaction(make(map[string]struct{}), txBytesTruncated, trinary.MustTrytesToBytes(hornetTx.GetHash())[:49]) + + return nil +}