diff --git a/config.default.json b/config.default.json
index 059be5f0ed5343b805a56f32c75fb3d558b5c8f4..ca9a59327bd521628e609bd4044ca56f7832dc2e 100644
--- a/config.default.json
+++ b/config.default.json
@@ -60,6 +60,11 @@
     "disablePlugins": [],
     "enablePlugins": []
   },
+  "pow": {
+    "difficulty": 22,
+    "numThreads": 1,
+    "timeout": "1m"
+  },
   "webapi": {
     "auth": {
       "password": "goshimmer",
diff --git a/dapps/valuetransfers/packages/payload/payload_test.go b/dapps/valuetransfers/packages/payload/payload_test.go
index fbc7c9690fc8fb0142a268bafcbaebf5fd4705b6..743e7310a0c3612e7f212df0834bdc5979230297 100644
--- a/dapps/valuetransfers/packages/payload/payload_test.go
+++ b/dapps/valuetransfers/packages/payload/payload_test.go
@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"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"
@@ -46,7 +45,6 @@ func ExamplePayload() {
 	)
 
 	// 3. build actual transaction (the base layer creates this and wraps the ontology provided payload)
-	localIdentity := identity.GenerateLocalIdentity()
 	tx := message.New(
 		// trunk in "network tangle" ontology (filled by tipSelector)
 		message.EmptyId,
@@ -54,17 +52,23 @@ func ExamplePayload() {
 		// branch in "network tangle" ontology (filled by tipSelector)
 		message.EmptyId,
 
-		// issuer of the transaction (signs automatically)
-		localIdentity,
-
 		// the time when the transaction was created
 		time.Now(),
 
+		// public key of the issuer
+		ed25519.PublicKey{},
+
 		// the ever increasing sequence number of this transaction
 		0,
 
 		// payload
 		valuePayload,
+
+		// nonce to check PoW
+		0,
+
+		// signature
+		ed25519.Signature{},
 	)
 
 	fmt.Println(tx)
diff --git a/dapps/valuetransfers/packages/tangle/signature_filter_test.go b/dapps/valuetransfers/packages/tangle/signature_filter_test.go
index 0dabaa30650947abc298cb069d28b0adfd89909b..4002da4add5d6724fa1dec9c0447ec110024eb46 100644
--- a/dapps/valuetransfers/packages/tangle/signature_filter_test.go
+++ b/dapps/valuetransfers/packages/tangle/signature_filter_test.go
@@ -29,7 +29,7 @@ func TestSignatureFilter(t *testing.T) {
 
 	// create helper instances
 	seed := wallet.NewSeed()
-	messageFactory := messagefactory.New(mapdb.NewMapDB(), identity.GenerateLocalIdentity(), tipselector.New(), []byte("sequenceKey"))
+	messageFactory := messagefactory.New(mapdb.NewMapDB(), []byte("sequenceKey"), identity.GenerateLocalIdentity(), tipselector.New())
 
 	// 1. test value message without signatures
 	{
diff --git a/packages/binary/messagelayer/message/message.go b/packages/binary/messagelayer/message/message.go
index 4d6cd6769b2126e3f64b4df018b93bc0beee8e15..6415cacba10e56ca2f779f08532a92f73842262e 100644
--- a/packages/binary/messagelayer/message/message.go
+++ b/packages/binary/messagelayer/message/message.go
@@ -5,7 +5,6 @@ import (
 	"time"
 
 	"github.com/iotaledger/hive.go/crypto/ed25519"
-	"github.com/iotaledger/hive.go/identity"
 	"github.com/iotaledger/hive.go/marshalutil"
 	"github.com/iotaledger/hive.go/objectstorage"
 	"github.com/iotaledger/hive.go/stringify"
@@ -20,38 +19,35 @@ type Message struct {
 	objectstorage.StorableObjectFlags
 
 	// core properties (get sent over the wire)
-	trunkId         Id
-	branchId        Id
+	trunkID         Id
+	branchID        Id
 	issuerPublicKey ed25519.PublicKey
 	issuingTime     time.Time
 	sequenceNumber  uint64
 	payload         payload.Payload
-	bytes           []byte
-	bytesMutex      sync.RWMutex
+	nonce           uint64
 	signature       ed25519.Signature
-	signatureMutex  sync.RWMutex
 
 	// derived properties
 	id             *Id
 	idMutex        sync.RWMutex
 	contentId      *ContentId
 	contentIdMutex sync.RWMutex
-
-	// only stored on the machine of the signer
-	issuerLocalIdentity *identity.LocalIdentity
+	bytes          []byte
+	bytesMutex     sync.RWMutex
 }
 
 // New creates a new message with the details provided by the issuer.
-func New(trunkMessageId Id, branchMessageId Id, localIdentity *identity.LocalIdentity, issuingTime time.Time, sequenceNumber uint64, payload payload.Payload) (result *Message) {
+func New(trunkID Id, branchID Id, issuingTime time.Time, issuerPublicKey ed25519.PublicKey, sequenceNumber uint64, payload payload.Payload, nonce uint64, signature ed25519.Signature) (result *Message) {
 	return &Message{
-		trunkId:         trunkMessageId,
-		branchId:        branchMessageId,
-		issuerPublicKey: localIdentity.PublicKey(),
+		trunkID:         trunkID,
+		branchID:        branchID,
+		issuerPublicKey: issuerPublicKey,
 		issuingTime:     issuingTime,
 		sequenceNumber:  sequenceNumber,
 		payload:         payload,
-
-		issuerLocalIdentity: localIdentity,
+		nonce:           nonce,
+		signature:       signature,
 	}
 }
 
@@ -113,10 +109,12 @@ func StorableObjectFromKey(key []byte, optionalTargetObject ...*Message) (result
 // VerifySignature verifies the signature of the message.
 func (message *Message) VerifySignature() bool {
 	msgBytes := message.Bytes()
-	message.signatureMutex.RLock()
-	valid := message.issuerPublicKey.VerifySignature(msgBytes[:len(msgBytes)-ed25519.SignatureSize], message.Signature())
-	message.signatureMutex.RUnlock()
-	return valid
+	signature := message.Signature()
+
+	contentLength := len(msgBytes) - len(signature)
+	content := msgBytes[:contentLength]
+
+	return message.issuerPublicKey.VerifySignature(content, signature)
 }
 
 // ID returns the id of the message which is made up of the content id and trunk/branch ids.
@@ -145,12 +143,12 @@ func (message *Message) Id() (result Id) {
 
 // TrunkID returns the id of the trunk message.
 func (message *Message) TrunkId() Id {
-	return message.trunkId
+	return message.trunkID
 }
 
 // BranchID returns the id of the branch message.
 func (message *Message) BranchId() Id {
-	return message.branchId
+	return message.branchID
 }
 
 // IssuerPublicKey returns the public key of the message issuer.
@@ -168,26 +166,21 @@ func (message *Message) SequenceNumber() uint64 {
 	return message.sequenceNumber
 }
 
-// Signature returns the signature of the message.
-func (message *Message) Signature() ed25519.Signature {
-	message.signatureMutex.RLock()
-	defer message.signatureMutex.RUnlock()
-
-	if message.signature == ed25519.EmptySignature {
-		// unlock the signatureMutex so Bytes() can write the Signature
-		message.signatureMutex.RUnlock()
-		message.Bytes()
-		message.signatureMutex.RLock()
-	}
-
-	return message.signature
-}
-
 // Payload returns the payload of the message.
 func (message *Message) Payload() payload.Payload {
 	return message.payload
 }
 
+// Payload returns the payload of the message.
+func (message *Message) Nonce() uint64 {
+	return message.nonce
+}
+
+// Signature returns the signature of the message.
+func (message *Message) Signature() ed25519.Signature {
+	return message.signature
+}
+
 // ContentId returns the content id of the message which is made up of all the
 // parts of the message minus the trunk and branch ids.
 func (message *Message) ContentId() (result ContentId) {
@@ -215,8 +208,8 @@ func (message *Message) ContentId() (result ContentId) {
 func (message *Message) calculateId() Id {
 	return blake2b.Sum512(
 		marshalutil.New(IdLength + IdLength + payload.IdLength).
-			WriteBytes(message.trunkId.Bytes()).
-			WriteBytes(message.branchId.Bytes()).
+			WriteBytes(message.trunkID.Bytes()).
+			WriteBytes(message.branchID.Bytes()).
 			WriteBytes(message.ContentId().Bytes()).
 			Bytes(),
 	)
@@ -247,17 +240,13 @@ func (message *Message) Bytes() []byte {
 
 	// marshal result
 	marshalUtil := marshalutil.New()
-	marshalUtil.WriteBytes(message.trunkId.Bytes())
-	marshalUtil.WriteBytes(message.branchId.Bytes())
+	marshalUtil.WriteBytes(message.trunkID.Bytes())
+	marshalUtil.WriteBytes(message.branchID.Bytes())
 	marshalUtil.WriteBytes(message.issuerPublicKey.Bytes())
 	marshalUtil.WriteTime(message.issuingTime)
 	marshalUtil.WriteUint64(message.sequenceNumber)
 	marshalUtil.WriteBytes(message.payload.Bytes())
-
-	message.signatureMutex.Lock()
-	message.signature = message.issuerLocalIdentity.Sign(marshalUtil.Bytes())
-	message.signatureMutex.Unlock()
-
+	marshalUtil.WriteUint64(message.nonce)
 	marshalUtil.WriteBytes(message.signature.Bytes())
 
 	message.bytes = marshalUtil.Bytes()
@@ -270,10 +259,10 @@ func (message *Message) UnmarshalObjectStorageValue(data []byte) (consumedBytes
 	marshalUtil := marshalutil.New(data)
 
 	// parse information
-	if message.trunkId, err = ParseId(marshalUtil); err != nil {
+	if message.trunkID, err = ParseId(marshalUtil); err != nil {
 		return
 	}
-	if message.branchId, err = ParseId(marshalUtil); err != nil {
+	if message.branchID, err = ParseId(marshalUtil); err != nil {
 		return
 	}
 	if message.issuerPublicKey, err = ed25519.ParsePublicKey(marshalUtil); err != nil {
@@ -288,6 +277,9 @@ func (message *Message) UnmarshalObjectStorageValue(data []byte) (consumedBytes
 	if message.payload, err = payload.Parse(marshalUtil); err != nil {
 		return
 	}
+	if message.nonce, err = marshalUtil.ReadUint64(); err != nil {
+		return
+	}
 	if message.signature, err = ed25519.ParseSignature(marshalUtil); err != nil {
 		return
 	}
@@ -311,19 +303,22 @@ func (message *Message) ObjectStorageValue() []byte {
 	return message.Bytes()
 }
 
-func (message *Message) Update(other objectstorage.StorableObject) {
+// Update updates the object with the values of another object.
+// Since a Message is immutable, this function is not implemented and panics.
+func (message *Message) Update(objectstorage.StorableObject) {
 	panic("messages should never be overwritten and only stored once to optimize IO")
 }
 
 func (message *Message) String() string {
 	return stringify.Struct("Message",
 		stringify.StructField("id", message.Id()),
-		stringify.StructField("trunkMessageId", message.TrunkId()),
-		stringify.StructField("branchMessageId", message.BranchId()),
+		stringify.StructField("trunkId", message.TrunkId()),
+		stringify.StructField("branchId", message.BranchId()),
 		stringify.StructField("issuer", message.IssuerPublicKey()),
 		stringify.StructField("issuingTime", message.IssuingTime()),
 		stringify.StructField("sequenceNumber", message.SequenceNumber()),
 		stringify.StructField("payload", message.Payload()),
+		stringify.StructField("nonce", message.Nonce()),
 		stringify.StructField("signature", message.Signature()),
 	)
 }
diff --git a/packages/binary/messagelayer/messagefactory/messagefactory.go b/packages/binary/messagelayer/messagefactory/messagefactory.go
index 7f06f292a8f7b46356287f34e1c6dfc99c5a142b..8401a9afb821d6e11d28c1c941ca68e5922a474a 100644
--- a/packages/binary/messagelayer/messagefactory/messagefactory.go
+++ b/packages/binary/messagelayer/messagefactory/messagefactory.go
@@ -2,27 +2,44 @@ package messagefactory
 
 import (
 	"fmt"
+	"sync"
 	"time"
 
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/payload"
-	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/tipselector"
+	"github.com/iotaledger/hive.go/crypto/ed25519"
 	"github.com/iotaledger/hive.go/identity"
 	"github.com/iotaledger/hive.go/kvstore"
 )
 
 const storeSequenceInterval = 100
 
+// A TipSelector selects two tips, branch and trunk, for a new message to attach to.
+type TipSelector interface {
+	Tips() (trunk message.Id, branch message.Id)
+}
+
+// A Worker performs the PoW for the provided message in serialized byte form.
+type Worker interface {
+	DoPOW([]byte) (nonce uint64, err error)
+}
+
+// ZeroWorker is a PoW worker that always returns 0 as the nonce.
+var ZeroWorker = WorkerFunc(func([]byte) (uint64, error) { return 0, nil })
+
 // MessageFactory acts as a factory to create new messages.
 type MessageFactory struct {
 	Events        *Events
 	sequence      *kvstore.Sequence
 	localIdentity *identity.LocalIdentity
-	tipSelector   *tipselector.TipSelector
+	selector      TipSelector
+
+	worker      Worker
+	workerMutex sync.RWMutex
 }
 
 // New creates a new message factory.
-func New(store kvstore.KVStore, localIdentity *identity.LocalIdentity, tipSelector *tipselector.TipSelector, sequenceKey []byte) *MessageFactory {
+func New(store kvstore.KVStore, sequenceKey []byte, localIdentity *identity.LocalIdentity, selector TipSelector) *MessageFactory {
 	sequence, err := kvstore.NewSequence(store, sequenceKey, storeSequenceInterval)
 	if err != nil {
 		panic(fmt.Sprintf("could not create message sequence number: %v", err))
@@ -32,10 +49,18 @@ func New(store kvstore.KVStore, localIdentity *identity.LocalIdentity, tipSelect
 		Events:        newEvents(),
 		sequence:      sequence,
 		localIdentity: localIdentity,
-		tipSelector:   tipSelector,
+		selector:      selector,
+		worker:        ZeroWorker,
 	}
 }
 
+// SetWorker sets the PoW worker to be used for the messages.
+func (m *MessageFactory) SetWorker(worker Worker) {
+	m.workerMutex.Lock()
+	defer m.workerMutex.Unlock()
+	m.worker = worker
+}
+
 // IssuePayload creates a new message including sequence number and tip selection and returns it.
 // It also triggers the MessageConstructed event once it's done, which is for example used by the plugins to listen for
 // messages that shall be attached to the tangle.
@@ -46,16 +71,30 @@ func (m *MessageFactory) IssuePayload(payload payload.Payload) *message.Message
 		return nil
 	}
 
-	trunkMessageId, branchMessageId := m.tipSelector.Tips()
+	trunkID, branchID := m.selector.Tips()
+	issuingTime := time.Now()
+	issuerPublicKey := m.localIdentity.PublicKey()
+
+	// do the PoW
+	nonce, err := m.doPOW(trunkID, branchID, issuingTime, issuerPublicKey, sequenceNumber, payload)
+	if err != nil {
+		m.Events.Error.Trigger(fmt.Errorf("pow failed: %w", err))
+		return nil
+	}
+
+	// create the signature
+	signature := m.sign(trunkID, branchID, issuingTime, issuerPublicKey, sequenceNumber, payload, nonce)
+
 	msg := message.New(
-		trunkMessageId,
-		branchMessageId,
-		m.localIdentity,
-		time.Now(),
+		trunkID,
+		branchID,
+		issuingTime,
+		issuerPublicKey,
 		sequenceNumber,
 		payload,
+		nonce,
+		signature,
 	)
-
 	m.Events.MessageConstructed.Trigger(msg)
 	return msg
 }
@@ -66,3 +105,37 @@ func (m *MessageFactory) Shutdown() {
 		m.Events.Error.Trigger(fmt.Errorf("could not release message sequence number: %w", err))
 	}
 }
+
+func (m *MessageFactory) doPOW(trunkID message.Id, branchID message.Id, issuingTime time.Time, key ed25519.PublicKey, seq uint64, payload payload.Payload) (uint64, error) {
+	// create a dummy message to simplify marshaling
+	dummy := message.New(trunkID, branchID, issuingTime, key, seq, payload, 0, ed25519.EmptySignature).Bytes()
+
+	m.workerMutex.RLock()
+	defer m.workerMutex.RUnlock()
+	return m.worker.DoPOW(dummy)
+}
+
+func (m *MessageFactory) sign(trunkID message.Id, branchID message.Id, issuingTime time.Time, key ed25519.PublicKey, seq uint64, payload payload.Payload, nonce uint64) ed25519.Signature {
+	// create a dummy message to simplify marshaling
+	dummy := message.New(trunkID, branchID, issuingTime, key, seq, payload, nonce, ed25519.EmptySignature)
+	dummyBytes := dummy.Bytes()
+
+	contentLength := len(dummyBytes) - len(dummy.Signature())
+	return m.localIdentity.Sign(dummyBytes[:contentLength])
+}
+
+// The TipSelectorFunc type is an adapter to allow the use of ordinary functions as tip selectors.
+type TipSelectorFunc func() (message.Id, message.Id)
+
+// Tips calls f().
+func (f TipSelectorFunc) Tips() (message.Id, message.Id) {
+	return f()
+}
+
+// The WorkerFunc type is an adapter to allow the use of ordinary functions as a PoW performer.
+type WorkerFunc func([]byte) (uint64, error)
+
+// DoPOW calls f(msg).
+func (f WorkerFunc) DoPOW(msg []byte) (uint64, error) {
+	return f(msg)
+}
diff --git a/packages/binary/messagelayer/messagefactory/messagefactory_test.go b/packages/binary/messagelayer/messagefactory/messagefactory_test.go
index f54aecb4c06bc3d3b8f520b4dc3d2098be2366cb..ad909fcf1d235f4b960f7bbd1f1708459d2e243d 100644
--- a/packages/binary/messagelayer/messagefactory/messagefactory_test.go
+++ b/packages/binary/messagelayer/messagefactory/messagefactory_test.go
@@ -1,7 +1,9 @@
 package messagefactory
 
 import (
-	"encoding"
+	"context"
+	"crypto"
+	"crypto/ed25519"
 	"sync"
 	"sync/atomic"
 	"testing"
@@ -9,21 +11,27 @@ import (
 
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/payload"
-	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/tipselector"
-	"github.com/iotaledger/hive.go/kvstore/mapdb"
-
+	"github.com/iotaledger/goshimmer/packages/pow"
 	"github.com/iotaledger/hive.go/events"
 	"github.com/iotaledger/hive.go/identity"
+	"github.com/iotaledger/hive.go/kvstore/mapdb"
 	"github.com/stretchr/testify/assert"
+	_ "golang.org/x/crypto/blake2b"
 )
 
 const (
 	sequenceKey   = "seq"
+	targetPOW     = 10
 	totalMessages = 2000
 )
 
 func TestMessageFactory_BuildMessage(t *testing.T) {
-	msgFactory := New(mapdb.NewMapDB(), identity.GenerateLocalIdentity(), tipselector.New(), []byte(sequenceKey))
+	msgFactory := New(
+		mapdb.NewMapDB(),
+		[]byte(sequenceKey),
+		identity.GenerateLocalIdentity(),
+		TipSelectorFunc(func() (message.Id, message.Id) { return message.EmptyId, message.EmptyId }),
+	)
 	defer msgFactory.Shutdown()
 
 	// keep track of sequence numbers
@@ -36,8 +44,7 @@ func TestMessageFactory_BuildMessage(t *testing.T) {
 	}))
 
 	t.Run("CheckProperties", func(t *testing.T) {
-		data := []byte("TestCheckProperties")
-		var p payload.Payload = NewMockPayload(data)
+		p := payload.NewData([]byte("TestCheckProperties"))
 		msg := msgFactory.IssuePayload(p)
 
 		assert.NotNil(t, msg.TrunkId())
@@ -47,8 +54,7 @@ func TestMessageFactory_BuildMessage(t *testing.T) {
 		assert.InDelta(t, time.Now().UnixNano(), msg.IssuingTime().UnixNano(), 100000000)
 
 		// check payload
-		assert.Same(t, p, msg.Payload())
-		assert.Equal(t, data, msg.Payload().Bytes())
+		assert.Equal(t, p, msg.Payload())
 
 		// check total events and sequence number
 		assert.EqualValues(t, 1, countEvents)
@@ -62,8 +68,8 @@ func TestMessageFactory_BuildMessage(t *testing.T) {
 		for i := 1; i < totalMessages; i++ {
 			t.Run("test", func(t *testing.T) {
 				t.Parallel()
-				data := []byte("TestCheckProperties")
-				var p payload.Payload = NewMockPayload(data)
+
+				p := payload.NewData([]byte("TestParallelCreation"))
 				msg := msgFactory.IssuePayload(p)
 
 				assert.NotNil(t, msg.TrunkId())
@@ -73,8 +79,7 @@ func TestMessageFactory_BuildMessage(t *testing.T) {
 				assert.InDelta(t, time.Now().UnixNano(), msg.IssuingTime().UnixNano(), 100000000)
 
 				// check payload
-				assert.Same(t, p, msg.Payload())
-				assert.Equal(t, data, msg.Payload().Bytes())
+				assert.Equal(t, p, msg.Payload())
 
 				sequenceNumbers.Store(msg.SequenceNumber(), true)
 			})
@@ -104,28 +109,27 @@ func TestMessageFactory_BuildMessage(t *testing.T) {
 	assert.EqualValues(t, totalMessages, countSequence)
 }
 
-type MockPayload struct {
-	data []byte
-	encoding.BinaryMarshaler
-	encoding.BinaryUnmarshaler
-}
-
-func NewMockPayload(data []byte) *MockPayload {
-	return &MockPayload{data: data}
-}
+func TestMessageFactory_POW(t *testing.T) {
+	msgFactory := New(
+		mapdb.NewMapDB(),
+		[]byte(sequenceKey),
+		identity.GenerateLocalIdentity(),
+		TipSelectorFunc(func() (message.Id, message.Id) { return message.EmptyId, message.EmptyId }),
+	)
+	defer msgFactory.Shutdown()
 
-func (m *MockPayload) Bytes() []byte {
-	return m.data
-}
+	worker := pow.New(crypto.BLAKE2b_512, 1)
 
-func (m *MockPayload) Type() payload.Type {
-	return payload.Type(0)
-}
+	msgFactory.SetWorker(WorkerFunc(func(msgBytes []byte) (uint64, error) {
+		content := msgBytes[:len(msgBytes)-ed25519.SignatureSize-8]
+		return worker.Mine(context.Background(), content, targetPOW)
+	}))
 
-func (m *MockPayload) String() string {
-	return string(m.data)
-}
+	msg := msgFactory.IssuePayload(payload.NewData([]byte("test")))
+	msgBytes := msg.Bytes()
+	content := msgBytes[:len(msgBytes)-ed25519.SignatureSize-8]
 
-func (m *MockPayload) Unmarshal(bytes []byte) error {
-	panic("implement me")
+	zeroes, err := worker.LeadingZerosWithNonce(content, msg.Nonce())
+	assert.GreaterOrEqual(t, zeroes, targetPOW)
+	assert.NoError(t, err)
 }
diff --git a/packages/binary/messagelayer/messageparser/builtinfilters/pow_filter.go b/packages/binary/messagelayer/messageparser/builtinfilters/pow_filter.go
new file mode 100644
index 0000000000000000000000000000000000000000..38f708cb4cbe22ffc51d8c0d63774025071f3617
--- /dev/null
+++ b/packages/binary/messagelayer/messageparser/builtinfilters/pow_filter.go
@@ -0,0 +1,108 @@
+package builtinfilters
+
+import (
+	"crypto/ed25519"
+	"errors"
+	"fmt"
+	"sync"
+
+	"github.com/iotaledger/goshimmer/packages/pow"
+	"github.com/iotaledger/hive.go/async"
+	"github.com/iotaledger/hive.go/autopeering/peer"
+)
+
+var (
+	// ErrInvalidPOWDifficultly is returned when the nonce of a message does not fulfill the PoW difficulty.
+	ErrInvalidPOWDifficultly = errors.New("invalid PoW")
+	// ErrMessageTooSmall is returned when the message does not contain enough data for the PoW.
+	ErrMessageTooSmall = errors.New("message too small")
+)
+
+// PowFilter is a message bytes filter validating the PoW nonce.
+type PowFilter struct {
+	worker     *pow.Worker
+	difficulty int
+	workerPool async.WorkerPool
+
+	mu             sync.Mutex
+	acceptCallback func([]byte, *peer.Peer)
+	rejectCallback func([]byte, error, *peer.Peer)
+}
+
+// NewPowFilter creates a new PoW bytes filter.
+func NewPowFilter(worker *pow.Worker, difficulty int) *PowFilter {
+	return &PowFilter{
+		worker:     worker,
+		difficulty: difficulty,
+	}
+}
+
+// Filter checks whether the given bytes pass the PoW validation and calls the corresponding callback.
+func (f *PowFilter) Filter(msgBytes []byte, p *peer.Peer) {
+	f.workerPool.Submit(func() {
+		if err := f.validate(msgBytes); err != nil {
+			f.reject(msgBytes, err, p)
+			return
+		}
+		f.accept(msgBytes, p)
+	})
+}
+
+// OnAccept registers the given callback as the acceptance function of the filter.
+func (f *PowFilter) OnAccept(callback func([]byte, *peer.Peer)) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+	f.acceptCallback = callback
+}
+
+// OnReject registers the given callback as the rejection function of the filter.
+func (f *PowFilter) OnReject(callback func([]byte, error, *peer.Peer)) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+	f.rejectCallback = callback
+}
+
+// Shutdown shuts down the filter.
+func (f *PowFilter) Shutdown() {
+	f.workerPool.ShutdownGracefully()
+}
+
+func (f *PowFilter) accept(msgBytes []byte, p *peer.Peer) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+	if f.acceptCallback != nil {
+		f.acceptCallback(msgBytes, p)
+	}
+}
+
+func (f *PowFilter) reject(msgBytes []byte, err error, p *peer.Peer) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+	if f.rejectCallback != nil {
+		f.rejectCallback(msgBytes, err, p)
+	}
+}
+
+func (f *PowFilter) validate(msgBytes []byte) error {
+	content, err := powData(msgBytes)
+	if err != nil {
+		return err
+	}
+	zeros, err := f.worker.LeadingZeros(content)
+	if err != nil {
+		return err
+	}
+	if zeros < f.difficulty {
+		return fmt.Errorf("%w: leading zeros %d for difficulty %d", ErrInvalidPOWDifficultly, zeros, f.difficulty)
+	}
+	return nil
+}
+
+// powData returns the bytes over which PoW should be computed.
+func powData(msgBytes []byte) ([]byte, error) {
+	contentLength := len(msgBytes) - ed25519.SignatureSize
+	if contentLength < pow.NonceBytes {
+		return nil, ErrMessageTooSmall
+	}
+	return msgBytes[:contentLength], nil
+}
diff --git a/packages/binary/messagelayer/messageparser/builtinfilters/pow_filter_test.go b/packages/binary/messagelayer/messageparser/builtinfilters/pow_filter_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..fe53a9d71cb715dec6a9ad5e26fc87365a680493
--- /dev/null
+++ b/packages/binary/messagelayer/messageparser/builtinfilters/pow_filter_test.go
@@ -0,0 +1,75 @@
+package builtinfilters
+
+import (
+	"context"
+	"crypto"
+	"errors"
+	"testing"
+	"time"
+
+	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
+	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/payload"
+	"github.com/iotaledger/goshimmer/packages/pow"
+	"github.com/iotaledger/hive.go/autopeering/peer"
+	"github.com/iotaledger/hive.go/crypto/ed25519"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+	_ "golang.org/x/crypto/blake2b" // required by crypto.BLAKE2b_512
+)
+
+var (
+	testPayload               = payload.NewData([]byte("test"))
+	testPeer       *peer.Peer = nil
+	testWorker                = pow.New(crypto.BLAKE2b_512, 1)
+	testDifficulty            = 10
+)
+
+func TestPowFilter_Filter(t *testing.T) {
+	filter := NewPowFilter(testWorker, testDifficulty)
+	defer filter.Shutdown()
+
+	// set callbacks
+	m := &callbackMock{}
+	filter.OnAccept(m.Accept)
+	filter.OnReject(m.Reject)
+
+	t.Run("reject small message", func(t *testing.T) {
+		m.On("Reject", mock.Anything, mock.MatchedBy(func(err error) bool { return errors.Is(err, ErrMessageTooSmall) }), testPeer)
+		filter.Filter(nil, testPeer)
+	})
+
+	msg := newTestMessage(0)
+	msgBytes := msg.Bytes()
+
+	t.Run("reject invalid nonce", func(t *testing.T) {
+		m.On("Reject", msgBytes, mock.MatchedBy(func(err error) bool { return errors.Is(err, ErrInvalidPOWDifficultly) }), testPeer)
+		filter.Filter(msgBytes, testPeer)
+	})
+
+	nonce, err := testWorker.Mine(context.Background(), msgBytes[:len(msgBytes)-len(msg.Signature())-pow.NonceBytes], testDifficulty)
+	require.NoError(t, err)
+
+	msgPOW := newTestMessage(nonce)
+	msgPOWBytes := msgPOW.Bytes()
+
+	t.Run("accept valid nonce", func(t *testing.T) {
+		zeroes, err := testWorker.LeadingZeros(msgPOWBytes[:len(msgPOWBytes)-len(msgPOW.Signature())])
+		require.NoError(t, err)
+		require.GreaterOrEqual(t, zeroes, testDifficulty)
+
+		m.On("Accept", msgPOWBytes, testPeer)
+		filter.Filter(msgPOWBytes, testPeer)
+	})
+
+	filter.Shutdown()
+	m.AssertExpectations(t)
+}
+
+type callbackMock struct{ mock.Mock }
+
+func (m *callbackMock) Accept(msg []byte, p *peer.Peer)            { m.Called(msg, p) }
+func (m *callbackMock) Reject(msg []byte, err error, p *peer.Peer) { m.Called(msg, err, p) }
+
+func newTestMessage(nonce uint64) *message.Message {
+	return message.New(message.EmptyId, message.EmptyId, time.Time{}, ed25519.PublicKey{}, 0, testPayload, nonce, ed25519.Signature{})
+}
diff --git a/packages/binary/messagelayer/messageparser/bytes_filter.go b/packages/binary/messagelayer/messageparser/bytes_filter.go
index 96d04f8f9222fcdff95bc9061aeb1e9530baf280..96d928e582ee330dfc732c002b4119c561e6f19b 100644
--- a/packages/binary/messagelayer/messageparser/bytes_filter.go
+++ b/packages/binary/messagelayer/messageparser/bytes_filter.go
@@ -11,7 +11,7 @@ type BytesFilter interface {
 	Filter(bytes []byte, peer *peer.Peer)
 	// OnAccept registers the given callback as the acceptance function of the filter.
 	OnAccept(callback func(bytes []byte, peer *peer.Peer))
-	// OnAccept registers the given callback as the rejection function of the filter.
+	// OnReject registers the given callback as the rejection function of the filter.
 	OnReject(callback func(bytes []byte, err error, peer *peer.Peer))
 	// Shutdown shuts down the filter.
 	Shutdown()
diff --git a/packages/binary/messagelayer/messageparser/message_parser_test.go b/packages/binary/messagelayer/messageparser/message_parser_test.go
index ae1b03c21b807088af20e00eb34ecd394a53d193..2e33aad3d231fb15852b182fbdbe7453891052b4 100644
--- a/packages/binary/messagelayer/messageparser/message_parser_test.go
+++ b/packages/binary/messagelayer/messageparser/message_parser_test.go
@@ -5,8 +5,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/iotaledger/hive.go/crypto/ed25519"
 	"github.com/iotaledger/hive.go/events"
-	"github.com/iotaledger/hive.go/identity"
 	"github.com/labstack/gommon/log"
 
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
@@ -14,8 +14,7 @@ import (
 )
 
 func BenchmarkMessageParser_ParseBytesSame(b *testing.B) {
-	localIdentity := identity.GenerateLocalIdentity()
-	msgBytes := message.New(message.EmptyId, message.EmptyId, localIdentity, time.Now(), 0, payload.NewData([]byte("Test"))).Bytes()
+	msgBytes := newTestMessage("Test").Bytes()
 	msgParser := New()
 
 	b.ResetTimer()
@@ -29,9 +28,8 @@ func BenchmarkMessageParser_ParseBytesSame(b *testing.B) {
 
 func BenchmarkMessageParser_ParseBytesDifferent(b *testing.B) {
 	messageBytes := make([][]byte, b.N)
-	localIdentity := identity.GenerateLocalIdentity()
 	for i := 0; i < b.N; i++ {
-		messageBytes[i] = message.New(message.EmptyId, message.EmptyId, localIdentity, time.Now(), 0, payload.NewData([]byte("Test"+strconv.Itoa(i)))).Bytes()
+		messageBytes[i] = newTestMessage("Test" + strconv.Itoa(i)).Bytes()
 	}
 
 	msgParser := New()
@@ -46,8 +44,7 @@ func BenchmarkMessageParser_ParseBytesDifferent(b *testing.B) {
 }
 
 func TestMessageParser_ParseMessage(t *testing.T) {
-	localIdentity := identity.GenerateLocalIdentity()
-	msg := message.New(message.EmptyId, message.EmptyId, localIdentity, time.Now(), 0, payload.NewData([]byte("Test")))
+	msg := newTestMessage("Test")
 
 	msgParser := New()
 	msgParser.Parse(msg.Bytes(), nil)
@@ -58,3 +55,7 @@ func TestMessageParser_ParseMessage(t *testing.T) {
 
 	msgParser.Shutdown()
 }
+
+func newTestMessage(payloadString string) *message.Message {
+	return message.New(message.EmptyId, message.EmptyId, time.Now(), ed25519.PublicKey{}, 0, payload.NewData([]byte(payloadString)), 0, ed25519.Signature{})
+}
diff --git a/packages/binary/messagelayer/tangle/tangle_test.go b/packages/binary/messagelayer/tangle/tangle_test.go
index 51bcb08378a3949873721bf352ea8792e8b59404..f0baac00765c9f5d564c879e98faf8ebcebfd848 100644
--- a/packages/binary/messagelayer/tangle/tangle_test.go
+++ b/packages/binary/messagelayer/tangle/tangle_test.go
@@ -7,8 +7,8 @@ import (
 
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/payload"
+	"github.com/iotaledger/hive.go/crypto/ed25519"
 	"github.com/iotaledger/hive.go/events"
-	"github.com/iotaledger/hive.go/identity"
 	"github.com/iotaledger/hive.go/kvstore/mapdb"
 )
 
@@ -20,11 +20,9 @@ func BenchmarkTangle_AttachMessage(b *testing.B) {
 		return
 	}
 
-	testIdentity := identity.GenerateLocalIdentity()
-
 	messageBytes := make([]*message.Message, b.N)
 	for i := 0; i < b.N; i++ {
-		messageBytes[i] = message.New(message.EmptyId, message.EmptyId, testIdentity, time.Now(), 0, payload.NewData([]byte("some data")))
+		messageBytes[i] = newTestMessage("some data")
 		messageBytes[i].Bytes()
 	}
 
@@ -73,10 +71,8 @@ func TestTangle_AttachMessage(t *testing.T) {
 		fmt.Println("REMOVED:", messageId)
 	}))
 
-	localIdentity1 := identity.GenerateLocalIdentity()
-	localIdentity2 := identity.GenerateLocalIdentity()
-	newMessageOne := message.New(message.EmptyId, message.EmptyId, localIdentity1, time.Now(), 0, payload.NewData([]byte("some data")))
-	newMessageTwo := message.New(newMessageOne.Id(), newMessageOne.Id(), localIdentity2, time.Now(), 0, payload.NewData([]byte("some other data")))
+	newMessageOne := newTestMessage("some data")
+	newMessageTwo := newTestMessage("some other data")
 
 	messageTangle.AttachMessage(newMessageTwo)
 
@@ -86,3 +82,7 @@ func TestTangle_AttachMessage(t *testing.T) {
 
 	messageTangle.Shutdown()
 }
+
+func newTestMessage(payloadString string) *message.Message {
+	return message.New(message.EmptyId, message.EmptyId, time.Now(), ed25519.PublicKey{}, 0, payload.NewData([]byte(payloadString)), 0, ed25519.Signature{})
+}
diff --git a/packages/binary/messagelayer/test/data_payload_test.go b/packages/binary/messagelayer/test/data_payload_test.go
index 5b571dbd2cfc0c79f49f1e4c55de7dd432ae33ef..30759328e04634f30579bc149eea4482a783f799 100644
--- a/packages/binary/messagelayer/test/data_payload_test.go
+++ b/packages/binary/messagelayer/test/data_payload_test.go
@@ -4,10 +4,12 @@ import (
 	"runtime"
 	"sync"
 	"testing"
-	"time"
 
+	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/messagefactory"
+	"github.com/iotaledger/goshimmer/plugins/messagelayer"
 	"github.com/iotaledger/hive.go/async"
 	"github.com/iotaledger/hive.go/identity"
+	"github.com/iotaledger/hive.go/kvstore/mapdb"
 
 	"github.com/panjf2000/ants/v2"
 
@@ -19,11 +21,11 @@ func BenchmarkVerifyDataMessages(b *testing.B) {
 	var pool async.WorkerPool
 	pool.Tune(runtime.NumCPU() * 2)
 
-	localIdentity := identity.GenerateLocalIdentity()
+	factory := messagefactory.New(mapdb.NewMapDB(), []byte(messagelayer.DBSequenceNumber), identity.GenerateLocalIdentity(), messagefactory.TipSelectorFunc(func() (message.Id, message.Id) { return message.EmptyId, message.EmptyId }))
 
 	messages := make([][]byte, b.N)
 	for i := 0; i < b.N; i++ {
-		messages[i] = message.New(message.EmptyId, message.EmptyId, localIdentity, time.Now(), 0, payload.NewData([]byte("some data"))).Bytes()
+		messages[i] = factory.IssuePayload(payload.NewData([]byte("some data"))).Bytes()
 	}
 
 	b.ResetTimer()
@@ -45,18 +47,16 @@ func BenchmarkVerifyDataMessages(b *testing.B) {
 func BenchmarkVerifySignature(b *testing.B) {
 	pool, _ := ants.NewPool(80, ants.WithNonblocking(false))
 
-	localIdentity := identity.GenerateLocalIdentity()
+	factory := messagefactory.New(mapdb.NewMapDB(), []byte(messagelayer.DBSequenceNumber), identity.GenerateLocalIdentity(), messagefactory.TipSelectorFunc(func() (message.Id, message.Id) { return message.EmptyId, message.EmptyId }))
 
 	messages := make([]*message.Message, b.N)
 	for i := 0; i < b.N; i++ {
-		messages[i] = message.New(message.EmptyId, message.EmptyId, localIdentity, time.Now(), 0, payload.NewData([]byte("test")))
+		messages[i] = factory.IssuePayload(payload.NewData([]byte("test")))
 		messages[i].Bytes()
 	}
-
-	var wg sync.WaitGroup
-
 	b.ResetTimer()
 
+	var wg sync.WaitGroup
 	for i := 0; i < b.N; i++ {
 		wg.Add(1)
 
diff --git a/packages/binary/messagelayer/test/message_test.go b/packages/binary/messagelayer/test/message_test.go
index fca5ec70b56df7d2b3457d959ddc08308c33febf..fa5ed95d3df8710f6ee136a67cf52ec5ba34479c 100644
--- a/packages/binary/messagelayer/test/message_test.go
+++ b/packages/binary/messagelayer/test/message_test.go
@@ -1,13 +1,13 @@
 package test
 
 import (
-	"fmt"
 	"testing"
 	"time"
 
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/messagefactory"
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/tipselector"
 	"github.com/iotaledger/goshimmer/plugins/messagelayer"
+	"github.com/iotaledger/hive.go/crypto/ed25519"
 	"github.com/iotaledger/hive.go/identity"
 	"github.com/iotaledger/hive.go/kvstore/mapdb"
 	"github.com/stretchr/testify/assert"
@@ -31,14 +31,28 @@ func TestMessage_StorableObjectFromKey(t *testing.T) {
 	assert.Equal(t, key, messageFromKey.(*message.Message).Id())
 }
 
+func TestMessage_VerifySignature(t *testing.T) {
+	keyPair := ed25519.GenerateKeyPair()
+	pl := payload.NewData([]byte("test"))
+
+	unsigned := message.New(message.EmptyId, message.EmptyId, time.Time{}, keyPair.PublicKey, 0, pl, 0, ed25519.Signature{})
+	assert.False(t, unsigned.VerifySignature())
+
+	unsignedBytes := unsigned.Bytes()
+	signature := keyPair.PrivateKey.Sign(unsignedBytes[:len(unsignedBytes)-ed25519.SignatureSize])
+
+	signed := message.New(message.EmptyId, message.EmptyId, time.Time{}, keyPair.PublicKey, 0, pl, 0, signature)
+	assert.True(t, signed.VerifySignature())
+}
+
 func TestMessage_MarshalUnmarshal(t *testing.T) {
-	msgFactory := messagefactory.New(mapdb.NewMapDB(), identity.GenerateLocalIdentity(), tipselector.New(), []byte(messagelayer.DBSequenceNumber))
+	msgFactory := messagefactory.New(mapdb.NewMapDB(), []byte(messagelayer.DBSequenceNumber), identity.GenerateLocalIdentity(), tipselector.New())
 	defer msgFactory.Shutdown()
 
-	testMessage := msgFactory.IssuePayload(payload.NewData([]byte("sth")))
+	testMessage := msgFactory.IssuePayload(payload.NewData([]byte("test")))
 	assert.Equal(t, true, testMessage.VerifySignature())
 
-	fmt.Print(testMessage)
+	t.Log(testMessage)
 
 	restoredMessage, err, _ := message.FromBytes(testMessage.Bytes())
 	if assert.NoError(t, err, err) {
@@ -48,6 +62,7 @@ func TestMessage_MarshalUnmarshal(t *testing.T) {
 		assert.Equal(t, testMessage.IssuerPublicKey(), restoredMessage.IssuerPublicKey())
 		assert.Equal(t, testMessage.IssuingTime().Round(time.Second), restoredMessage.IssuingTime().Round(time.Second))
 		assert.Equal(t, testMessage.SequenceNumber(), restoredMessage.SequenceNumber())
+		assert.Equal(t, testMessage.Nonce(), restoredMessage.Nonce())
 		assert.Equal(t, testMessage.Signature(), restoredMessage.Signature())
 		assert.Equal(t, true, restoredMessage.VerifySignature())
 	}
diff --git a/packages/binary/messagelayer/tipselector/tipselector_test.go b/packages/binary/messagelayer/tipselector/tipselector_test.go
index 310612207ede51c1bf5173cd5fd3bc66da10d438..fbcf727d4b61b2f56312b052e2f73a468d3e5c18 100644
--- a/packages/binary/messagelayer/tipselector/tipselector_test.go
+++ b/packages/binary/messagelayer/tipselector/tipselector_test.go
@@ -4,7 +4,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/iotaledger/hive.go/identity"
+	"github.com/iotaledger/hive.go/crypto/ed25519"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
@@ -21,8 +21,7 @@ func Test(t *testing.T) {
 	assert.Equal(t, message.EmptyId, branch1)
 
 	// create a message and attach it
-	localIdentity1 := identity.GenerateLocalIdentity()
-	message1 := message.New(trunk1, branch1, localIdentity1, time.Now(), 0, payload.NewData([]byte("testmessage")))
+	message1 := newTestMessage(trunk1, branch1, "testmessage")
 	tipSelector.AddTip(message1)
 
 	// check if the tip shows up in the tip count
@@ -34,17 +33,15 @@ func Test(t *testing.T) {
 	assert.Equal(t, message1.Id(), branch2)
 
 	// create a 2nd message and attach it
-	localIdentity2 := identity.GenerateLocalIdentity()
-	message2 := message.New(message.EmptyId, message.EmptyId, localIdentity2, time.Now(), 0, payload.NewData([]byte("testmessage")))
+	message2 := newTestMessage(message.EmptyId, message.EmptyId, "testmessage")
 	tipSelector.AddTip(message2)
 
 	// check if the tip shows up in the tip count
 	assert.Equal(t, 2, tipSelector.TipCount())
 
 	// attach a message to our two tips
-	localIdentity3 := identity.GenerateLocalIdentity()
 	trunk3, branch3 := tipSelector.Tips()
-	message3 := message.New(trunk3, branch3, localIdentity3, time.Now(), 0, payload.NewData([]byte("testmessage")))
+	message3 := newTestMessage(trunk3, branch3, "testmessage")
 	tipSelector.AddTip(message3)
 
 	// check if the tip shows replaces the current tips
@@ -53,3 +50,7 @@ func Test(t *testing.T) {
 	assert.Equal(t, message3.Id(), trunk4)
 	assert.Equal(t, message3.Id(), branch4)
 }
+
+func newTestMessage(trunk, branch message.Id, payloadString string) *message.Message {
+	return message.New(trunk, branch, time.Now(), ed25519.PublicKey{}, 0, payload.NewData([]byte(payloadString)), 0, ed25519.Signature{})
+}
diff --git a/packages/pow/pow.go b/packages/pow/pow.go
index 035086ecef88b65a712687d648fafae1c046a54d..3d60d13c55ebc0e3a1e945e11f40f2a1c4717643 100644
--- a/packages/pow/pow.go
+++ b/packages/pow/pow.go
@@ -17,6 +17,9 @@ var (
 	ErrDone      = errors.New("done")
 )
 
+// NonceBytes specifies the number of bytes required for the nonce.
+const NonceBytes = 8
+
 // Hash identifies a cryptographic hash function that is implemented in another package.
 type Hash interface {
 	// Size returns the length, in bytes, of a digest resulting from the given hash function.
@@ -38,7 +41,7 @@ func New(hash Hash, numWorkers ...int) *Worker {
 		hash:       hash,
 		numWorkers: 1,
 	}
-	if len(numWorkers) > 0 {
+	if len(numWorkers) > 0 && numWorkers[0] > 0 {
 		w.numWorkers = numWorkers[0]
 	}
 	return w
@@ -106,15 +109,15 @@ func (w *Worker) LeadingZeros(data []byte) (int, error) {
 // LeadingZerosWithNonce returns the number of leading zeros in the digest
 // after the provided 8-byte nonce is appended to msg.
 func (w *Worker) LeadingZerosWithNonce(msg []byte, nonce uint64) (int, error) {
-	buf := make([]byte, len(msg)+8)
+	buf := make([]byte, len(msg)+NonceBytes)
 	copy(buf, msg)
-	binary.BigEndian.PutUint64(buf[len(msg):], nonce)
+	putUint64(buf[len(msg):], nonce)
 
 	return w.LeadingZeros(buf)
 }
 
 func (w *Worker) worker(msg []byte, startNonce uint64, target int, done *uint32, counter *uint64) (uint64, error) {
-	buf := make([]byte, len(msg)+8)
+	buf := make([]byte, len(msg)+NonceBytes)
 	copy(buf, msg)
 	asAnInt := new(big.Int)
 
@@ -125,7 +128,7 @@ func (w *Worker) worker(msg []byte, startNonce uint64, target int, done *uint32,
 		atomic.AddUint64(counter, 1)
 
 		// write nonce in the buffer
-		binary.BigEndian.PutUint64(buf[len(msg):], nonce)
+		putUint64(buf[len(msg):], nonce)
 
 		digest, err := w.sum(buf)
 		if err != nil {
@@ -149,3 +152,7 @@ func (w *Worker) sum(data []byte) ([]byte, error) {
 	}
 	return h.Sum(nil), nil
 }
+
+func putUint64(b []byte, v uint64) {
+	binary.LittleEndian.PutUint64(b, v)
+}
diff --git a/packages/pow/pow_test.go b/packages/pow/pow_test.go
index 33e360161da68b9d68f214bfb1131e4be3937601..8c000aee844002c42f0338f08780e2cce6488c8e 100644
--- a/packages/pow/pow_test.go
+++ b/packages/pow/pow_test.go
@@ -10,7 +10,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	_ "golang.org/x/crypto/sha3" // required by crypto.SHA3_256
+	_ "golang.org/x/crypto/blake2b" // required by crypto.BLAKE2b_512
 )
 
 const (
@@ -18,7 +18,7 @@ const (
 	target  = 10
 )
 
-var testWorker = New(crypto.SHA3_256, workers)
+var testWorker = New(crypto.BLAKE2b_512, workers)
 
 func TestWorker_Work(t *testing.T) {
 	nonce, err := testWorker.Mine(context.Background(), nil, target)
@@ -36,11 +36,11 @@ func TestWorker_Validate(t *testing.T) {
 		expErr          error
 	}{
 		{msg: nil, nonce: 0, expLeadingZeros: 1, expErr: nil},
-		{msg: nil, nonce: 13176245766944605079, expLeadingZeros: 29, expErr: nil},
-		{msg: make([]byte, 1024), nonce: 0, expLeadingZeros: 4, expErr: nil},
+		{msg: nil, nonce: 4611686018451317632, expLeadingZeros: 28, expErr: nil},
+		{msg: make([]byte, 10240), nonce: 0, expLeadingZeros: 1, expErr: nil},
 	}
 
-	w := &Worker{hash: crypto.SHA3_256}
+	w := &Worker{hash: crypto.BLAKE2b_512}
 	for _, tt := range tests {
 		zeros, err := w.LeadingZerosWithNonce(tt.msg, tt.nonce)
 		assert.Equal(t, tt.expLeadingZeros, zeros)
diff --git a/pluginmgr/core/plugins.go b/pluginmgr/core/plugins.go
index 1eba3f117348d559b68e2f677c8789f23f58a082..7bdae01c301a5d6f8945a630cd98dd18c57c0fbf 100644
--- a/pluginmgr/core/plugins.go
+++ b/pluginmgr/core/plugins.go
@@ -16,6 +16,7 @@ import (
 	"github.com/iotaledger/goshimmer/plugins/messagelayer"
 	"github.com/iotaledger/goshimmer/plugins/metrics"
 	"github.com/iotaledger/goshimmer/plugins/portcheck"
+	"github.com/iotaledger/goshimmer/plugins/pow"
 	"github.com/iotaledger/goshimmer/plugins/profiling"
 	"github.com/iotaledger/goshimmer/plugins/sync"
 
@@ -31,6 +32,7 @@ var PLUGINS = node.Plugins(
 	profiling.Plugin,
 	database.Plugin,
 	autopeering.Plugin,
+	pow.Plugin,
 	messagelayer.Plugin,
 	gossip.Plugin,
 	issuer.Plugin,
diff --git a/plugins/messagelayer/plugin.go b/plugins/messagelayer/plugin.go
index 8fa819b1510e98701a0755ca7fe7387f0f3ec899..ca1fb0e535950b01ec4c74816aae0c30141682bb 100644
--- a/plugins/messagelayer/plugin.go
+++ b/plugins/messagelayer/plugin.go
@@ -44,7 +44,7 @@ func configure(*node.Plugin) {
 	Tangle = tangle.New(store)
 
 	// Setup MessageFactory (behavior + logging))
-	MessageFactory = messagefactory.New(database.Store(), local.GetInstance().LocalIdentity(), TipSelector, []byte(DBSequenceNumber))
+	MessageFactory = messagefactory.New(database.Store(), []byte(DBSequenceNumber), local.GetInstance().LocalIdentity(), TipSelector)
 	MessageFactory.Events.MessageConstructed.Attach(events.NewClosure(Tangle.AttachMessage))
 	MessageFactory.Events.Error.Attach(events.NewClosure(func(err error) {
 		log.Errorf("internal error in message factory: %v", err)
diff --git a/plugins/pow/parameters.go b/plugins/pow/parameters.go
new file mode 100644
index 0000000000000000000000000000000000000000..422260b1e9bc7521319f0103e9970d6161103f08
--- /dev/null
+++ b/plugins/pow/parameters.go
@@ -0,0 +1,22 @@
+package pow
+
+import (
+	"time"
+
+	flag "github.com/spf13/pflag"
+)
+
+const (
+	// CfgPOWDifficulty defines the config flag of the PoW difficulty.
+	CfgPOWDifficulty = "pow.difficulty"
+	// CfgPOWNumThreads defines the config flag of the number of threads used to do the PoW.
+	CfgPOWNumThreads = "pow.numThreads"
+	// CfgPOWTimeout defines the config flag for the PoW timeout.
+	CfgPOWTimeout = "pow.timeout"
+)
+
+func init() {
+	flag.Int(CfgPOWDifficulty, 22, "PoW difficulty")
+	flag.Int(CfgPOWNumThreads, 1, "number of threads used to do the PoW")
+	flag.Duration(CfgPOWTimeout, time.Minute, "PoW timeout")
+}
diff --git a/plugins/pow/plugin.go b/plugins/pow/plugin.go
new file mode 100644
index 0000000000000000000000000000000000000000..1660eecb7218f1fd39b9a0dbdd5e8423f7f2b058
--- /dev/null
+++ b/plugins/pow/plugin.go
@@ -0,0 +1,33 @@
+package pow
+
+import (
+	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/messagefactory"
+	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/messageparser/builtinfilters"
+	"github.com/iotaledger/goshimmer/plugins/messagelayer"
+	"github.com/iotaledger/hive.go/logger"
+	"github.com/iotaledger/hive.go/node"
+)
+
+// PluginName is the name of the PoW plugin.
+const PluginName = "PoW"
+
+var (
+	// Plugin is the plugin instance of the PoW plugin.
+	Plugin = node.NewPlugin(PluginName, node.Enabled, run)
+)
+
+func run(*node.Plugin) {
+	// assure that the logger is available
+	log := logger.NewLogger(PluginName)
+
+	if node.IsSkipped(messagelayer.Plugin) {
+		log.Infof("%s is disabled; skipping %s\n", messagelayer.PluginName, PluginName)
+		return
+	}
+
+	// assure that the PoW worker is initialized
+	worker := Worker()
+
+	messagelayer.MessageParser.AddBytesFilter(builtinfilters.NewPowFilter(worker, difficulty))
+	messagelayer.MessageFactory.SetWorker(messagefactory.WorkerFunc(DoPOW))
+}
diff --git a/plugins/pow/pow.go b/plugins/pow/pow.go
new file mode 100644
index 0000000000000000000000000000000000000000..c547d4ae6f78f342b226b42ec794fdca543a7f3c
--- /dev/null
+++ b/plugins/pow/pow.go
@@ -0,0 +1,81 @@
+package pow
+
+import (
+	"context"
+	"crypto"
+	"crypto/ed25519"
+	"errors"
+	"sync"
+	"time"
+
+	"github.com/iotaledger/goshimmer/packages/pow"
+	"github.com/iotaledger/goshimmer/plugins/config"
+	"github.com/iotaledger/hive.go/logger"
+	_ "golang.org/x/crypto/blake2b" // required by crypto.BLAKE2b_512
+)
+
+var (
+	// ErrMessageTooSmall is returned when the message is smaller than the 8-byte nonce.
+	ErrMessageTooSmall = errors.New("message too small")
+)
+
+// parameters
+var (
+	hash = crypto.BLAKE2b_512
+
+	// configured via parameters
+	difficulty int
+	numWorkers int
+	timeout    time.Duration
+)
+
+var (
+	log *logger.Logger
+
+	workerOnce sync.Once
+	worker     *pow.Worker
+)
+
+// Worker returns the PoW worker instance of the PoW plugin.
+func Worker() *pow.Worker {
+	workerOnce.Do(func() {
+		log = logger.NewLogger(PluginName)
+		// load the parameters
+		difficulty = config.Node.GetInt(CfgPOWDifficulty)
+		numWorkers = config.Node.GetInt(CfgPOWNumThreads)
+		timeout = config.Node.GetDuration(CfgPOWTimeout)
+		// create the worker
+		worker = pow.New(hash, numWorkers)
+	})
+	return worker
+}
+
+// DoPOW performs the PoW on the provided msg and returns the nonce.
+func DoPOW(msg []byte) (uint64, error) {
+	content, err := powData(msg)
+	if err != nil {
+		return 0, err
+	}
+
+	// get the PoW worker
+	worker := Worker()
+
+	log.Debugw("start PoW", "difficulty", difficulty, "numWorkers", numWorkers)
+
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	nonce, err := worker.Mine(ctx, content[:len(content)-pow.NonceBytes], difficulty)
+
+	log.Debugw("PoW stopped", "nonce", nonce, "err", err)
+
+	return nonce, err
+}
+
+// powData returns the bytes over which PoW should be computed.
+func powData(msgBytes []byte) ([]byte, error) {
+	contentLength := len(msgBytes) - ed25519.SignatureSize
+	if contentLength < pow.NonceBytes {
+		return nil, ErrMessageTooSmall
+	}
+	return msgBytes[:contentLength], nil
+}
diff --git a/tools/docker-network/config.docker.json b/tools/docker-network/config.docker.json
index 423498cfd22123869f7fcaa728e3c2c42796c56b..34e3346867014d28ad25816dff5f5c1a5dd69705 100644
--- a/tools/docker-network/config.docker.json
+++ b/tools/docker-network/config.docker.json
@@ -59,6 +59,11 @@
     "disablePlugins": "portcheck",
     "enablePlugins": []
   },
+  "pow": {
+    "difficulty": 4,
+    "numThreads": 1,
+    "timeout": "10s"
+  },
   "webapi": {
     "auth": {
       "password": "goshimmer",
diff --git a/tools/docker-network/docker-compose.yml b/tools/docker-network/docker-compose.yml
index 84d345b6e9cb1ca6e4ec48439097f52fd26cd3ec..c87a61f2d6593753025134b6e81ea6a92a2da869 100644
--- a/tools/docker-network/docker-compose.yml
+++ b/tools/docker-network/docker-compose.yml
@@ -29,7 +29,7 @@ services:
       --metrics.global=true
       --prometheus.bindAddress=0.0.0.0:9312
       --node.enablePlugins=analysis-server,analysis-dashboard,prometheus
-      --node.disablePlugins=portcheck,dashboard,analysis-client,gossip,drng,issuer,sync,messagelayer,valuetransfers,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint
+      --node.disablePlugins=portcheck,dashboard,analysis-client,gossip,drng,issuer,sync,messagelayer,pow,valuetransfers,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint
     volumes:
       - ./config.docker.json:/tmp/config.json:ro
       - goshimmer-cache:/go
diff --git a/tools/integration-tests/tester/framework/parameters.go b/tools/integration-tests/tester/framework/parameters.go
index 6c8e63c67b0018cbf84ff42c39b3dbaee32b39c8..5a17154afcab266dda478a8142aed2da88098500 100644
--- a/tools/integration-tests/tester/framework/parameters.go
+++ b/tools/integration-tests/tester/framework/parameters.go
@@ -13,7 +13,7 @@ const (
 
 	logsDir = "/tmp/logs/"
 
-	disabledPluginsEntryNode = "portcheck,dashboard,analysis-client,profiling,gossip,drng,issuer,sync,metrics,valuetransfers,messagelayer,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint"
+	disabledPluginsEntryNode = "portcheck,dashboard,analysis-client,profiling,gossip,drng,issuer,sync,metrics,valuetransfers,messagelayer,pow,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint"
 	disabledPluginsPeer      = "portcheck,dashboard,analysis-client,profiling"
 	snapshotFilePath         = "/assets/7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih.bin"
 	dockerLogsPrefixLen      = 8