diff --git a/dapps/faucet/dapp.go b/dapps/faucet/dapp.go index fff78faa8d426dd150777ced9f246bc736df744a..2c930afc9dd428cd0fd6f0124a9f6a0d47642da0 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 d4e5788eb386969a6251c05a3272b32b7a275586..fbbcc66c034d5d6c556cd978dd7c1fb63c2b8556 100644 --- a/dapps/faucet/packages/faucet.go +++ b/dapps/faucet/packages/faucet.go @@ -1,6 +1,7 @@ package faucet import ( + "errors" "fmt" "sync" "time" @@ -13,16 +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" ) +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, } } @@ -36,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. @@ -46,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() @@ -90,6 +128,8 @@ func (f *Faucet) SendFunds(msg *message.Message) (m *message.Message, txID strin return nil, "", fmt.Errorf("%w: tx %s", err, tx.ID().String()) } + f.addAddressToBlacklist(addr) + return msg, tx.ID().String(), nil } diff --git a/dapps/faucet/packages/faucet_test.go b/dapps/faucet/packages/faucet_test.go index 9f56fc2e130fef818d3a9dba590641ffb127048c..f321c74035f2e85a006312f0007f432cc8745397 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 264aa31134635cf2ba81670eadae8289efe16618..ba5474352b902c7c1cc3205aacdbe3728a216729 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 40684e9879ba132e27508ace9dd9ce9c031d0376..fe9d3b8693acd62fd4885698560a5c6486ce8476 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/plugins/dashboard/faucet_routes.go b/plugins/dashboard/faucet_routes.go index efaf0c4945f61adad9d1e647f7fdb0f72e83221e..41e115493eb1bb001d5bca6bec65e35635bbdf4e 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/webapi/faucet/plugin.go b/plugins/webapi/faucet/plugin.go index 74d2c796f59c61d6e2dc1af8b31db619b3672ba3..791bc2e4767a6039038e91660a0343a905c456fe 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/tools/integration-tests/tester/framework/docker.go b/tools/integration-tests/tester/framework/docker.go index cbf017797caf4426122ff700ac075ecd6ce6be44..0b5d688f84d72d10b7cd092840df917c0923f83b 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 9fa13fe839cefc05874ce95917f054d1e8d1f40d..9694ed97ec0c1f404f32583f8fa3305d1807036d 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 42aa18741dbfbcd9967bab878feeb77b2e1a12a8..c337b7daac7eef6061cf9f208a00fd3c923a84a6 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()) } }