diff --git a/.goreleaser.yml b/.goreleaser.yml index 50e6ac8059..d2686e2681 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -76,6 +76,7 @@ archives: - README.md - LICENSE - config.json + - snapshot.bin # Checksum checksum: diff --git a/CHANGELOG.md b/CHANGELOG.md index 78697c4c20..8e606a4cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v0.2.1 - 2020-07-01 +* Adds PoW requirement to faucet payloads +* Adds tips broadcaster to ensure that all chains are getting solidified +* Fixes being able to send a double-spend via one node +* **Breaking**: bumps network and database versions + # v0.2.0 - 2020-06-30 * Adds the value transfer dApp: * New binary transaction layout diff --git a/README.md b/README.md index ad69bd3d9a..0349a0f82d 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ You can find more info about this on our [client-lib](https://github.com/iotaled ## Getting started -You can find tutorials on how to [setup a GoShimmer node](https://github.com/iotaledger/goshimmer/wiki/Setting-up-a-GoShimmer-node), [writing a dApp](https://github.com/iotaledger/goshimmer/wiki/How-to-create-a-simple-dApp), [obtaining tokens from the faucet](https://github.com/iotaledger/goshimmer/wiki/How-to-obtain-tokens-from-the-faucet) and more on our [wiki](https://github.com/iotaledger/goshimmer/wiki). +You can find tutorials on how to [setup a GoShimmer node](https://github.com/iotaledger/goshimmer/wiki/Setup-up-a-GoShimmer-node-(Joining-the-pollen-testnet)), [writing a dApp](https://github.com/iotaledger/goshimmer/wiki/How-to-create-a-simple-dApp), [obtaining tokens from the faucet](https://github.com/iotaledger/goshimmer/wiki/How-to-obtain-tokens-from-the-faucet) and more on our [wiki](https://github.com/iotaledger/goshimmer/wiki). ## Supporting the project diff --git a/config.default.json b/config.default.json index 5f172cac79..924792efdd 100644 --- a/config.default.json +++ b/config.default.json @@ -40,7 +40,10 @@ "bindAddress": "0.0.0.0:10895" }, "gossip": { - "port": 14666 + "port": 14666, + "tipsBroadcaster": { + "interval": "10s" + } }, "logger": { "level": "info", diff --git a/dapps/faucet/dapp.go b/dapps/faucet/dapp.go index fff78faa8d..2c930afc9d 100644 --- a/dapps/faucet/dapp.go +++ b/dapps/faucet/dapp.go @@ -1,6 +1,7 @@ package faucet import ( + "crypto" "runtime" "sync" "time" @@ -9,6 +10,7 @@ import ( faucetpayload "github.com/iotaledger/goshimmer/dapps/faucet/packages/payload" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/tangle" + "github.com/iotaledger/goshimmer/packages/pow" "github.com/iotaledger/goshimmer/packages/shutdown" "github.com/iotaledger/goshimmer/plugins/config" "github.com/iotaledger/goshimmer/plugins/messagelayer" @@ -32,12 +34,19 @@ const ( // CfgFaucetMaxTransactionBookedAwaitTimeSeconds defines the time to await for the transaction fulfilling a funding request // to become booked in the value layer. CfgFaucetMaxTransactionBookedAwaitTimeSeconds = "faucet.maxTransactionBookedAwaitTimeSeconds" + // CfgFaucetPoWDifficulty defines the PoW difficulty for faucet payloads. + CfgFaucetPoWDifficulty = "faucet.powDifficulty" + // CfgFaucetBlacklistCapacity holds the maximum amount the address blacklist holds. + // An address for which a funding was done in the past is added to the blacklist and eventually is removed from it. + CfgFaucetBlacklistCapacity = "faucet.blacklistCapacity" ) func init() { flag.String(CfgFaucetSeed, "", "the base58 encoded seed of the faucet, must be defined if this dApp is enabled") flag.Int(CfgFaucetTokensPerRequest, 1337, "the amount of tokens the faucet should send for each request") - flag.Int(CfgFaucetMaxTransactionBookedAwaitTimeSeconds, 5, "the max amount of time for a funding transaction to become booked in the value layer.") + flag.Int(CfgFaucetMaxTransactionBookedAwaitTimeSeconds, 5, "the max amount of time for a funding transaction to become booked in the value layer") + flag.Int(CfgFaucetPoWDifficulty, 25, "defines the PoW difficulty for faucet payloads") + flag.Int(CfgFaucetBlacklistCapacity, 10000, "holds the maximum amount the address blacklist holds") } var ( @@ -47,6 +56,7 @@ var ( _faucet *faucet.Faucet faucetOnce sync.Once log *logger.Logger + powVerifier = pow.New(crypto.BLAKE2b_512) fundingWorkerPool *workerpool.WorkerPool fundingWorkerCount = runtime.GOMAXPROCS(0) fundingWorkerQueueSize = 500 @@ -79,7 +89,8 @@ func Faucet() *faucet.Faucet { if maxTxBookedAwaitTime <= 0 { log.Fatalf("the max transaction booked await time must be more than 0") } - _faucet = faucet.New(seedBytes, tokensPerRequest, time.Duration(maxTxBookedAwaitTime)*time.Second) + blacklistCapacity := config.Node().GetInt(CfgFaucetBlacklistCapacity) + _faucet = faucet.New(seedBytes, tokensPerRequest, blacklistCapacity, time.Duration(maxTxBookedAwaitTime)*time.Second) }) return _faucet } @@ -93,7 +104,7 @@ func configure(*node.Plugin) { addr := msg.Payload().(*faucetpayload.Payload).Address() msg, txID, err := Faucet().SendFunds(msg) if err != nil { - log.Errorf("couldn't fulfill funding request to %s: %s", addr, err) + log.Warnf("couldn't fulfill funding request to %s: %s", addr, err) return } log.Infof("sent funds to address %s via tx %s and msg %s", addr, txID, msg.Id().String()) @@ -122,7 +133,26 @@ func configureEvents() { return } - addr := msg.Payload().(*faucetpayload.Payload).Address() + fundingRequest := msg.Payload().(*faucetpayload.Payload) + addr := fundingRequest.Address() + if Faucet().IsAddressBlacklisted(addr) { + log.Debugf("can't fund address %s since it is blacklisted", addr) + return + } + + // verify PoW + leadingZeroes, err := powVerifier.LeadingZeros(fundingRequest.Bytes()) + if err != nil { + log.Warnf("couldn't verify PoW of funding request for address %s", addr) + return + } + targetPoWDifficulty := config.Node().GetInt(CfgFaucetPoWDifficulty) + if leadingZeroes < targetPoWDifficulty { + log.Debugf("funding request for address %s doesn't fulfill PoW requirement %d vs. %d", addr, targetPoWDifficulty, leadingZeroes) + return + } + + // finally add it to the faucet to be processed _, added := fundingWorkerPool.TrySubmit(msg) if !added { log.Info("dropped funding request for address %s as queue is full", addr) diff --git a/dapps/faucet/packages/faucet.go b/dapps/faucet/packages/faucet.go index dd40e51fff..fbbcc66c03 100644 --- a/dapps/faucet/packages/faucet.go +++ b/dapps/faucet/packages/faucet.go @@ -14,23 +14,27 @@ import ( "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" + "github.com/iotaledger/goshimmer/packages/binary/datastructure/orderedmap" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" "github.com/iotaledger/goshimmer/plugins/issuer" - "github.com/iotaledger/hive.go/events" ) var ( // ErrFundingTxNotBookedInTime is returned when a funding transaction didn't get booked // by this node in the maximum defined await time for it to get booked. ErrFundingTxNotBookedInTime = errors.New("funding transaction didn't get booked in time") + // ErrAddressIsBlacklisted is returned if a funding can't be processed since the address is blacklisted. + ErrAddressIsBlacklisted = errors.New("can't fund address as it is blacklisted") ) // New creates a new faucet using the given seed and tokensPerRequest config. -func New(seed []byte, tokensPerRequest int64, maxTxBookedAwaitTime time.Duration) *Faucet { +func New(seed []byte, tokensPerRequest int64, blacklistCapacity int, maxTxBookedAwaitTime time.Duration) *Faucet { return &Faucet{ tokensPerRequest: tokensPerRequest, wallet: wallet.New(seed), maxTxBookedAwaitTime: maxTxBookedAwaitTime, + blacklist: orderedmap.New(), + blacklistCapacity: blacklistCapacity, } } @@ -44,6 +48,28 @@ type Faucet struct { // the time to await for the transaction fulfilling a funding request // to become booked in the value layer maxTxBookedAwaitTime time.Duration + blacklistCapacity int + blacklist *orderedmap.OrderedMap +} + +// IsAddressBlacklisted checks whether the given address is currently blacklisted. +func (f *Faucet) IsAddressBlacklisted(addr address.Address) bool { + _, blacklisted := f.blacklist.Get(addr) + return blacklisted +} + +// adds the given address to the blacklist and removes the oldest blacklist entry +// if it would go over capacity. +func (f *Faucet) addAddressToBlacklist(addr address.Address) { + f.blacklist.Set(addr, true) + if f.blacklist.Size() > f.blacklistCapacity { + var headKey interface{} + f.blacklist.ForEach(func(key, value interface{}) bool { + headKey = key + return false + }) + f.blacklist.Delete(headKey) + } } // SendFunds sends IOTA tokens to the address from faucet request. @@ -54,6 +80,10 @@ func (f *Faucet) SendFunds(msg *message.Message) (m *message.Message, txID strin addr := msg.Payload().(*faucetpayload.Payload).Address() + if f.IsAddressBlacklisted(addr) { + return nil, "", ErrAddressIsBlacklisted + } + // get the output ids for the inputs and remainder balance outputIds, addrsIndices, remainder := f.collectUTXOsForFunding() @@ -80,7 +110,10 @@ func (f *Faucet) SendFunds(msg *message.Message) (m *message.Message, txID strin } // prepare value payload with value factory - payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx) + payload, err := valuetransfers.ValueObjectFactory().IssueTransaction(tx) + if err != nil { + return nil, "", fmt.Errorf("failed to issue transaction: %w", err) + } // attach to message layer msg, err = issuer.IssuePayload(payload) @@ -91,40 +124,13 @@ func (f *Faucet) SendFunds(msg *message.Message) (m *message.Message, txID strin // block for a certain amount of time until we know that the transaction // actually got booked by this node itself // TODO: replace with an actual more reactive way - bookedInTime := f.awaitTransactionBooked(tx.ID(), f.maxTxBookedAwaitTime) - if !bookedInTime { - return nil, "", fmt.Errorf("%w: tx %s", ErrFundingTxNotBookedInTime, tx.ID().String()) + if err := valuetransfers.AwaitTransactionToBeBooked(tx.ID(), f.maxTxBookedAwaitTime); err != nil { + return nil, "", fmt.Errorf("%w: tx %s", err, tx.ID().String()) } - return msg, tx.ID().String(), nil -} + f.addAddressToBlacklist(addr) -// awaitTransactionBooked awaits maxAwait for the given transaction to get booked. -func (f *Faucet) awaitTransactionBooked(txID transaction.ID, maxAwait time.Duration) bool { - booked := make(chan struct{}, 1) - // exit is used to let the caller exit if for whatever - // reason the same transaction gets booked multiple times - exit := make(chan struct{}) - defer close(exit) - closure := events.NewClosure(func(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, decisionPending bool) { - defer cachedTransaction.Release() - defer cachedTransactionMetadata.Release() - if cachedTransaction.Unwrap().ID() != txID { - return - } - select { - case booked <- struct{}{}: - case <-exit: - } - }) - valuetransfers.Tangle().Events.TransactionBooked.Attach(closure) - defer valuetransfers.Tangle().Events.TransactionBooked.Detach(closure) - select { - case <-time.After(maxAwait): - return false - case <-booked: - return true - } + return msg, tx.ID().String(), nil } // collectUTXOsForFunding iterates over the faucet's UTXOs until the token threshold is reached. diff --git a/dapps/faucet/packages/faucet_test.go b/dapps/faucet/packages/faucet_test.go index 9f56fc2e13..f321c74035 100644 --- a/dapps/faucet/packages/faucet_test.go +++ b/dapps/faucet/packages/faucet_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/iotaledger/hive.go/crypto/ed25519" "github.com/iotaledger/hive.go/identity" @@ -19,13 +20,18 @@ func TestIsFaucetReq(t *testing.T) { keyPair := ed25519.GenerateKeyPair() local := identity.NewLocalIdentity(keyPair.PublicKey, keyPair.PrivateKey) + faucetPayload, err := faucet.New(address.Random(), 4) + if err != nil { + require.NoError(t, err) + return + } faucetMsg := message.New( message.EmptyId, message.EmptyId, time.Now(), local.PublicKey(), 0, - faucet.New(address.Random()), + faucetPayload, 0, ed25519.EmptySignature, ) diff --git a/dapps/faucet/packages/payload/payload.go b/dapps/faucet/packages/payload/payload.go index 264aa31134..ba5474352b 100644 --- a/dapps/faucet/packages/payload/payload.go +++ b/dapps/faucet/packages/payload/payload.go @@ -1,7 +1,13 @@ package faucetpayload import ( + "context" + "crypto" + + _ "golang.org/x/crypto/blake2b" + "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" + "github.com/iotaledger/goshimmer/packages/pow" "github.com/iotaledger/hive.go/marshalutil" "github.com/iotaledger/hive.go/stringify" @@ -18,17 +24,28 @@ const ( type Payload struct { payloadType payload.Type address address.Address + nonce uint64 } // Type represents the identifier for the faucet Payload type. var Type = payload.Type(2) +var powWorker = pow.New(crypto.BLAKE2b_512, 1) // New is the constructor of a Payload and creates a new Payload object from the given details. -func New(addr address.Address) *Payload { - return &Payload{ +func New(addr address.Address, powTarget int) (*Payload, error) { + p := &Payload{ payloadType: Type, address: addr, } + + payloadBytes := p.Bytes() + powRelevantBytes := payloadBytes[:len(payloadBytes)-pow.NonceBytes] + nonce, err := powWorker.Mine(context.Background(), powRelevantBytes, powTarget) + if err != nil { + return nil, err + } + p.nonce = nonce + return p, nil } func init() { @@ -56,15 +73,22 @@ func FromBytes(bytes []byte, optionalTargetObject ...*Payload) (result *Payload, if err != nil { return } - payloadBytes, err := marshalUtil.ReadUint32() + if _, err = marshalUtil.ReadUint32(); err != nil { + return + } + addr, err := marshalUtil.ReadBytes(address.Length) if err != nil { return } - addr, err := marshalUtil.ReadBytes(int(payloadBytes)) + result.address, _, err = address.FromBytes(addr) + if err != nil { + return + } + + result.nonce, err = marshalUtil.ReadUint64() if err != nil { return } - result.address, _, _ = address.FromBytes(addr) // return the number of bytes we processed consumedBytes = marshalUtil.ReadOffset() @@ -89,8 +113,9 @@ func (faucetPayload *Payload) Bytes() []byte { // marshal the payload specific information marshalUtil.WriteUint32(faucetPayload.Type()) - marshalUtil.WriteUint32(uint32(len(faucetPayload.address))) + marshalUtil.WriteUint32(uint32(address.Length + pow.NonceBytes)) marshalUtil.WriteBytes(faucetPayload.address.Bytes()) + marshalUtil.WriteUint64(faucetPayload.nonce) // return result return marshalUtil.Bytes() diff --git a/dapps/faucet/packages/payload/payload_test.go b/dapps/faucet/packages/payload/payload_test.go index 40684e9879..fe9d3b8693 100644 --- a/dapps/faucet/packages/payload/payload_test.go +++ b/dapps/faucet/packages/payload/payload_test.go @@ -5,10 +5,9 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/iotaledger/hive.go/crypto/ed25519" "github.com/iotaledger/hive.go/identity" + "github.com/stretchr/testify/assert" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" @@ -19,10 +18,10 @@ func ExamplePayload() { local := identity.NewLocalIdentity(keyPair.PublicKey, keyPair.PrivateKey) // 1. create faucet payload - faucetPayload := New( - // request address - address.Random(), - ) + faucetPayload, err := New(address.Random(), 4) + if err != nil { + panic(err) + } // 2. build actual message tx := message.New( @@ -39,7 +38,10 @@ func ExamplePayload() { } func TestPayload(t *testing.T) { - originalPayload := New(address.Random()) + originalPayload, err := New(address.Random(), 4) + if err != nil { + panic(err) + } clonedPayload1, err, _ := FromBytes(originalPayload.Bytes()) if err != nil { diff --git a/dapps/valuetransfers/dapp.go b/dapps/valuetransfers/dapp.go index e16014d0ac..60adff59bb 100644 --- a/dapps/valuetransfers/dapp.go +++ b/dapps/valuetransfers/dapp.go @@ -1,6 +1,7 @@ package valuetransfers import ( + "errors" "os" "sync" "time" @@ -10,6 +11,7 @@ import ( valuepayload "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tipmanager" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" messageTangle "github.com/iotaledger/goshimmer/packages/binary/messagelayer/tangle" "github.com/iotaledger/goshimmer/packages/shutdown" @@ -44,6 +46,10 @@ func init() { } var ( + // ErrTransactionWasNotBookedInTime is returned if a transaction did not get booked + // within the defined await time. + ErrTransactionWasNotBookedInTime = errors.New("transaction could not be booked in time") + // app is the "plugin" instance of the value-transfers application. app *node.Plugin appOnce sync.Once @@ -217,7 +223,35 @@ func TipManager() *tipmanager.TipManager { // ValueObjectFactory returns the ValueObjectFactory singleton. func ValueObjectFactory() *tangle.ValueObjectFactory { valueObjectFactoryOnce.Do(func() { - valueObjectFactory = tangle.NewValueObjectFactory(TipManager()) + valueObjectFactory = tangle.NewValueObjectFactory(Tangle(), TipManager()) }) return valueObjectFactory } + +// AwaitTransactionToBeBooked awaits maxAwait for the given transaction to get booked. +func AwaitTransactionToBeBooked(txID transaction.ID, maxAwait time.Duration) error { + booked := make(chan struct{}, 1) + // exit is used to let the caller exit if for whatever + // reason the same transaction gets booked multiple times + exit := make(chan struct{}) + defer close(exit) + closure := events.NewClosure(func(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, decisionPending bool) { + defer cachedTransaction.Release() + defer cachedTransactionMetadata.Release() + if cachedTransaction.Unwrap().ID() != txID { + return + } + select { + case booked <- struct{}{}: + case <-exit: + } + }) + Tangle().Events.TransactionBooked.Attach(closure) + defer Tangle().Events.TransactionBooked.Detach(closure) + select { + case <-time.After(maxAwait): + return ErrTransactionWasNotBookedInTime + case <-booked: + return nil + } +} diff --git a/dapps/valuetransfers/packages/tangle/errors.go b/dapps/valuetransfers/packages/tangle/errors.go index 3ada3b8ae0..935661ea0a 100644 --- a/dapps/valuetransfers/packages/tangle/errors.go +++ b/dapps/valuetransfers/packages/tangle/errors.go @@ -11,4 +11,7 @@ var ( // ErrPayloadInvalid represents an error type that is triggered when an invalid payload is detected. ErrPayloadInvalid = errors.New("payload invalid") + + // ErrDoubleSpendForbidden represents an error that is triggered when a user tries to issue a double spend. + ErrDoubleSpendForbidden = errors.New("it is not allowed to issue a double spend") ) diff --git a/dapps/valuetransfers/packages/tangle/factory.go b/dapps/valuetransfers/packages/tangle/factory.go index 6720e5d2f9..106ee55a5b 100644 --- a/dapps/valuetransfers/packages/tangle/factory.go +++ b/dapps/valuetransfers/packages/tangle/factory.go @@ -9,13 +9,15 @@ import ( // ValueObjectFactory acts as a factory to create new value objects. type ValueObjectFactory struct { + tangle *Tangle tipManager *tipmanager.TipManager Events *ValueObjectFactoryEvents } // NewValueObjectFactory creates a new ValueObjectFactory. -func NewValueObjectFactory(tipManager *tipmanager.TipManager) *ValueObjectFactory { +func NewValueObjectFactory(tangle *Tangle, tipManager *tipmanager.TipManager) *ValueObjectFactory { return &ValueObjectFactory{ + tangle: tangle, tipManager: tipManager, Events: &ValueObjectFactoryEvents{ ValueObjectConstructed: events.NewEvent(valueObjectConstructedEvent), @@ -25,13 +27,27 @@ func NewValueObjectFactory(tipManager *tipmanager.TipManager) *ValueObjectFactor // IssueTransaction creates a new value object including tip selection and returns it. // It also triggers the ValueObjectConstructed event once it's done. -func (v *ValueObjectFactory) IssueTransaction(tx *transaction.Transaction) *payload.Payload { +func (v *ValueObjectFactory) IssueTransaction(tx *transaction.Transaction) (valueObject *payload.Payload, err error) { parent1, parent2 := v.tipManager.Tips() - valueObject := payload.New(parent1, parent2, tx) + // check if the tx that is supposed to be issued is a double spend + tx.Inputs().ForEach(func(outputId transaction.OutputID) bool { + v.tangle.TransactionOutput(outputId).Consume(func(output *Output) { + if output.ConsumerCount() >= 1 { + err = ErrDoubleSpendForbidden + } + }) + + return err == nil + }) + if err != nil { + return + } + + valueObject = payload.New(parent1, parent2, tx) v.Events.ValueObjectConstructed.Trigger(valueObject) - return valueObject + return } // ValueObjectFactoryEvents represent events happening on a ValueObjectFactory. diff --git a/dapps/valuetransfers/packages/transaction/inputs.go b/dapps/valuetransfers/packages/transaction/inputs.go index bfc8ea1398..f5071c62ce 100644 --- a/dapps/valuetransfers/packages/transaction/inputs.go +++ b/dapps/valuetransfers/packages/transaction/inputs.go @@ -104,7 +104,7 @@ func (inputs *Inputs) Bytes() (bytes []byte) { // ForEach iterates through the referenced Outputs and calls the consumer function for every Output. The iteration can // be aborted by returning false in the consumer. -func (inputs *Inputs) ForEach(consumer func(outputId OutputID) bool) bool { +func (inputs *Inputs) ForEach(consumer func(outputID OutputID) bool) bool { return inputs.OrderedMap.ForEach(func(key, value interface{}) bool { return value.(*orderedmap.OrderedMap).ForEach(func(key, value interface{}) bool { return consumer(value.(OutputID)) diff --git a/plugins/autopeering/autopeering.go b/plugins/autopeering/autopeering.go index 4b3707cc7a..49a78a06a4 100644 --- a/plugins/autopeering/autopeering.go +++ b/plugins/autopeering/autopeering.go @@ -25,7 +25,7 @@ import ( // autopeering constants const ( ProtocolVersion = 0 // update on protocol changes - NetworkVersion = 2 // update on network changes + NetworkVersion = 3 // update on network changes ) var ( diff --git a/plugins/banner/plugin.go b/plugins/banner/plugin.go index 5603df69d1..6229660976 100644 --- a/plugins/banner/plugin.go +++ b/plugins/banner/plugin.go @@ -18,7 +18,7 @@ var ( const ( // AppVersion version number - AppVersion = "v0.2.0" + AppVersion = "v0.2.1" // AppName app code name AppName = "GoShimmer" diff --git a/plugins/dashboard/faucet_routes.go b/plugins/dashboard/faucet_routes.go index efaf0c4945..41e115493e 100644 --- a/plugins/dashboard/faucet_routes.go +++ b/plugins/dashboard/faucet_routes.go @@ -2,9 +2,12 @@ package dashboard import ( "net/http" + "sync" + "github.com/iotaledger/goshimmer/dapps/faucet" faucetpayload "github.com/iotaledger/goshimmer/dapps/faucet/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/plugins/config" "github.com/iotaledger/goshimmer/plugins/messagelayer" "github.com/labstack/echo" @@ -32,8 +35,16 @@ func setupFaucetRoutes(routeGroup *echo.Group) { }) } +var fundingReqMu = sync.Mutex{} + func sendFaucetReq(addr address.Address) (res *ReqMsg, err error) { - msg := messagelayer.MessageFactory().IssuePayload(faucetpayload.New(addr)) + fundingReqMu.Lock() + defer fundingReqMu.Unlock() + faucetPayload, err := faucetpayload.New(addr, config.Node().GetInt(faucet.CfgFaucetPoWDifficulty)) + if err != nil { + return nil, err + } + msg := messagelayer.MessageFactory().IssuePayload(faucetPayload) if msg == nil { return nil, errors.Wrapf(ErrInternalError, "Fail to send faucet request") } diff --git a/plugins/database/versioning.go b/plugins/database/versioning.go index 0b62a5bf48..68b367b275 100644 --- a/plugins/database/versioning.go +++ b/plugins/database/versioning.go @@ -10,7 +10,7 @@ import ( const ( // DBVersion defines the version of the database schema this version of GoShimmer supports. // Every time there's a breaking change regarding the stored data, this version flag should be adjusted. - DBVersion = 3 + DBVersion = 4 ) var ( diff --git a/plugins/gossip/parameters.go b/plugins/gossip/parameters.go index ade8e8a22e..44ac5efa94 100644 --- a/plugins/gossip/parameters.go +++ b/plugins/gossip/parameters.go @@ -1,14 +1,20 @@ package gossip import ( + "time" + flag "github.com/spf13/pflag" ) const ( // CfgGossipPort defines the config flag of the gossip port. CfgGossipPort = "gossip.port" + + // CfgGossipTipsBroadcastInterval the interval in which the oldest known tip is re-broadcasted. + CfgGossipTipsBroadcastInterval = "gossip.tipsBroadcaster.interval" ) func init() { flag.Int(CfgGossipPort, 14666, "tcp port for gossip connection") + flag.Duration(CfgGossipTipsBroadcastInterval, 10*time.Second, "the interval in which the oldest known tip is re-broadcasted") } diff --git a/plugins/gossip/plugin.go b/plugins/gossip/plugin.go index d32ad25341..83f36f08c0 100644 --- a/plugins/gossip/plugin.go +++ b/plugins/gossip/plugin.go @@ -42,6 +42,7 @@ func configure(*node.Plugin) { configureLogging() configureMessageLayer() configureAutopeering() + configureTipBroadcaster() } func run(*node.Plugin) { diff --git a/plugins/gossip/tips_broadcaster.go b/plugins/gossip/tips_broadcaster.go new file mode 100644 index 0000000000..0ea6341f5e --- /dev/null +++ b/plugins/gossip/tips_broadcaster.go @@ -0,0 +1,126 @@ +package gossip + +import ( + "container/list" + "sync" + "time" + + "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" + "github.com/iotaledger/goshimmer/packages/shutdown" + "github.com/iotaledger/goshimmer/plugins/config" + "github.com/iotaledger/goshimmer/plugins/messagelayer" + "github.com/iotaledger/hive.go/daemon" + "github.com/iotaledger/hive.go/events" +) + +const ( + // the amount of oldest tips in the tip pool to broadcast up on each interval + maxOldestTipsToBroadcastPerInterval = 2 +) + +var tips = tiplist{dict: make(map[message.Id]*list.Element)} + +type tiplist struct { + mu sync.Mutex + + dict map[message.Id]*list.Element + list list.List + iterator *list.Element +} + +func (s *tiplist) AddTip(id message.Id) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, contains := s.dict[id]; contains { + return + } + + elem := s.list.PushBack(id) + s.dict[id] = elem + if s.iterator == nil { + s.iterator = elem + } +} + +func (s *tiplist) RemoveTip(id message.Id) { + s.mu.Lock() + defer s.mu.Unlock() + + elem, ok := s.dict[id] + if ok { + s.list.Remove(elem) + if s.iterator == elem { + s.next(elem) + } + } +} + +func (s *tiplist) Next() (id message.Id) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.iterator != nil { + id = s.iterator.Value.(message.Id) + s.next(s.iterator) + } + return +} + +func (s *tiplist) next(elem *list.Element) { + s.iterator = elem.Next() + if s.iterator == nil { + s.iterator = s.list.Front() + } +} + +func configureTipBroadcaster() { + tipSelector := messagelayer.TipSelector() + addedTipsClosure := events.NewClosure(tips.AddTip) + removedTipClosure := events.NewClosure(tips.RemoveTip) + tipSelector.Events.TipAdded.Attach(addedTipsClosure) + tipSelector.Events.TipRemoved.Attach(removedTipClosure) + + if err := daemon.BackgroundWorker("Tips-Broadcaster", func(shutdownSignal <-chan struct{}) { + log.Info("broadcaster started") + defer log.Info("broadcaster stopped") + defer tipSelector.Events.TipAdded.Detach(addedTipsClosure) + defer tipSelector.Events.TipRemoved.Detach(removedTipClosure) + ticker := time.NewTicker(config.Node().GetDuration(CfgGossipTipsBroadcastInterval)) + defer ticker.Stop() + for { + select { + case <-ticker.C: + broadcastOldestTips() + case <-shutdownSignal: + return + } + } + }, shutdown.PriorityGossip); err != nil { + log.Panicf("Couldn't create demon: %s", err) + } +} + +// broadcasts up to maxOldestTipsToBroadcastPerInterval tips from the tip pool +// to all connected neighbors. +func broadcastOldestTips() { + for toBroadcast := maxOldestTipsToBroadcastPerInterval; toBroadcast > 0; toBroadcast-- { + msgID := tips.Next() + if msgID == message.EmptyId { + break + } + log.Debugf("broadcasting tip %s", msgID) + broadcastMessage(msgID) + } +} + +// broadcasts the given message to all neighbors if it exists. +func broadcastMessage(msgID message.Id) { + cachedMessage := messagelayer.Tangle().Message(msgID) + defer cachedMessage.Release() + if !cachedMessage.Exists() { + return + } + msg := cachedMessage.Unwrap() + Manager().SendMessage(msg.Bytes()) +} diff --git a/plugins/webapi/faucet/plugin.go b/plugins/webapi/faucet/plugin.go index 74d2c796f5..791bc2e476 100644 --- a/plugins/webapi/faucet/plugin.go +++ b/plugins/webapi/faucet/plugin.go @@ -4,8 +4,10 @@ import ( "net/http" goSync "sync" + "github.com/iotaledger/goshimmer/dapps/faucet" faucetpayload "github.com/iotaledger/goshimmer/dapps/faucet/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/plugins/config" "github.com/iotaledger/goshimmer/plugins/messagelayer" "github.com/iotaledger/goshimmer/plugins/webapi" "github.com/iotaledger/hive.go/logger" @@ -20,9 +22,10 @@ const ( var ( // plugin is the plugin instance of the web API info endpoint plugin. - plugin *node.Plugin - once goSync.Once - log *logger.Logger + plugin *node.Plugin + once goSync.Once + log *logger.Logger + fundingMu goSync.Mutex ) // Plugin gets the plugin instance. @@ -41,6 +44,8 @@ func configure(plugin *node.Plugin) { // requestFunds creates a faucet request (0-value) message with the given destination address and // broadcasts it to the node's neighbors. It returns the message ID if successful. func requestFunds(c echo.Context) error { + fundingMu.Lock() + defer fundingMu.Unlock() var request Request var addr address.Address if err := c.Bind(&request); err != nil { @@ -55,8 +60,11 @@ func requestFunds(c echo.Context) error { return c.JSON(http.StatusBadRequest, Response{Error: "Invalid address"}) } - // build faucet message with transaction factory - msg := messagelayer.MessageFactory().IssuePayload(faucetpayload.New(addr)) + faucetPayload, err := faucetpayload.New(addr, config.Node().GetInt(faucet.CfgFaucetPoWDifficulty)) + if err != nil { + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + msg := messagelayer.MessageFactory().IssuePayload(faucetPayload) if msg == nil { return c.JSON(http.StatusInternalServerError, Response{Error: "Fail to send faucetrequest"}) } diff --git a/plugins/webapi/value/sendtransaction/handler.go b/plugins/webapi/value/sendtransaction/handler.go index dda59ab8c3..56bc66b2a8 100644 --- a/plugins/webapi/value/sendtransaction/handler.go +++ b/plugins/webapi/value/sendtransaction/handler.go @@ -2,6 +2,8 @@ package sendtransaction import ( "net/http" + "sync" + "time" "github.com/iotaledger/goshimmer/dapps/valuetransfers" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" @@ -9,8 +11,16 @@ import ( "github.com/labstack/echo" ) +var ( + sendTxMu sync.Mutex + maxBookedAwaitTime = 5 * time.Second +) + // Handler sends a transaction. func Handler(c echo.Context) error { + sendTxMu.Lock() + defer sendTxMu.Unlock() + var request Request if err := c.Bind(&request); err != nil { return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) @@ -28,12 +38,18 @@ func Handler(c echo.Context) error { } // Prepare value payload and send the message to tangle - payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx) + payload, err := valuetransfers.ValueObjectFactory().IssueTransaction(tx) + if err != nil { + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } _, err = issuer.IssuePayload(payload) if err != nil { return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } + if err := valuetransfers.AwaitTransactionToBeBooked(tx.ID(), maxBookedAwaitTime); err != nil { + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } return c.JSON(http.StatusOK, Response{TransactionID: tx.ID().String()}) } diff --git a/plugins/webapi/value/testsendtxn/handler.go b/plugins/webapi/value/testsendtxn/handler.go index 222e919ede..15b7a7673b 100644 --- a/plugins/webapi/value/testsendtxn/handler.go +++ b/plugins/webapi/value/testsendtxn/handler.go @@ -66,8 +66,11 @@ func Handler(c echo.Context) error { tx := transaction.New(inputs, outputs) // Prepare value payload and send the message to tangle - payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx) - _, err := issuer.IssuePayload(payload) + payload, err := valuetransfers.ValueObjectFactory().IssueTransaction(tx) + if err != nil { + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + _, err = issuer.IssuePayload(payload) if err != nil { return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } diff --git a/tools/integration-tests/tester/framework/docker.go b/tools/integration-tests/tester/framework/docker.go index cbf017797c..0b5d688f84 100644 --- a/tools/integration-tests/tester/framework/docker.go +++ b/tools/integration-tests/tester/framework/docker.go @@ -85,6 +85,7 @@ func (d *DockerContainer) CreateGoShimmerPeer(config GoShimmerConfig) error { fmt.Sprintf("--valueLayer.fcob.averageNetworkDelay=%d", ParaFCoBAverageNetworkDelay), fmt.Sprintf("--node.disablePlugins=%s", config.DisabledPlugins), fmt.Sprintf("--pow.difficulty=%d", ParaPoWDifficulty), + fmt.Sprintf("--faucet.powDifficulty=%d", ParaPoWFaucetDifficulty), fmt.Sprintf("--gracefulshutdown.waitToKillTime=%d", ParaWaitToKill), fmt.Sprintf("--node.enablePlugins=%s", func() string { var plugins []string diff --git a/tools/integration-tests/tester/framework/parameters.go b/tools/integration-tests/tester/framework/parameters.go index 9fa13fe839..9694ed97ec 100644 --- a/tools/integration-tests/tester/framework/parameters.go +++ b/tools/integration-tests/tester/framework/parameters.go @@ -37,6 +37,8 @@ var ( ParaPoWDifficulty = 2 // ParaWaitToKill defines the time to wait before killing the node. ParaWaitToKill = 60 + // ParaPoWFaucetDifficulty defines the PoW difficulty for faucet payloads. + ParaPoWFaucetDifficulty = 2 ) var ( diff --git a/tools/integration-tests/tester/tests/testutil.go b/tools/integration-tests/tester/tests/testutil.go index 42aa18741d..c337b7daac 100644 --- a/tools/integration-tests/tester/tests/testutil.go +++ b/tools/integration-tests/tester/tests/testutil.go @@ -9,7 +9,6 @@ import ( "testing" "time" - faucet_payload "github.com/iotaledger/goshimmer/dapps/faucet/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address/signaturescheme" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" @@ -81,33 +80,28 @@ func SendDataMessage(t *testing.T, peer *framework.Peer, data []byte, number int func SendFaucetRequestOnRandomPeer(t *testing.T, peers []*framework.Peer, numMessages int) (ids map[string]DataMessageSent, addrBalance map[string]map[balance.Color]int64) { ids = make(map[string]DataMessageSent, numMessages) addrBalance = make(map[string]map[balance.Color]int64) - for _, p := range peers { - addr := p.Seed().Address(0).String() - addrBalance[addr] = make(map[balance.Color]int64) - addrBalance[addr][balance.ColorIOTA] = 0 - } for i := 0; i < numMessages; i++ { peer := peers[rand.Intn(len(peers))] - id, sent := SendFaucetRequest(t, peer) - + addr := peer.Seed().Address(uint64(i)) + id, sent := SendFaucetRequest(t, peer, addr) ids[id] = sent - addrBalance[peer.Seed().Address(0).String()][balance.ColorIOTA] += framework.ParaFaucetTokensPerRequest + addrBalance[addr.String()] = map[balance.Color]int64{ + balance.ColorIOTA: framework.ParaFaucetTokensPerRequest, + } } return ids, addrBalance } // SendFaucetRequest sends a data message on a given peer and returns the id and a DataMessageSent struct. -func SendFaucetRequest(t *testing.T, peer *framework.Peer) (string, DataMessageSent) { - addr := peer.Seed().Address(0) +func SendFaucetRequest(t *testing.T, peer *framework.Peer, addr address.Address) (string, DataMessageSent) { resp, err := peer.SendFaucetRequest(addr.String()) require.NoErrorf(t, err, "Could not send faucet request on %s", peer.String()) sent := DataMessageSent{ - id: resp.ID, - // save payload to be able to compare API response - data: faucet_payload.New(addr).Bytes(), + id: resp.ID, + data: nil, issuerPublicKey: peer.Identity.PublicKey().String(), } return resp.ID, sent @@ -143,7 +137,9 @@ func CheckForMessageIds(t *testing.T, peers []*framework.Peer, ids map[string]Da msgSent := ids[msg.ID] assert.Equalf(t, msgSent.issuerPublicKey, msg.IssuerPublicKey, "messageID=%s, issuer=%s not correct issuer in %s.", msgSent.id, msgSent.issuerPublicKey, peer.String()) - assert.Equalf(t, msgSent.data, msg.Payload, "messageID=%s, issuer=%s data not equal in %s.", msgSent.id, msgSent.issuerPublicKey, peer.String()) + if msgSent.data != nil { + assert.Equalf(t, msgSent.data, msg.Payload, "messageID=%s, issuer=%s data not equal in %s.", msgSent.id, msgSent.issuerPublicKey, peer.String()) + } assert.Truef(t, msg.Metadata.Solid, "messageID=%s, issuer=%s not solid in %s.", msgSent.id, msgSent.issuerPublicKey, peer.String()) } }