gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[gnunet-go] branch master updated: Reworked gnunet/transport and gnunet/


From: gnunet
Subject: [gnunet-go] branch master updated: Reworked gnunet/transport and gnunet/service.
Date: Wed, 01 Jun 2022 21:03:22 +0200

This is an automated email from the git hooks/post-receive script.

bernd-fix pushed a commit to branch master
in repository gnunet-go.

The following commit(s) were added to refs/heads/master by this push:
     new 913c80f  Reworked gnunet/transport and gnunet/service.
913c80f is described below

commit 913c80f317270b41f339048afa5d63278eecf8f4
Author: Bernd Fix <brf@hoi-polloi.org>
AuthorDate: Wed Jun 1 20:59:52 2022 +0200

    Reworked gnunet/transport and gnunet/service.
---
 README.md                                          | 125 ++++---
 src/cmd/peer_mockup/main.go                        |  68 ----
 src/cmd/peer_mockup/peers.go                       |  46 ---
 src/cmd/peer_mockup/process.go                     | 128 -------
 src/cmd/revoke-zonekey/main.go                     | 202 -----------
 src/gnunet/build.sh                                |   3 +
 src/{ => gnunet}/cmd/.gitignore                    |   0
 .../cmd/gnunet-service-dht-test-go}/main.go        |  74 ++--
 src/{ => gnunet}/cmd/gnunet-service-gns-go/main.go |  31 +-
 .../cmd/gnunet-service-revocation-go/main.go       |  29 +-
 src/gnunet/cmd/peer_mockup/main.go                 | 178 ++++++++++
 src/gnunet/cmd/revoke-zonekey/main.go              | 336 ++++++++++++++++++
 src/{ => gnunet}/cmd/vanityid/main.go              |   0
 src/gnunet/config/config.go                        |  68 +++-
 src/gnunet/config/gnunet-config.json               |  48 ++-
 src/gnunet/core/core.go                            | 234 +++++++++++++
 src/gnunet/core/core_test.go                       | 150 ++++++++
 src/gnunet/core/event.go                           | 109 ++++++
 src/gnunet/core/peer.go                            | 108 +++++-
 src/gnunet/core/peer_test.go                       |  72 ++++
 src/gnunet/crypto/gns.go                           |   9 +-
 src/gnunet/crypto/hash.go                          |  16 +-
 src/gnunet/go.mod                                  |  26 +-
 src/gnunet/go.sum                                  | 128 ++-----
 src/gnunet/message/factory.go                      |   4 +
 src/gnunet/message/message.go                      |   8 +
 src/gnunet/message/msg_dht.go                      |  95 +++++-
 src/gnunet/message/msg_gns.go                      |  94 -----
 src/gnunet/message/msg_hello.go                    | 103 ++++++
 src/gnunet/message/msg_namecache.go                |  12 +-
 src/gnunet/message/msg_transport.go                |  82 +----
 src/gnunet/modules.go                              |  28 +-
 src/gnunet/service/client.go                       |  30 +-
 src/gnunet/service/connection.go                   | 280 +++++++++++++++
 src/gnunet/service/context.go                      |  87 -----
 src/gnunet/service/dht/blocks/generic.go           | 196 +++++++++++
 src/gnunet/service/dht/blocks/generic_test.go      |  67 ++++
 src/gnunet/service/dht/blocks/gns.go               | 172 ++++++++++
 src/gnunet/service/dht/blocks/hello.go             | 226 ++++++++++++
 .../fs.go => service/dht/blocks/hello_test.go}     |  37 +-
 .../session.go => service/dht/blocks/types.go}     |  15 +-
 src/gnunet/service/dht/bloomfilter.go              | 123 +++++++
 src/gnunet/service/dht/dhtstore_test.go            |  89 +++++
 src/gnunet/service/dht/module.go                   |  99 +++++-
 src/gnunet/service/dht/routingtable.go             | 305 +++++++++++++++++
 src/gnunet/service/dht/routingtable_test.go        | 140 ++++++++
 src/gnunet/service/dht/service.go                  | 134 ++++++++
 src/gnunet/service/gns/block_handler.go            |  18 +-
 src/gnunet/service/gns/dns.go                      |   4 +-
 src/gnunet/service/gns/module.go                   | 135 +++++---
 src/gnunet/service/gns/service.go                  | 235 ++++++-------
 src/gnunet/service/module.go                       |  70 ++++
 src/gnunet/service/namecache/module.go             |  31 +-
 src/gnunet/service/revocation/module.go            | 100 ++++--
 src/gnunet/service/revocation/pow.go               |  46 ++-
 src/gnunet/service/revocation/pow_test.go          |   8 +-
 src/gnunet/service/revocation/service.go           | 192 +++++------
 src/gnunet/service/service.go                      | 200 +++++------
 src/gnunet/service/store.go                        | 379 +++++++++++++++++++++
 src/gnunet/test.sh                                 |   3 +
 src/gnunet/transport/channel.go                    | 213 ------------
 src/gnunet/transport/channel_netw.go               | 285 ----------------
 src/gnunet/transport/channel_test.go               | 232 -------------
 src/gnunet/transport/connection.go                 | 108 ++++--
 src/gnunet/transport/endpoint.go                   | 282 +++++++++++++++
 src/gnunet/transport/reader_writer.go              | 157 +++++++++
 src/gnunet/transport/transport.go                  | 151 ++++++++
 src/gnunet/util/address.go                         | 141 +++++++-
 src/gnunet/util/array.go                           |  65 ++--
 src/gnunet/util/database.go                        | 159 +++++++--
 src/gnunet/util/fs.go                              |   2 +-
 src/gnunet/util/key_value_store.go                 | 188 ----------
 src/gnunet/util/misc.go                            |  75 +++-
 src/gnunet/util/peer_id.go                         |  11 +-
 src/gnunet/util/time.go                            |  17 +
 75 files changed, 5641 insertions(+), 2480 deletions(-)

diff --git a/README.md b/README.md
index e721a94..1a1545f 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,103 @@
-# GNUnet in Go
+# gnunet-go: GNUnet implementation in Go
 
-This repository has two parts:
+Copyright (C) 2019-2022 Bernd Fix  >Y<
 
-* `src/` contains a Go implementation of GNUnet: It is WIP and only provides a
-very limited coverage of GNUnet. The goal is to have a complete, functionally
-equivalent implementation of the GNUnet protocol in Go.
+gnunet-go is free software: you can redistribute it and/or modify it
+under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, either version 3 of the License,
+or (at your option) any later version.
 
-* `doc/` contains documents for an implementation-agnostic specification of the
-GNUnet P2P protocols. It focuses on the peer messages, but also provides
-information on the internal messages.
+gnunet-go is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Affero General Public License for more details.
 
-## Author(s)
- * Bernd Fix <brf@hoi-polloi.org>
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-All files are licensed under GNU AGPL-3.0. Copyright by the authors.
+SPDX-License-Identifier: AGPL3.0-or-later
 
 ## Caveat
 
 THIS IS WORK-IN-PROGRESS AT A VERY EARLY STATE. DON'T EXPECT ANY COMPLETE
 DOCUMENTATION OR COMPILABLE, RUNNABLE OR EVEN OPERATIONAL SOURCE CODE.
 
+## TL;DR
+
+Go v1.18+ is required to compile the code.
+
+```bash
+git clone https://github.com/bfix/gnunet-go
+cd gnunet-go/src/gnunet
+go mod tidy
+go install ./...
+go test ./...
+```
+
+The binaries are stored in `${GOPATH}/bin`.
+
 ## Source code
 
-All source code is written in Golang (version 1.13+).
+All source code is written for Go v1.18+.
 
-### Dependencies
+3rd party libraries are managed by the Go module framework. After downloading
+the source code, make sure you run `go mod tidy` in the `src/gnunet` folder
+to install all dependencies.
 
-3rd party libraries are used to provide helper functionality (logging, MPI,
-Ed25519 support and other crypto-related packages). Make sure the dependent
-packages are accessible through `GOPATH`. To install the dependencies:
+### `./src/gnunet`
 
-```bash
-$ go get -u golang.org/x/crypto/...
-$ go get -u golang.org/x/text/...
-$ go get -u github.com/miekg/dns/...
-$ go get -u github.com/bfix/gospel/...
-```
+The folder `src/gnunet` contains a Go implementation of GNUnet: It is WIP
+and only provides a very limited coverage of GNUnet. The goal is to have
+a complete, functionally equivalent implementation of the GNUnet protocol
+in Go. Currently only some aspects of Transport, GNS, Revocation, Namecache
+and DHT are implemented.
+
+Use `./build.sh` to build the executables (services and utilities, see
+below). The resulting programs are stored in `${GOPATH}/bin`.
+
+To run the unit tests, use `./test.sh`. 
+
+### `./src/gnunet/cmd`
+
+#### `gnunet-service-dht-test-go`: Implementation of the DHT core service 
(testbed).
+
+#### `gnunet-service-gns-go`: Implementation of the GNS core service.
 
-### ./src/cmd folder
+Stand-alone GNS service that could be used with other GNUnet utilities and
+services.
 
+#### `gnunet-service-revocation-go`: Implementation of the GNS revocation 
service.
 
-#### `gnunet-service-gns-go`: Implementation of the GNS service.
+Stand-alone Revocation service that could be used with other GNUnet utilities
+and services.
+
+#### `revoke-zonekey`: Implementation of a stand-alone program to calculate 
revocations.
+
+This program creates a zone key revocation block. Depending on the parameters
+the calculation can take days or even weeks. The program can be interrupted
+at any time using `^C`; restarting the program with the exact same parameters
+continues the calculation.
+
+The following command-line options are available:
+
+* **`-b`**: Number of leading zero bits (difficulty, default: 24). The minimum
+difficulty `D` is fixed at 23. The expiration of a revocation is derived using
+`(b-D+1)*(1.1*EPOCH)`, where `EPOCH` is 365 days and it is extended by 10% in
+order to deal with unsynchronized clocks.
+
+The default difficulty will create a revocation valid for ~2 years.
+
+* **`-z`**: Zone key to be revoked (zone ID)
+
+* **`-f`**: Name of file to store revocation data
+
+* **`-t`**: testing mode: allow small difficulties for test runs.
+
+* **`-v`**: verbose output
 
 #### `peer_mockup`: test message exchange on the lowest level (transport).
 
-#### `vanityid`: Compute GNUnet vanity peer and ego id for a given regexp 
pattern.
+#### `vanityid`: Compute GNUnet vanity peer id for a given regexp pattern.
 
 N.B.: Key generation is slow at the moment, so be patient! To generate a single
 matching key some 1,000,000 keys need to be generated for a four letter prefix;
@@ -65,21 +118,11 @@ found; `time` is the time needed to find a match.
 To generate the key files, make sure GNUnet **is not running** and do: 
 
 ```bash
-$ # use a vanity peer id:
-$ echo "<hex.seed>" | xxd -r -p > 
/var/lib/gnunet/.local/share/gnunet/private_key.ecc
-$ sudo chown gnunet:gnunet /var/lib/gnunet/.local/share/gnunet/private_key.ecc
-$ sudo chmod 600 /var/lib/gnunet/.local/share/gnunet/private_key.ecc
-$ # use a vanity ego id:
-$ echo "<hex.scalar>" | xxd -r -p > 
~/.local/share/gnunet/identity/egos/<vanity_ego>
-$ chmod 600 ~/.local/share/gnunet/identity/egos/<vanity_ego>
+echo "<hex.seed>" | xxd -r -p > 
/var/lib/gnunet/.local/share/gnunet/private_key.ecc
+chown gnunet:gnunet /var/lib/gnunet/.local/share/gnunet/private_key.ecc
+chmod 600 /var/lib/gnunet/.local/share/gnunet/private_key.ecc
 ```
-### ./src/gnunet folder
-
-Packages used to implement GNUnet protocols (currently only some of TRANSPORT
-and GNS).
-
-## Documentation
-
-* raw: raw ASCII protocol definition
-* specification: texinfo protocol definition
 
+For gnunet-go configuration files you need to paste the result of
+`echo "<hex.seed>" | xxd -r -p | base64` into the `PrivateSeed` field in the
+`NodeConfig` section.
diff --git a/src/cmd/peer_mockup/main.go b/src/cmd/peer_mockup/main.go
deleted file mode 100644
index 59bc002..0000000
--- a/src/cmd/peer_mockup/main.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package main
-
-import (
-       "encoding/hex"
-       "flag"
-       "fmt"
-
-       "github.com/bfix/gospel/logger"
-       "gnunet/core"
-       "gnunet/transport"
-)
-
-var (
-       p *core.Peer // local peer (with private key)
-       t *core.Peer // remote peer
-)
-
-func main() {
-       // handle command line arguments
-       var (
-               asServer bool
-               err      error
-               ch       transport.Channel
-       )
-       flag.BoolVar(&asServer, "s", false, "accept incoming connections")
-       flag.Parse()
-
-       // setup peer instances from static data
-       if err = setupPeers(false); err != nil {
-               fmt.Println(err.Error())
-               return
-       }
-
-       
fmt.Println("======================================================================")
-       fmt.Println("GNUnet peer mock-up (EXPERIMENTAL)     (c) 2018,2019 by 
Bernd Fix, >Y<")
-       fmt.Printf("    Identity '%s'\n", p.GetIDString())
-       fmt.Printf("    [%s]\n", hex.EncodeToString(p.GetID()))
-       
fmt.Println("======================================================================")
-
-       if asServer {
-               // run as server
-               fmt.Println("Waiting for connections...")
-               hdlr := make(chan transport.Channel)
-               go func() {
-                       for {
-                               select {
-                               case ch = <-hdlr:
-                                       mc := transport.NewMsgChannel(ch)
-                                       if err = process(mc, t, p); err != nil {
-                                               logger.Println(logger.ERROR, 
err.Error())
-                                       }
-                               }
-                       }
-               }()
-               _, err = transport.NewChannelServer("tcp+0.0.0.0:2086", hdlr)
-       } else {
-               // connect to peer
-               fmt.Println("Connecting to target peer")
-               if ch, err = transport.NewChannel("tcp+172.17.0.5:2086"); err 
!= nil {
-                       logger.Println(logger.ERROR, err.Error())
-               }
-               mc := transport.NewMsgChannel(ch)
-               err = process(mc, p, t)
-       }
-       if err != nil {
-               fmt.Println(err)
-       }
-}
diff --git a/src/cmd/peer_mockup/peers.go b/src/cmd/peer_mockup/peers.go
deleted file mode 100644
index 16d3c87..0000000
--- a/src/cmd/peer_mockup/peers.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package main
-
-import (
-       "github.com/bfix/gospel/data"
-       "gnunet/core"
-       "gnunet/util"
-)
-
-func setupPeers(rnd bool) (err error) {
-
-       //------------------------------------------------------------------
-       // create local peer
-       //------------------------------------------------------------------
-       secret := []byte{
-               0x78, 0xde, 0xcf, 0xc0, 0x26, 0x9e, 0x62, 0x3d,
-               0x17, 0x24, 0xe6, 0x1b, 0x98, 0x25, 0xec, 0x2f,
-               0x40, 0x6b, 0x1e, 0x39, 0xa5, 0x19, 0xac, 0x9b,
-               0xb2, 0xdd, 0xf4, 0x6c, 0x12, 0x83, 0xdb, 0x86,
-       }
-       if rnd {
-               util.RndArray(secret)
-       }
-       p, err = core.NewPeer(secret, true)
-       if err != nil {
-               return
-       }
-       addr, _ := data.Marshal(util.NewIPAddress([]byte{172, 17, 0, 6}, 2086))
-       p.AddAddress(util.NewAddress("tcp", addr))
-
-       //------------------------------------------------------------------
-       // create remote peer
-       //------------------------------------------------------------------
-       id := []byte{
-               0x92, 0xdc, 0xbf, 0x39, 0x40, 0x2d, 0xc6, 0x3c,
-               0x97, 0xa6, 0x81, 0xe0, 0xfc, 0xd8, 0x7c, 0x74,
-               0x17, 0xd3, 0xa3, 0x8c, 0x52, 0xfd, 0xe0, 0x49,
-               0xbc, 0xd0, 0x1c, 0x0a, 0x0b, 0x8c, 0x02, 0x51,
-       }
-       t, err = core.NewPeer(id, false)
-       if err != nil {
-               return
-       }
-       addr, _ = data.Marshal(util.NewIPAddress([]byte{172, 17, 0, 5}, 2086))
-       t.AddAddress(util.NewAddress("tcp", addr))
-       return
-}
diff --git a/src/cmd/peer_mockup/process.go b/src/cmd/peer_mockup/process.go
deleted file mode 100644
index 510fc48..0000000
--- a/src/cmd/peer_mockup/process.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package main
-
-import (
-       "errors"
-       "fmt"
-
-       "gnunet/core"
-       "gnunet/crypto"
-       "gnunet/message"
-       "gnunet/transport"
-       "gnunet/util"
-
-       "github.com/bfix/gospel/concurrent"
-)
-
-var (
-       sig = concurrent.NewSignaller()
-)
-
-func process(ch *transport.MsgChannel, from, to *core.Peer) (err error) {
-       // create a new connection instance
-       c := transport.NewConnection(ch, from, to)
-       defer c.Close()
-
-       // read and push next message
-       in := make(chan message.Message)
-       go func() {
-               for {
-                       msg, err := c.Receive(sig)
-                       if err != nil {
-                               fmt.Printf("Receive: %s\n", err.Error())
-                               return
-                       }
-                       in <- msg
-               }
-       }()
-
-       // are we initiating the connection?
-       init := (from == p)
-       if init {
-               peerid := util.NewPeerID(p.GetID())
-               c.Send(message.NewTransportTcpWelcomeMsg(peerid), sig)
-       }
-
-       // remember peer addresses (only ONE!)
-       pAddr := p.GetAddressList()[0]
-       tAddr := t.GetAddressList()[0]
-
-       send := make(map[uint16]bool)
-       //received := make(map[uint16]bool)
-       pending := make(map[uint16]message.Message)
-
-       // process loop
-       for {
-               select {
-               case m := <-in:
-                       switch msg := m.(type) {
-
-                       case *message.TransportTcpWelcomeMsg:
-                               peerid := util.NewPeerID(p.GetID())
-                               if init {
-                                       c.Send(message.NewHelloMsg(peerid), sig)
-                                       target := util.NewPeerID(t.GetID())
-                                       
c.Send(message.NewTransportPingMsg(target, tAddr), sig)
-                               } else {
-                                       
c.Send(message.NewTransportTcpWelcomeMsg(peerid), sig)
-                               }
-
-                       case *message.HelloMsg:
-
-                       case *message.TransportPingMsg:
-                               mOut := 
message.NewTransportPongMsg(msg.Challenge, pAddr)
-                               if err := mOut.Sign(p.PrvKey()); err != nil {
-                                       return err
-                               }
-                               c.Send(mOut, sig)
-
-                       case *message.TransportPongMsg:
-                               rc, err := msg.Verify(t.PubKey())
-                               if err != nil {
-                                       return err
-                               }
-                               if !rc {
-                                       return errors.New("PONG verification 
failed")
-                               }
-                               send[message.TRANSPORT_PONG] = true
-                               if mOut, ok := 
pending[message.TRANSPORT_SESSION_SYN]; ok {
-                                       c.Send(mOut, sig)
-                               }
-
-                       case *message.SessionSynMsg:
-                               mOut := message.NewSessionSynAckMsg()
-                               mOut.Timestamp = msg.Timestamp
-                               if send[message.TRANSPORT_PONG] {
-                                       c.Send(mOut, sig)
-                               } else {
-                                       pending[message.TRANSPORT_SESSION_SYN] 
= mOut
-                               }
-
-                       case *message.SessionQuotaMsg:
-                               c.SetBandwidth(msg.Quota)
-
-                       case *message.SessionAckMsg:
-
-                       case *message.SessionKeepAliveMsg:
-                               
c.Send(message.NewSessionKeepAliveRespMsg(msg.Nonce), sig)
-
-                       case *message.EphemeralKeyMsg:
-                               rc, err := msg.Verify(t.PubKey())
-                               if err != nil {
-                                       return err
-                               }
-                               if !rc {
-                                       return errors.New("EPHKEY verification 
failed")
-                               }
-                               t.SetEphKeyMsg(msg)
-                               c.Send(p.EphKeyMsg(), sig)
-                               secret := crypto.SharedSecret(p.EphPrvKey(), 
t.EphKeyMsg().Public())
-                               c.SharedSecret(util.Clone(secret.Bits[:]))
-
-                       default:
-                               fmt.Printf("!!! %v\n", msg)
-                       }
-               default:
-               }
-       }
-       return nil
-}
diff --git a/src/cmd/revoke-zonekey/main.go b/src/cmd/revoke-zonekey/main.go
deleted file mode 100644
index 7deeda6..0000000
--- a/src/cmd/revoke-zonekey/main.go
+++ /dev/null
@@ -1,202 +0,0 @@
-// This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
-//
-// gnunet-go is free software: you can redistribute it and/or modify it
-// under the terms of the GNU Affero General Public License as published
-// by the Free Software Foundation, either version 3 of the License,
-// or (at your option) any later version.
-//
-// gnunet-go is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-// Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <http://www.gnu.org/licenses/>.
-//
-// SPDX-License-Identifier: AGPL3.0-or-later
-
-package main
-
-import (
-       "context"
-       "encoding/hex"
-       "flag"
-       "log"
-       "os"
-       "os/signal"
-       "sync"
-       "syscall"
-
-       "gnunet/service/revocation"
-       "gnunet/util"
-
-       "github.com/bfix/gospel/data"
-)
-
-func main() {
-       log.Println("*** Compute revocation data for a zone key")
-       log.Println("*** Copyright (c) 2020, Bernd Fix  >Y<")
-       log.Println("*** This is free software distributed under the Affero GPL 
v3.")
-
-       // handle command line arguments
-       var (
-               verbose  bool   // be verbose with messages
-               bits     int    // number of leading zero-bit requested
-               zonekey  string // zonekey to be revoked
-               filename string // name of file for persistance
-       )
-       flag.IntVar(&bits, "b", 25, "Number of leading zero bits")
-       flag.BoolVar(&verbose, "v", false, "verbose output")
-       flag.StringVar(&zonekey, "z", "", "Zone key to be revoked")
-       flag.StringVar(&filename, "f", "", "Name of file to store revocation")
-       flag.Parse()
-
-       if len(filename) == 0 {
-               log.Fatal("Missing '-f' argument (filename fot revocation 
data)")
-       }
-
-       // define layout of persistant data
-       var revData struct {
-               Rd      *revocation.RevDataCalc // Revocation data
-               T       util.RelativeTime       // time spend in calculations
-               Last    uint64                  // last value used for PoW test
-               Numbits uint8                   // number of leading zero-bits
-       }
-       dataBuf := make([]byte, 450)
-
-       // read revocation object from file
-       file, err := os.Open(filename)
-       cont := true
-       if err != nil {
-               if len(zonekey) != 52 {
-                       log.Fatal("Missing or invalid zonekey and no file 
specified -- aborting")
-               }
-               keyData, err := util.DecodeStringToBinary(zonekey, 32)
-               if err != nil {
-                       log.Fatal("Invalid zonekey: " + err.Error())
-               }
-               revData.Rd = revocation.NewRevDataCalc(keyData)
-               revData.Numbits = uint8(bits)
-               revData.T = util.NewRelativeTime(0)
-               cont = false
-       } else {
-               n, err := file.Read(dataBuf)
-               if err != nil {
-                       log.Fatal("Error reading file: " + err.Error())
-               }
-               if n != len(dataBuf) {
-                       log.Fatal("File corrupted -- aborting")
-               }
-               if err = data.Unmarshal(&revData, dataBuf); err != nil {
-                       log.Fatal("File corrupted: " + err.Error())
-               }
-               bits = int(revData.Numbits)
-               if err = file.Close(); err != nil {
-                       log.Fatal("Error closing file: " + err.Error())
-               }
-       }
-
-       if cont {
-               log.Printf("Revocation calculation started at %s\n", 
revData.Rd.Timestamp.String())
-               log.Printf("Time spent on calculation: %s\n", 
revData.T.String())
-               log.Printf("Last tested PoW value: %d\n", revData.Last)
-               log.Println("Continuing...")
-       } else {
-               log.Println("Starting new revocation calculation...")
-       }
-       log.Println("Press ^C to abort...")
-
-       // pre-set difficulty
-       log.Printf("Difficulty: %d\n", bits)
-       if bits < 25 {
-               log.Println("WARNING: difficulty is less than 25!")
-       }
-
-       // Start or continue calculation
-       ctx, cancelFcn := context.WithCancel(context.Background())
-       wg := new(sync.WaitGroup)
-       wg.Add(1)
-       go func() {
-               defer wg.Done()
-               cb := func(average float64, last uint64) {
-                       log.Printf("Improved PoW: %f average zero bits, %d 
steps\n", average, last)
-               }
-
-               startTime := util.AbsoluteTimeNow()
-               average, last := revData.Rd.Compute(ctx, bits, revData.Last, cb)
-               if average < float64(bits) {
-                       log.Printf("Incomplete revocation: Only %f zero bits on 
average!\n", average)
-               } else {
-                       log.Println("Revocation data object:")
-                       log.Println("   0x" + 
hex.EncodeToString(revData.Rd.Blob()))
-                       log.Println("Status:")
-                       rc := revData.Rd.Verify(false)
-                       switch {
-                       case rc == -1:
-                               log.Println("    Missing/invalid signature")
-                       case rc == -2:
-                               log.Println("    Expired revocation")
-                       case rc == -3:
-                               log.Println("    Wrong PoW sequence order")
-                       case rc < 25:
-                               log.Println("    Difficulty to small")
-                       default:
-                               log.Printf("    Difficulty: %d\n", rc)
-                       }
-               }
-               if !cont || last != revData.Last {
-                       revData.Last = last
-                       revData.T = util.AbsoluteTimeNow().Diff(startTime)
-
-                       log.Println("Writing revocation data to file...")
-                       file, err := os.Create(filename)
-                       if err != nil {
-                               log.Fatal("Can't write to output file: " + 
err.Error())
-                       }
-                       buf, err := data.Marshal(&revData)
-                       if err != nil {
-                               log.Fatal("Internal error: " + err.Error())
-                       }
-                       if len(buf) != len(dataBuf) {
-                               log.Fatalf("Internal error: Buffer mismatch %d 
!= %d", len(buf), len(dataBuf))
-                       }
-                       n, err := file.Write(buf)
-                       if err != nil {
-                               log.Fatal("Can't write to output file: " + 
err.Error())
-                       }
-                       if n != len(dataBuf) {
-                               log.Fatal("Can't write data to output file!")
-                       }
-                       if err = file.Close(); err != nil {
-                               log.Fatal("Error closing file: " + err.Error())
-                       }
-               }
-       }()
-
-       go func() {
-               // handle OS signals
-               sigCh := make(chan os.Signal, 5)
-               signal.Notify(sigCh)
-       loop:
-               for {
-                       select {
-                       // handle OS signals
-                       case sig := <-sigCh:
-                               switch sig {
-                               case syscall.SIGKILL, syscall.SIGINT, 
syscall.SIGTERM:
-                                       log.Printf("Terminating (on signal 
'%s')\n", sig)
-                                       cancelFcn()
-                                       break loop
-                               case syscall.SIGHUP:
-                                       log.Println("SIGHUP")
-                               case syscall.SIGURG:
-                                       // TODO: 
https://github.com/golang/go/issues/37942
-                               default:
-                                       log.Println("Unhandled signal: " + 
sig.String())
-                               }
-                       }
-               }
-       }()
-       wg.Wait()
-}
diff --git a/src/gnunet/build.sh b/src/gnunet/build.sh
new file mode 100755
index 0000000..5ec677f
--- /dev/null
+++ b/src/gnunet/build.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+go install -v -gcflags "-N -l" ./...
diff --git a/src/cmd/.gitignore b/src/gnunet/cmd/.gitignore
similarity index 100%
rename from src/cmd/.gitignore
rename to src/gnunet/cmd/.gitignore
diff --git a/src/cmd/gnunet-service-gns-go/main.go 
b/src/gnunet/cmd/gnunet-service-dht-test-go/main.go
similarity index 55%
copy from src/cmd/gnunet-service-gns-go/main.go
copy to src/gnunet/cmd/gnunet-service-dht-test-go/main.go
index 57eec7e..cd2bd1a 100644
--- a/src/cmd/gnunet-service-gns-go/main.go
+++ b/src/gnunet/cmd/gnunet-service-dht-test-go/main.go
@@ -1,5 +1,5 @@
 // This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
 //
 // gnunet-go is free software: you can redistribute it and/or modify it
 // under the terms of the GNU Affero General Public License as published
@@ -28,71 +28,93 @@ import (
        "time"
 
        "gnunet/config"
+       "gnunet/core"
        "gnunet/rpc"
        "gnunet/service"
-       "gnunet/service/gns"
+       "gnunet/service/dht"
 
        "github.com/bfix/gospel/logger"
 )
 
 func main() {
        defer func() {
-               logger.Println(logger.INFO, "[gns] Bye.")
+               logger.Println(logger.INFO, "[dht] Bye.")
                // flush last messages
                logger.Flush()
        }()
-       logger.Println(logger.INFO, "[gns] Starting service...")
+       logger.Println(logger.INFO, "[dht] Starting service...")
 
        var (
                cfgFile  string
-               srvEndp  string
+               socket   string
+               param    string
                err      error
                logLevel int
                rpcEndp  string
        )
        // handle command line arguments
        flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet 
configuration file")
-       flag.StringVar(&srvEndp, "s", "", "GNS service end-point")
-       flag.IntVar(&logLevel, "L", logger.INFO, "GNS log level (default: 
INFO)")
+       flag.StringVar(&socket, "s", "", "GNS service socket")
+       flag.StringVar(&param, "p", "", "socket parameters (<key>=<value>,...)")
+       flag.IntVar(&logLevel, "L", logger.INFO, "DHT log level (default: 
INFO)")
        flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)")
        flag.Parse()
 
        // read configuration file and set missing arguments.
        if err = config.ParseConfig(cfgFile); err != nil {
-               logger.Printf(logger.ERROR, "[gns] Invalid configuration file: 
%s\n", err.Error())
+               logger.Printf(logger.ERROR, "[dht] Invalid configuration file: 
%s\n", err.Error())
                return
        }
 
        // apply configuration
        logger.SetLogLevel(logLevel)
-       if len(srvEndp) == 0 {
-               srvEndp = config.Cfg.GNS.Endpoint
+       if len(socket) == 0 {
+               socket = config.Cfg.GNS.Service.Socket
+       }
+       params := make(map[string]string)
+       if len(param) == 0 {
+               for _, p := range strings.Split(param, ",") {
+                       kv := strings.SplitN(p, "=", 2)
+                       params[kv[0]] = kv[1]
+               }
+       } else {
+               params = config.Cfg.GNS.Service.Params
+       }
+
+       // instantiate core service
+       ctx, cancel := context.WithCancel(context.Background())
+       var local *core.Peer
+       if local, err = core.NewLocalPeer(config.Cfg.Local); err != nil {
+               logger.Printf(logger.ERROR, "[dht] No local peer: %s\n", 
err.Error())
+               return
+       }
+       var c *core.Core
+       if c, err = core.NewCore(ctx, local); err != nil {
+               logger.Printf(logger.ERROR, "[dht] core failed: %s\n", 
err.Error())
+               return
        }
 
-       // start a new GNS service
-       gns := gns.NewService()
-       srv := service.NewServiceImpl("gns", gns)
-       if err = srv.Start(srvEndp); err != nil {
-               logger.Printf(logger.ERROR, "[gns] Error: '%s'", err.Error())
+       // start a new DHT service
+       dht := dht.NewService(ctx, c)
+       srv := service.NewSocketHandler("dht", dht)
+       if err = srv.Start(ctx, socket, params); err != nil {
+               logger.Printf(logger.ERROR, "[dht] Failed to start DHT service: 
'%s'", err.Error())
                return
        }
 
        // start JSON-RPC server on request
-       var cancel func() = func() {}
        if len(rpcEndp) > 0 {
-               var ctx context.Context
-               ctx, cancel = context.WithCancel(context.Background())
-               parts := strings.Split(rpcEndp, "+")
+               parts := strings.Split(rpcEndp, ":")
                if parts[0] != "tcp" {
-                       logger.Println(logger.ERROR, "[gns] RPC must have a 
TCP/IP endpoint")
+                       logger.Println(logger.ERROR, "[dht] RPC must have a 
TCP/IP endpoint")
                        return
                }
                config.Cfg.RPC.Endpoint = parts[1]
                if err = rpc.Start(ctx); err != nil {
-                       logger.Printf(logger.ERROR, "[gns] RPC failed to start: 
%s", err.Error())
+                       logger.Printf(logger.ERROR, "[dht] RPC failed to start: 
%s", err.Error())
                        return
                }
-               rpc.Register(gns)
+               rpc.Register(dht)
        }
 
        // handle OS signals
@@ -109,18 +131,18 @@ loop:
                case sig := <-sigCh:
                        switch sig {
                        case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM:
-                               logger.Printf(logger.INFO, "[gns] Terminating 
service (on signal '%s')\n", sig)
+                               logger.Printf(logger.INFO, "[dht] Terminating 
service (on signal '%s')\n", sig)
                                break loop
                        case syscall.SIGHUP:
-                               logger.Println(logger.INFO, "[gns] SIGHUP")
+                               logger.Println(logger.INFO, "[dht] SIGHUP")
                        case syscall.SIGURG:
                                // TODO: 
https://github.com/golang/go/issues/37942
                        default:
-                               logger.Println(logger.INFO, "[gns] Unhandled 
signal: "+sig.String())
+                               logger.Println(logger.INFO, "[dht] Unhandled 
signal: "+sig.String())
                        }
                // handle heart beat
                case now := <-tick.C:
-                       logger.Println(logger.INFO, "[gns] Heart beat at 
"+now.String())
+                       logger.Println(logger.INFO, "[dht] Heart beat at 
"+now.String())
                }
        }
 
diff --git a/src/cmd/gnunet-service-gns-go/main.go 
b/src/gnunet/cmd/gnunet-service-gns-go/main.go
similarity index 81%
rename from src/cmd/gnunet-service-gns-go/main.go
rename to src/gnunet/cmd/gnunet-service-gns-go/main.go
index 57eec7e..6eb027b 100644
--- a/src/cmd/gnunet-service-gns-go/main.go
+++ b/src/gnunet/cmd/gnunet-service-gns-go/main.go
@@ -45,14 +45,16 @@ func main() {
 
        var (
                cfgFile  string
-               srvEndp  string
+               socket   string
+               param    string
                err      error
                logLevel int
                rpcEndp  string
        )
        // handle command line arguments
        flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet 
configuration file")
-       flag.StringVar(&srvEndp, "s", "", "GNS service end-point")
+       flag.StringVar(&socket, "s", "", "GNS service socket")
+       flag.StringVar(&param, "p", "", "socket parameters (<key>=<value>,...)")
        flag.IntVar(&logLevel, "L", logger.INFO, "GNS log level (default: 
INFO)")
        flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)")
        flag.Parse()
@@ -63,26 +65,33 @@ func main() {
                return
        }
 
-       // apply configuration
+       // apply configuration (from file and command-line)
        logger.SetLogLevel(logLevel)
-       if len(srvEndp) == 0 {
-               srvEndp = config.Cfg.GNS.Endpoint
+       if len(socket) == 0 {
+               socket = config.Cfg.GNS.Service.Socket
+       }
+       params := make(map[string]string)
+       if len(param) == 0 {
+               for _, p := range strings.Split(param, ",") {
+                       kv := strings.SplitN(p, "=", 2)
+                       params[kv[0]] = kv[1]
+               }
+       } else {
+               params = config.Cfg.GNS.Service.Params
        }
 
        // start a new GNS service
+       ctx, cancel := context.WithCancel(context.Background())
        gns := gns.NewService()
-       srv := service.NewServiceImpl("gns", gns)
-       if err = srv.Start(srvEndp); err != nil {
+       srv := service.NewSocketHandler("gns", gns)
+       if err = srv.Start(ctx, socket, params); err != nil {
                logger.Printf(logger.ERROR, "[gns] Error: '%s'", err.Error())
                return
        }
 
        // start JSON-RPC server on request
-       var cancel func() = func() {}
        if len(rpcEndp) > 0 {
-               var ctx context.Context
-               ctx, cancel = context.WithCancel(context.Background())
-               parts := strings.Split(rpcEndp, "+")
+               parts := strings.Split(rpcEndp, ":")
                if parts[0] != "tcp" {
                        logger.Println(logger.ERROR, "[gns] RPC must have a 
TCP/IP endpoint")
                        return
diff --git a/src/cmd/gnunet-service-revocation-go/main.go 
b/src/gnunet/cmd/gnunet-service-revocation-go/main.go
similarity index 83%
rename from src/cmd/gnunet-service-revocation-go/main.go
rename to src/gnunet/cmd/gnunet-service-revocation-go/main.go
index ec5ce73..e21732c 100644
--- a/src/cmd/gnunet-service-revocation-go/main.go
+++ b/src/gnunet/cmd/gnunet-service-revocation-go/main.go
@@ -45,14 +45,16 @@ func main() {
 
        var (
                cfgFile  string
-               srvEndp  string
+               socket   string
+               param    string
                err      error
                logLevel int
                rpcEndp  string
        )
        // handle command line arguments
        flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet 
configuration file")
-       flag.StringVar(&srvEndp, "s", "", "REVOCATION service end-point")
+       flag.StringVar(&socket, "s", "", "GNS service socket")
+       flag.StringVar(&param, "p", "", "socket parameters (<key>=<value>,...)")
        flag.IntVar(&logLevel, "L", logger.INFO, "REVOCATION log level 
(default: INFO)")
        flag.StringVar(&rpcEndp, "R", "", "JSON-RPC endpoint (default: none)")
        flag.Parse()
@@ -65,24 +67,31 @@ func main() {
 
        // apply configuration
        logger.SetLogLevel(logLevel)
-       if len(srvEndp) == 0 {
-               srvEndp = config.Cfg.GNS.Endpoint
+       if len(socket) == 0 {
+               socket = config.Cfg.GNS.Service.Socket
+       }
+       params := make(map[string]string)
+       if len(param) == 0 {
+               for _, p := range strings.Split(param, ",") {
+                       kv := strings.SplitN(p, "=", 2)
+                       params[kv[0]] = kv[1]
+               }
+       } else {
+               params = config.Cfg.GNS.Service.Params
        }
 
        // start a new REVOCATION service
+       ctx, cancel := context.WithCancel(context.Background())
        rvc := revocation.NewService()
-       srv := service.NewServiceImpl("revocation", rvc)
-       if err = srv.Start(srvEndp); err != nil {
+       srv := service.NewSocketHandler("revocation", rvc)
+       if err = srv.Start(ctx, socket, params); err != nil {
                logger.Printf(logger.ERROR, "[revocation] Error: '%s'\n", 
err.Error())
                return
        }
 
        // start JSON-RPC server on request
-       var cancel func() = func() {}
        if len(rpcEndp) > 0 {
-               var ctx context.Context
-               ctx, cancel = context.WithCancel(context.Background())
-               parts := strings.Split(rpcEndp, "+")
+               parts := strings.Split(rpcEndp, ":")
                if parts[0] != "tcp" {
                        logger.Println(logger.ERROR, "[revocation] RPC must 
have a TCP/IP endpoint")
                        return
diff --git a/src/gnunet/cmd/peer_mockup/main.go 
b/src/gnunet/cmd/peer_mockup/main.go
new file mode 100644
index 0000000..4288fb1
--- /dev/null
+++ b/src/gnunet/cmd/peer_mockup/main.go
@@ -0,0 +1,178 @@
+package main
+
+import (
+       "context"
+       "flag"
+       "fmt"
+       "os"
+       "os/signal"
+       "syscall"
+       "time"
+
+       "gnunet/config"
+       "gnunet/core"
+       "gnunet/crypto"
+       "gnunet/message"
+       "gnunet/service"
+
+       "github.com/bfix/gospel/logger"
+)
+
+var (
+       // configuration for local node
+       localCfg = &config.NodeConfig{
+               PrivateSeed: "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=",
+               Endpoints: []string{
+                       "udp:127.0.0.1:2086",
+               },
+       }
+       // configuration for remote node
+       remoteCfg  = "3GXXMNb5YpIUO7ejIR2Yy0Cf5texuLfDjHkXcqbPxkc="
+       remoteAddr = "udp:172.17.0.5:2086"
+
+       // top-level variables used accross functions
+       local  *core.Peer // local peer (with private key)
+       remote *core.Peer // remote peer
+       c      *core.Core
+       secret *crypto.HashCode
+)
+
+func main() {
+       ctx, cancel := context.WithCancel(context.Background())
+       defer cancel()
+
+       // handle command line arguments
+       var (
+               asServer bool
+               err      error
+       )
+       flag.BoolVar(&asServer, "s", false, "wait for incoming connections")
+       flag.Parse()
+
+       // setup peer and core instances
+       if local, err = core.NewLocalPeer(localCfg); err != nil {
+               fmt.Println("local failed: " + err.Error())
+               return
+       }
+       if c, err = core.NewCore(ctx, local); err != nil {
+               fmt.Println("core failed: " + err.Error())
+               return
+       }
+       if remote, err = core.NewPeer(remoteCfg); err != nil {
+               fmt.Println("remote failed: " + err.Error())
+               return
+       }
+
+       
fmt.Println("======================================================================")
+       fmt.Println("GNUnet peer mock-up (EXPERIMENTAL)     (c) 2018-2022 by 
Bernd Fix, >Y<")
+       fmt.Printf("    Identity '%s'\n", local.GetIDString())
+       fmt.Printf("    [%s]\n", local.GetID().String())
+       
fmt.Println("======================================================================")
+
+       // handle messages coming from network
+       module := service.NewModuleImpl()
+       listener := module.Run(ctx, process, nil)
+       c.Register("mockup", listener)
+
+       if !asServer {
+               // we start the message exchange
+               c.Send(ctx, remote.GetID(), 
message.NewTransportTCPWelcomeMsg(c.PeerID()))
+       }
+
+       // handle OS signals
+       sigCh := make(chan os.Signal, 5)
+       signal.Notify(sigCh)
+
+       // heart beat
+       tick := time.NewTicker(5 * time.Minute)
+
+loop:
+       for {
+               select {
+               // handle OS signals
+               case sig := <-sigCh:
+                       switch sig {
+                       case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM:
+                               logger.Printf(logger.INFO, "Terminating service 
(on signal '%s')\n", sig)
+                               break loop
+                       case syscall.SIGHUP:
+                               logger.Println(logger.INFO, "SIGHUP")
+                       case syscall.SIGURG:
+                               // TODO: 
https://github.com/golang/go/issues/37942
+                       default:
+                               logger.Println(logger.INFO, "Unhandled signal: 
"+sig.String())
+                       }
+               // handle heart beat
+               case now := <-tick.C:
+                       logger.Println(logger.INFO, "Heart beat at 
"+now.String())
+               }
+       }
+       // terminate pending routines
+       cancel()
+}
+
+// process incoming messages and send responses; it is used for protocol 
exploration only.
+// it tries to mimick the message flow between "real" GNUnet peers.
+func process(ctx context.Context, ev *core.Event) {
+
+       logger.Printf(logger.DBG, "<<< %s", ev.Msg.String())
+
+       switch msg := ev.Msg.(type) {
+
+       case *message.TransportTCPWelcomeMsg:
+               c.Send(ctx, ev.Peer, message.NewTransportPingMsg(ev.Peer, nil))
+
+       case *message.HelloMsg:
+
+       case *message.TransportPingMsg:
+               mOut := message.NewTransportPongMsg(msg.Challenge, nil)
+               if err := mOut.Sign(local.PrvKey()); err != nil {
+                       logger.Println(logger.ERROR, "PONG: signing failed")
+                       return
+               }
+               c.Send(ctx, ev.Peer, mOut)
+               logger.Printf(logger.DBG, ">>> %s", mOut)
+
+       case *message.TransportPongMsg:
+               rc, err := msg.Verify(remote.PubKey())
+               if err != nil {
+                       logger.Println(logger.ERROR, "PONG verification: 
"+err.Error())
+               }
+               if !rc {
+                       logger.Println(logger.ERROR, "PONG verification failed")
+               }
+
+       case *message.SessionSynMsg:
+               mOut := message.NewSessionSynAckMsg()
+               mOut.Timestamp = msg.Timestamp
+               c.Send(ctx, ev.Peer, mOut)
+               logger.Printf(logger.DBG, ">>> %s", mOut)
+
+       case *message.SessionQuotaMsg:
+
+       case *message.SessionAckMsg:
+
+       case *message.SessionKeepAliveMsg:
+               mOut := message.NewSessionKeepAliveRespMsg(msg.Nonce)
+               c.Send(ctx, ev.Peer, mOut)
+               logger.Printf(logger.DBG, ">>> %s", mOut)
+
+       case *message.EphemeralKeyMsg:
+               rc, err := msg.Verify(remote.PubKey())
+               if err != nil {
+                       logger.Println(logger.ERROR, "EPHKEY verification: 
"+err.Error())
+                       return
+               } else if !rc {
+                       logger.Println(logger.ERROR, "EPHKEY verification 
failed")
+                       return
+               }
+               remote.SetEphKeyMsg(msg)
+               mOut := local.EphKeyMsg()
+               c.Send(ctx, ev.Peer, mOut)
+               logger.Printf(logger.DBG, ">>> %s", mOut)
+               secret = crypto.SharedSecret(local.EphPrvKey(), 
remote.EphKeyMsg().Public())
+
+       default:
+               fmt.Printf("!!! %v\n", msg)
+       }
+}
diff --git a/src/gnunet/cmd/revoke-zonekey/main.go 
b/src/gnunet/cmd/revoke-zonekey/main.go
new file mode 100644
index 0000000..298a7e4
--- /dev/null
+++ b/src/gnunet/cmd/revoke-zonekey/main.go
@@ -0,0 +1,336 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package main
+
+import (
+       "context"
+       "encoding/base64"
+       "flag"
+       "fmt"
+       "log"
+       "os"
+       "os/signal"
+       "sync"
+       "syscall"
+
+       "gnunet/crypto"
+       "gnunet/service/revocation"
+       "gnunet/util"
+
+       "github.com/bfix/gospel/data"
+)
+
+//----------------------------------------------------------------------
+// Data structure used to calculate a valid revocation for a given
+// zone key.
+//----------------------------------------------------------------------
+
+// State of RevData calculation
+const (
+       S_NEW    = iota // start new PoW calculation
+       S_CONT          // continue PoW calculation
+       S_DONE          // PoW calculation done
+       S_SIGNED        // revocation data signed
+)
+
+// RevData is the storage layout for persistent data used by this program.
+// Data is read from and written to a file
+type RevData struct {
+       Rd      *revocation.RevDataCalc ``            // Revocation data
+       T       util.RelativeTime       ``            // time spend in 
calculations
+       Last    uint64                  `order:"big"` // last value used for 
PoW test
+       Numbits uint8                   ``            // number of leading 
zero-bits (difficulty)
+       State   uint8                   ``            // processing state
+}
+
+// ReadRevData restores revocation data from perstistent storage. If no
+// stored data is found, a new revocation data structure is returned.
+func ReadRevData(filename string, bits int, zk *crypto.ZoneKey) (rd *RevData, 
err error) {
+       // create new initialized revocation instance with no PoWs.
+       rd = &RevData{
+               Rd:      revocation.NewRevDataCalc(zk),
+               Numbits: uint8(bits),
+               T:       util.NewRelativeTime(0),
+               State:   S_NEW,
+       }
+
+       // read revocation object from file. If the file does not exist, a new
+       // calculation is started; otherwise the old calculation will continue.
+       var file *os.File
+       if file, err = os.Open(filename); err != nil {
+               return
+       }
+       // read existing file
+       dataBuf := make([]byte, rd.size())
+       var n int
+       if n, err = file.Read(dataBuf); err != nil {
+               err = fmt.Errorf("Error reading file: " + err.Error())
+               return
+       }
+       if n != len(dataBuf) {
+               err = fmt.Errorf("File size mismatch")
+               return
+       }
+       if err = data.Unmarshal(&rd, dataBuf); err != nil {
+               err = fmt.Errorf("File corrupted: " + err.Error())
+               return
+       }
+       if !zk.Equal(&rd.Rd.RevData.ZoneKeySig.ZoneKey) {
+               err = fmt.Errorf("Zone key mismatch")
+               return
+       }
+       bits = int(rd.Numbits)
+       if err = file.Close(); err != nil {
+               err = fmt.Errorf("Error closing file: " + err.Error())
+       }
+       return
+}
+
+// Write revocation data to file
+func (r *RevData) Write(filename string) (err error) {
+       var file *os.File
+       if file, err = os.Create(filename); err != nil {
+               return fmt.Errorf("Can't write to output file: " + err.Error())
+       }
+       var buf []byte
+       if buf, err = data.Marshal(r); err != nil {
+               return fmt.Errorf("Internal error: " + err.Error())
+       }
+       if len(buf) != r.size() {
+               return fmt.Errorf("Internal error: Buffer mismatch %d != %d", 
len(buf), r.size())
+       }
+       var n int
+       if n, err = file.Write(buf); err != nil {
+               return fmt.Errorf("Can't write to output file: " + err.Error())
+       }
+       if n != len(buf) {
+               return fmt.Errorf("Can't write data to output file!")
+       }
+       if err = file.Close(); err != nil {
+               return fmt.Errorf("Error closing file: " + err.Error())
+       }
+       return
+}
+
+// size of the RevData instance in bytes.
+func (r *RevData) size() int {
+       return 18 + r.Rd.Size()
+}
+
+// revoke-zonekey generates a revocation message in a multi-step/multi-state
+// process run stand-alone from other GNUnet services:
+//
+// (1) Generate the desired PoWs for the public zone key:
+//     This process can be started, stopped and resumed, so the long
+//     calculation time (usually days or even weeks) can be interruped if
+//     desired. For security reasons you should only pass the "-z" argument to
+//     this step but not the "-k" argument (private key) as it is not required
+//     to calculate the PoWs.
+//
+//
+// (2) A fully generated PoW set can be signed with the private key to create
+//     the final revocation data to be send out. This requires to pass the "-k"
+//     and "-z" argument.
+//
+// The two steps can be run (sequentially) on separate machines; step one 
requires
+// computing power nd memory and step two requires a trusted environment.
+func main() {
+       log.Println("*** Compute revocation data for a zone key")
+       log.Println("*** Copyright (c) 2020-2022, Bernd Fix  >Y<")
+       log.Println("*** This is free software distributed under the Affero GPL 
v3.")
+
+       //------------------------------------------------------------------
+       // handle command line arguments
+       //------------------------------------------------------------------
+       var (
+               verbose  bool   // be verbose with messages
+               bits     int    // number of leading zero-bit requested
+               zonekey  string // zonekey to be revoked
+               prvkey   string // private zonekey (base64-encoded key data)
+               testing  bool   // test mode (no minimum difficulty)
+               filename string // name of file for persistance
+       )
+       minDiff := revocation.MinDifficulty
+       flag.IntVar(&bits, "b", minDiff+1, "Number of leading zero bits")
+       flag.StringVar(&zonekey, "z", "", "Zone key to be revoked (zone ID)")
+       flag.StringVar(&prvkey, "k", "", "Private zone key (base54-encoded)")
+       flag.StringVar(&filename, "f", "", "Name of file to store revocation")
+       flag.BoolVar(&verbose, "v", false, "verbose output")
+       flag.BoolVar(&testing, "t", false, "test-mode only")
+       flag.Parse()
+
+       // check arguments (difficulty, zonekey and filename)
+       if bits < minDiff {
+               if testing {
+                       log.Printf("WARNING: difficulty is less than %d!", 
minDiff)
+               } else {
+                       log.Printf("INFO: difficulty set to %d (required 
minimum)", minDiff)
+                       bits = minDiff
+               }
+       }
+       if len(filename) == 0 {
+               log.Fatal("Missing '-f' argument (filename for revocation 
data)")
+       }
+
+       //------------------------------------------------------------------
+       // Handle zone keys.
+       //------------------------------------------------------------------
+       var (
+               keyData []byte              // binary key data
+               zk      *crypto.ZoneKey     // GNUnet zone key
+               sk      *crypto.ZonePrivate // GNUnet private zone key
+               err     error
+       )
+       // reconstruct public key
+       if keyData, err = util.DecodeStringToBinary(zonekey, 32); err != nil {
+               log.Fatal("Invalid zonekey encoding: " + err.Error())
+       }
+       if zk, err = crypto.NewZoneKey(keyData); err != nil {
+               log.Fatal("Invalid zonekey format: " + err.Error())
+       }
+       // reconstruct private key (optional)
+       if len(prvkey) > 0 {
+               if keyData, err = base64.StdEncoding.DecodeString(prvkey); err 
!= nil {
+                       log.Fatal("Invalid private zonekey encoding: " + 
err.Error())
+               }
+               if sk, err = crypto.NewZonePrivate(zk.Type, keyData); err != 
nil {
+                       log.Fatal("Invalid zonekey format: " + err.Error())
+               }
+               // verify consistency
+               if !zk.Equal(sk.Public()) {
+                       log.Fatal("Public and private zone keys don't match.")
+               }
+       }
+
+       //------------------------------------------------------------------
+       // Read revocation data from file to continue calculation or to sign
+       // the revocation. If no file exists, a new (empty) instance is
+       // returned.
+       //------------------------------------------------------------------
+       rd, err := ReadRevData(filename, bits, zk)
+
+       // handle revocation data state
+       switch rd.State {
+       case S_NEW:
+               log.Println("Starting new revocation calculation...")
+               rd.State = S_CONT
+
+       case S_CONT:
+               log.Printf("Revocation calculation started at %s\n", 
rd.Rd.Timestamp.String())
+               log.Printf("Time spent on calculation: %s\n", rd.T.String())
+               log.Printf("Last tested PoW value: %d\n", rd.Last)
+               log.Println("Continuing...")
+
+       case S_DONE:
+               // calculation complete: sign with private key
+               if sk == nil {
+                       log.Fatal("Need to sign revocation: private key is 
missing.")
+               }
+               log.Println("Signing revocation with private key")
+               if err := rd.Rd.Sign(sk); err != nil {
+                       log.Fatal("Failed to sign revocation: " + err.Error())
+               }
+               // write final revocation
+               rd.State = S_SIGNED
+               if err = rd.Write(filename); err != nil {
+                       log.Fatal("Failed to write revocation: " + err.Error())
+               }
+               log.Println("Revocation complete and ready for (later) use.")
+               return
+       }
+       // Continue (or start) calculation
+       log.Println("Press ^C to abort...")
+       log.Printf("Difficulty: %d\n", bits)
+
+       ctx, cancelFcn := context.WithCancel(context.Background())
+       wg := new(sync.WaitGroup)
+       wg.Add(1)
+       go func() {
+               defer wg.Done()
+               // show progress messages
+               cb := func(average float64, last uint64) {
+                       log.Printf("Improved PoW: %.2f average zero bits, %d 
steps\n", average, last)
+               }
+
+               // calculate revocation data until the required difficulty is 
met
+               // or the process is terminated by the user (by pressing ^C).
+               startTime := util.AbsoluteTimeNow()
+               average, last := rd.Rd.Compute(ctx, bits, rd.Last, cb)
+
+               // check achieved diffiulty (average)
+               if average < float64(bits) {
+                       // The calculation was interrupted; we still need to 
compute
+                       // more and better PoWs...
+                       log.Printf("Incomplete revocation: Only %f zero bits on 
average!\n", average)
+                       rd.State = S_CONT
+               } else {
+                       // we have reached the required PoW difficulty
+                       rd.State = S_DONE
+                       // check if we have a valid revocation.
+                       log.Println("Revocation calculation complete:")
+                       diff, rc := rd.Rd.Verify(false)
+                       switch {
+                       case rc == -1:
+                               log.Println("    Missing/invalid signature")
+                       case rc == -2:
+                               log.Println("    Expired revocation")
+                       case rc == -3:
+                               log.Println("    Wrong PoW sequence order")
+                       case diff < float64(revocation.MinAvgDifficulty):
+                               log.Println("    Difficulty to small")
+                       default:
+                               log.Printf("    Difficulty is %.2f\n", diff)
+                       }
+               }
+               // update elapsed time
+               rd.T.Add(util.AbsoluteTimeNow().Diff(startTime))
+               rd.Last = last
+
+               log.Println("Writing revocation data to file...")
+               if err = rd.Write(filename); err != nil {
+                       log.Fatal("Can't write to file: " + err.Error())
+               }
+       }()
+
+       go func() {
+               // handle OS signals
+               sigCh := make(chan os.Signal, 5)
+               signal.Notify(sigCh)
+       loop:
+               for {
+                       select {
+                       // handle OS signals
+                       case sig := <-sigCh:
+                               switch sig {
+                               case syscall.SIGKILL, syscall.SIGINT, 
syscall.SIGTERM:
+                                       log.Printf("Terminating (on signal 
'%s')\n", sig)
+                                       cancelFcn()
+                                       break loop
+                               case syscall.SIGHUP:
+                                       log.Println("SIGHUP")
+                               case syscall.SIGURG:
+                                       // TODO: 
https://github.com/golang/go/issues/37942
+                               default:
+                                       log.Println("Unhandled signal: " + 
sig.String())
+                               }
+                       }
+               }
+       }()
+       wg.Wait()
+}
diff --git a/src/cmd/vanityid/main.go b/src/gnunet/cmd/vanityid/main.go
similarity index 100%
rename from src/cmd/vanityid/main.go
rename to src/gnunet/cmd/vanityid/main.go
diff --git a/src/gnunet/config/config.go b/src/gnunet/config/config.go
index 914a017..41a65f0 100644
--- a/src/gnunet/config/config.go
+++ b/src/gnunet/config/config.go
@@ -28,56 +28,96 @@ import (
        "github.com/bfix/gospel/logger"
 )
 
-///////////////////////////////////////////////////////////////////////
+//----------------------------------------------------------------------
+// Configuration for local node
+//----------------------------------------------------------------------
+
+// NodeConfig holds parameters for the local node instance
+type NodeConfig struct {
+       PrivateSeed string   `json:"privateSeed"` // Node private key seed 
(base64)
+       Endpoints   []string `json:"endpoints"`   // list of endpoints available
+}
+
+//----------------------------------------------------------------------
+// Bootstrap configuration
+//----------------------------------------------------------------------
+
+// BootstrapConfig holds parameters for the initial connection to the network.
+type BootstrapConfig struct {
+       Nodes []string `json:"nodes"` // bootstrap nodes
+}
+
+//----------------------------------------------------------------------
 // RPC configuration
+//----------------------------------------------------------------------
 
 // RPCConfig contains parameters for the JSON-RPC service
 type RPCConfig struct {
-       Endpoint string `json:"endpoint"` // end-point of JSON-RPC service
+       Endpoint string `json:"endpoint"` // endpoint for JSON-RPC service
+}
+
+//----------------------------------------------------------------------
+// Generic service endpoint configuration (socket)
+//----------------------------------------------------------------------
+
+type ServiceConfig struct {
+       Socket string            `json:"socket"` // socket file name
+       Params map[string]string `json:"params"` // socket parameters
 }
 
-///////////////////////////////////////////////////////////////////////
+//----------------------------------------------------------------------
 // GNS configuration
+//----------------------------------------------------------------------
 
 // GNSConfig contains parameters for the GNU Name System service
 type GNSConfig struct {
-       Endpoint     string `json:"endpoint"`     // end-point of GNS service
-       DHTReplLevel int    `json:"dhtReplLevel"` // DHT replication level
-       MaxDepth     int    `json:"maxDepth"`     // maximum recursion depth in 
resolution
+       Service      *ServiceConfig `json:"service"`      // socket for GNS 
service
+       DHTReplLevel int            `json:"dhtReplLevel"` // DHT replication 
level
+       MaxDepth     int            `json:"maxDepth"`     // maximum recursion 
depth in resolution
 }
 
-///////////////////////////////////////////////////////////////////////
+//----------------------------------------------------------------------
 // DHT configuration
+//----------------------------------------------------------------------
 
 // DHTConfig contains parameters for the distributed hash table (DHT)
 type DHTConfig struct {
-       Endpoint string `json:"endpoint"` // end-point of DHT service
+       Service *ServiceConfig `json:"service"` // socket for DHT service
+       Storage string         `json:"storage"` // filesystem storage location
+       Cache   string         `json:"cache"`   // key/value cache
 }
 
-///////////////////////////////////////////////////////////////////////
+//----------------------------------------------------------------------
 // Namecache configuration
+//----------------------------------------------------------------------
 
 // NamecacheConfig contains parameters for the local name cache
 type NamecacheConfig struct {
-       Endpoint string `json:"endpoint"` // end-point of Namecache service
+       Service *ServiceConfig `json:"service"` // socket for Namecache service
+       Storage string         `json:"storage"` // key/value cache
 }
 
-///////////////////////////////////////////////////////////////////////
+//----------------------------------------------------------------------
 // Revocation configuration
+//----------------------------------------------------------------------
 
 // RevocationConfig contains parameters for the key revocation service
 type RevocationConfig struct {
-       Endpoint string `json:"endpoint"` // end-point of Revocation service
-       Storage  string `json:"storage"`  // persistance mechanism for 
revocation data
+       Service *ServiceConfig `json:"service"` // socket for Revocation service
+       Storage string         `json:"storage"` // persistance mechanism for 
revocation data
 }
 
-///////////////////////////////////////////////////////////////////////
+//----------------------------------------------------------------------
+// Combined configuration
+//----------------------------------------------------------------------
 
 // Environment settings
 type Environment map[string]string
 
 // Config is the aggregated configuration for GNUnet.
 type Config struct {
+       Local      *NodeConfig       `json:"local"`
+       Bootstrap  *BootstrapConfig  `json:"bootstrap"`
        Env        Environment       `json:"environ"`
        RPC        *RPCConfig        `json:"rpc"`
        DHT        *DHTConfig        `json:"dht"`
diff --git a/src/gnunet/config/gnunet-config.json 
b/src/gnunet/config/gnunet-config.json
index daf65f9..941cf21 100644
--- a/src/gnunet/config/gnunet-config.json
+++ b/src/gnunet/config/gnunet-config.json
@@ -1,24 +1,58 @@
 {
+       "local": {
+               "privateSeed": "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=",
+               "endpoints": [
+                       "r5n+ip+udp:127.0.0.1:6666"
+               ]
+       },
+       "bootstrap": {
+               "nodes": [
+                       
"gnunet://hello/7KTBJ90340HF1Q2GB0A57E2XJER4FDHX8HP5GHEB9125VPWPD27G/BNMDFN6HJCPWSPNBSEC06MC1K8QN1Z2DHRQSRXDTFR7FTBD4JHNBJ2RJAAEZ31FWG1Q3PMN3PXGZQ3Q7NTNEKQZFA7TE2Y46FM8E20R/1653499308?r5n%2Bip%2Budp%3A127.0.0.1%3A7654"
+               ]
+       },
        "environ": {
                "TMP": "/tmp",
                "RT_SYS": "${TMP}/gnunet-system-runtime"
        },
        "dht": {
-               "endpoint": "unix+${RT_SYS}/gnunet-service-dht.sock"
+               "service": {
+                       "socket": "${RT_SYS}/gnunet-service-dht.sock",
+                       "params": {
+                               "perm": "0770"
+                       }
+               },
+               "storage": "dht_file_store+/var/lib/gnunet/dht/store",
+               "cache": "dht_file_cache+/var/lib/gnunet/dht/cache+1000"
        },
        "gns": {
-               "endpoint": 
"unix+${RT_SYS}/gnunet-service-gns-go.sock+perm=0770",
+               "service": {
+                       "socket": "${RT_SYS}/gnunet-service-gns-go.sock",
+                       "params": {
+                               "perm": "0770"
+                       }
+               },
                "dhtReplLevel": 10,
                "maxDepth": 250
        },
        "namecache": {
-               "endpoint": "unix+${RT_SYS}/gnunet-service-namecache.sock"
+               "service": {
+                       "socket": "${RT_SYS}/gnunet-service-namecache.sock",
+                       "params": {
+                               "perm": "0770"
+                       }
+               },
+               "storage": "dht_file_cache:/var/lib/gnunet/namecache:1000"
        },
        "revocation": {
-               "endpoint": 
"unix+${RT_SYS}/gnunet-service-revocation-go.sock+perm=0770",
-               "storage": "redis+localhost:6397++15"
+               "service": {
+                       "socket": "${RT_SYS}/gnunet-service-revocation-go.sock",
+                       "params": {
+                               "perm": "0770"
+                       }
+               },
+               "storage": "redis:localhost:6397::15"
        },
        "rpc": {
-               "endpoint": "tcp+127.0.0.1:80"
+               "endpoint": "tcp:127.0.0.1:80"
        }
-}
+}
\ No newline at end of file
diff --git a/src/gnunet/core/core.go b/src/gnunet/core/core.go
new file mode 100644
index 0000000..c3bf355
--- /dev/null
+++ b/src/gnunet/core/core.go
@@ -0,0 +1,234 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package core
+
+import (
+       "context"
+       "gnunet/message"
+       "gnunet/service/dht/blocks"
+       "gnunet/transport"
+       "gnunet/util"
+       "net"
+       "time"
+
+       "github.com/bfix/gospel/data"
+)
+
+// Core service
+type Core struct {
+       // local peer instance
+       local *Peer
+
+       // incoming messages from transport
+       incoming chan *transport.TransportMessage
+
+       // reference to transport implementation
+       trans *transport.Transport
+
+       // registered listeners
+       listeners map[string]*Listener
+
+       // list of known peers with addresses
+       peers *util.PeerAddrList
+}
+
+//----------------------------------------------------------------------
+
+// NewCore creates and runs a new core instance.
+func NewCore(ctx context.Context, local *Peer) (c *Core, err error) {
+       // create new core instance
+       incoming := make(chan *transport.TransportMessage)
+       c = &Core{
+               local:     local,
+               incoming:  incoming,
+               listeners: make(map[string]*Listener),
+               trans:     transport.NewTransport(ctx, incoming),
+               peers:     util.NewPeerAddrList(),
+       }
+       // add all local peer endpoints to transport.
+       for _, addr := range local.addrList {
+               if _, err = c.trans.AddEndpoint(ctx, addr); err != nil {
+                       return
+               }
+       }
+       // run message pump
+       go func() {
+               // wait for incoming messages
+               for {
+                       select {
+                       // get (next) message from transport
+                       case tm := <-c.incoming:
+                               var ev *Event
+
+                               // inspect message for peer state events
+                               m, err := tm.Message()
+                               if err == nil {
+                                       switch msg := m.(type) {
+                                       case *message.HelloMsg:
+                                               // keep peer addresses
+                                               for _, addr := range 
msg.Addresses {
+                                                       a := &util.Address{
+                                                               Netw:    
addr.Transport,
+                                                               Address: 
addr.Address,
+                                                               Expires: 
addr.ExpireOn,
+                                                       }
+                                                       c.Learn(ctx, 
msg.PeerID, a)
+                                               }
+                                               // generate EV_CONNECT event
+                                               ev = new(Event)
+                                               ev.ID = EV_CONNECT
+                                               ev.Peer = tm.Peer
+                                               ev.Msg = msg
+                                               c.dispatch(ev)
+                                       }
+                               }
+                               // generate EV_MESSAGE event
+                               ev = new(Event)
+                               ev.ID = EV_MESSAGE
+                               ev.Peer = tm.Peer
+                               ev.Msg, _ = tm.Message()
+                               c.dispatch(ev)
+
+                       // wait for termination
+                       case <-ctx.Done():
+                               return
+                       }
+               }
+       }()
+       return
+}
+
+//----------------------------------------------------------------------
+
+// Send is a function that allows the local peer to send a protocol
+// message to a remote peer.
+func (c *Core) Send(ctx context.Context, peer *util.PeerID, msg 
message.Message) error {
+       // TODO: select best endpoint protocol for transport; now fixed to UDP
+       netw := "udp"
+       addr := c.peers.Get(peer.String(), netw)
+       payload, err := data.Marshal(msg)
+       if err != nil {
+               return err
+       }
+       tm := transport.NewTransportMessage(c.PeerID(), payload)
+       return c.trans.Send(ctx, addr, tm)
+}
+
+// Learn a (new) address for peer
+func (c *Core) Learn(ctx context.Context, peer *util.PeerID, addr 
*util.Address) (err error) {
+       if c.peers.Add(peer.String(), addr) == 1 {
+               // new peer id: send HELLO message to newly added peer
+               node := c.local
+               var hello *blocks.HelloBlock
+               hello, err = node.HelloData(time.Hour)
+               if err != nil {
+                       return
+               }
+               msg := message.NewHelloMsg(node.GetID())
+               for _, a := range hello.Addresses() {
+                       ha := message.NewHelloAddress(a)
+                       msg.AddAddress(ha)
+               }
+               err = c.Send(ctx, peer, msg)
+       }
+       return
+}
+
+// PeerID returns the peer id of the local node.
+func (c *Core) PeerID() *util.PeerID {
+       return c.local.GetID()
+}
+
+// TryConnect is a function which allows the local peer to attempt the
+// establishment of a connection to another peer using an address.
+// When the connection attempt is successful, information on the new
+// peer is offered through the PEER_CONNECTED signal.
+func (c *Core) TryConnect(peer *util.PeerID, addr net.Addr) error {
+       // select endpoint for address
+       if ep := c.findEndpoint(peer, addr); ep == nil {
+               return transport.ErrTransNoEndpoint
+       }
+       return nil
+}
+
+func (c *Core) findEndpoint(peer *util.PeerID, addr net.Addr) 
transport.Endpoint {
+       return nil
+}
+
+// Hold is a function which tells the underlay to keep a hold on to a
+// connection to a peer P. Underlays are usually limited in the number
+// of active connections. With this function the DHT can indicate to the
+// underlay which connections should preferably be preserved.
+func (c *Core) Hold(peer *util.PeerID) {}
+
+// Drop is a function which tells the underlay to drop the connection to a
+// peer P. This function is only there for symmetry and used during the
+// peer's shutdown to release all of the remaining HOLDs. As R5N always
+// prefers the longest-lived connections, it would never drop an active
+// connection that it has called HOLD() on before. Nevertheless, underlay
+// implementations should not rely on this always being true. A call to
+// DROP() also does not imply that the underlay must close the connection:
+// it merely removes the preference to preserve the connection that was
+// established by HOLD().
+func (c *Core) Drop(peer *util.PeerID) {}
+
+// L2NSE is ESTIMATE_NETWORK_SIZE(), a procedure that provides estimates
+// on the base-2 logarithm of the network size L2NSE, that is the base-2
+// logarithm number of peers in the network, for use by the routing
+// algorithm.
+func (c *Core) L2NSE() float64 {
+       return 0.
+}
+
+//----------------------------------------------------------------------
+// Event listener and event dispatch.
+//----------------------------------------------------------------------
+
+// Register a named event listener.
+func (c *Core) Register(name string, l *Listener) {
+       c.listeners[name] = l
+}
+
+// Unregister named event listener.
+func (c *Core) Unregister(name string) *Listener {
+       if l, ok := c.listeners[name]; ok {
+               delete(c.listeners, name)
+               return l
+       }
+       return nil
+}
+
+// internal: dispatch event to listeners
+func (c *Core) dispatch(ev *Event) {
+       // dispatch event to listeners
+       for _, l := range c.listeners {
+               if l.filter.CheckEvent(ev.ID) {
+                       mt := ev.Msg.Header().MsgType
+                       if ev.ID == EV_MESSAGE {
+                               if mt != 0 && !l.filter.CheckMsgType(mt) {
+                                       // skip event
+                                       return
+                               }
+                       }
+                       go func() {
+                               l.ch <- ev
+                       }()
+               }
+       }
+}
diff --git a/src/gnunet/core/core_test.go b/src/gnunet/core/core_test.go
new file mode 100644
index 0000000..102abf1
--- /dev/null
+++ b/src/gnunet/core/core_test.go
@@ -0,0 +1,150 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package core
+
+import (
+       "context"
+       "gnunet/config"
+       "gnunet/util"
+       "testing"
+       "time"
+)
+
+var (
+       peer1Cfg = &config.NodeConfig{
+               PrivateSeed: "iYK1wSi5XtCP774eNFk1LYXqKlOPEpwKBw+2/bMkE24=",
+               Endpoints:   []string{"udp://127.0.0.1:20861"},
+       }
+
+       peer2Cfg = &config.NodeConfig{
+               PrivateSeed: "Bv9umksEO51jjWWrOGEH+4r8wl9Vi+LItpdBpTOi2PE=",
+               Endpoints:   []string{"udp://127.0.0.1:20862"},
+       }
+)
+
+//----------------------------------------------------------------------
+// create and run a node with given spec
+//----------------------------------------------------------------------
+
+type TestNode struct {
+       id   int
+       t    *testing.T
+       peer *Peer
+       core *Core
+       addr *util.Address
+}
+
+func (n *TestNode) Learn(ctx context.Context, peer *util.PeerID, addr 
*util.Address) {
+       n.t.Logf("[%d] Learning %s for %s", n.id, addr.StringAll(), 
peer.String())
+       n.core.Learn(ctx, peer, addr)
+}
+
+func NewTestNode(t *testing.T, ctx context.Context, cfg *config.NodeConfig) 
(node *TestNode, err error) {
+
+       // create test node
+       node = new(TestNode)
+       node.t = t
+       node.id = util.NextID()
+
+       // create peer object
+       if node.peer, err = NewLocalPeer(cfg); err != nil {
+               return
+       }
+       t.Logf("[%d] Node %s starting", node.id, node.peer.GetID())
+
+       // create core service
+       if node.core, err = NewCore(ctx, node.peer); err != nil {
+               return
+       }
+       for _, addr := range node.core.trans.Endpoints() {
+               s := addr.Network() + ":" + addr.String()
+               if node.addr, err = util.ParseAddress(s); err != nil {
+                       continue
+               }
+               t.Logf("[%d] Listening on %s", node.id, s)
+       }
+
+       // register as event listener
+       incoming := make(chan *Event)
+       node.core.Register("test", NewListener(incoming, nil))
+
+       // heart beat
+       tick := time.NewTicker(5 * time.Minute)
+
+       // run event handler
+       go func() {
+               for {
+                       select {
+                       // show incoming event
+                       case ev := <-incoming:
+                               switch ev.ID {
+                               case EV_CONNECT:
+                                       t.Logf("[%d] <<< Peer %s connected", 
node.id, ev.Peer)
+                               case EV_DISCONNECT:
+                                       t.Logf("[%d] <<< Peer %s diconnected", 
node.id, ev.Peer)
+                               case EV_MESSAGE:
+                                       t.Logf("[%d] <<< Msg from %s of type 
%d", node.id, ev.Peer, ev.Msg.Header().MsgType)
+                               }
+
+                       // handle termination signal
+                       case <-ctx.Done():
+                               t.Logf("[%d] Shutting down node", node.id)
+                               return
+
+                       // handle heart beat
+                       case now := <-tick.C:
+                               t.Logf("[%d] Heart beat at %s", node.id, 
now.String())
+                       }
+               }
+       }()
+       return
+}
+
+//----------------------------------------------------------------------
+// Two node GNUnet (smallest and simplest network)
+//----------------------------------------------------------------------
+
+// TestCoreSimple test a two node network
+func TestCoreSimple(t *testing.T) {
+
+       // setup execution context
+       ctx, cancel := context.WithCancel(context.Background())
+       defer func() {
+               cancel()
+               time.Sleep(time.Second)
+       }()
+
+       // create and run nodes
+       node1, err := NewTestNode(t, ctx, peer1Cfg)
+       if err != nil {
+               t.Fatal(err)
+       }
+       node2, err := NewTestNode(t, ctx, peer2Cfg)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       // learn peer addresses (triggers HELLO)
+       for _, addr := range node2.core.trans.Endpoints() {
+               node1.Learn(ctx, node2.peer.GetID(), util.NewAddressWrap(addr))
+       }
+
+       // wait for 5 seconds
+       time.Sleep(5 * time.Second)
+}
diff --git a/src/gnunet/core/event.go b/src/gnunet/core/event.go
new file mode 100644
index 0000000..4eab112
--- /dev/null
+++ b/src/gnunet/core/event.go
@@ -0,0 +1,109 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package core
+
+import (
+       "gnunet/message"
+       "gnunet/util"
+)
+
+//----------------------------------------------------------------------
+// Core events and listeners
+//----------------------------------------------------------------------
+
+// Event types
+const (
+       EV_CONNECT    = iota // peer connected
+       EV_DISCONNECT        // peer disconnected
+       EV_MESSAGE           // incoming message
+)
+
+// EventFilter is a filter for events a listener is interested in.
+// The filter works on event types; if EV_MESSAGE is set, messages
+// can be filtered by message type also.
+type EventFilter struct {
+       evTypes  map[int]bool
+       msgTypes map[uint16]bool
+}
+
+// NewEventFilter creates a new empty filter instance.
+func NewEventFilter() *EventFilter {
+       return &EventFilter{
+               evTypes:  make(map[int]bool),
+               msgTypes: make(map[uint16]bool),
+       }
+}
+
+// AddEvent add  an event id to filter
+func (f *EventFilter) AddEvent(ev int) {
+       f.evTypes[ev] = true
+}
+
+// AddMsgType adds a message type to filter
+func (f *EventFilter) AddMsgType(mt uint16) {
+       f.evTypes[EV_MESSAGE] = true
+       f.msgTypes[mt] = true
+}
+
+// CheckEvent returns true if an event id is matched
+// by the filter or the filter is empty.
+func (f *EventFilter) CheckEvent(ev int) bool {
+       if len(f.evTypes) == 0 {
+               return true
+       }
+       _, ok := f.evTypes[ev]
+       return ok
+}
+
+// CheckMsgType returns true if a message type is matched
+// by the filter or the filter is empty.
+func (f *EventFilter) CheckMsgType(mt uint16) bool {
+       if len(f.msgTypes) == 0 {
+               return true
+       }
+       _, ok := f.msgTypes[mt]
+       return ok
+}
+
+// Event sent to listeners
+type Event struct {
+       ID   int             // event type
+       Peer *util.PeerID    // remote peer
+       Msg  message.Message // GNUnet message (can be nil)
+}
+
+//----------------------------------------------------------------------
+
+// Listener for network events
+type Listener struct {
+       ch     chan *Event  // listener channel
+       filter *EventFilter // event filter settimgs
+}
+
+// NewListener for given filter and receiving channel
+func NewListener(ch chan *Event, f *EventFilter) *Listener {
+       if f == nil {
+               // set empty default filter
+               f = NewEventFilter()
+       }
+       return &Listener{
+               ch:     ch,
+               filter: f,
+       }
+}
diff --git a/src/gnunet/core/peer.go b/src/gnunet/core/peer.go
index f81bab8..2b6fe74 100644
--- a/src/gnunet/core/peer.go
+++ b/src/gnunet/core/peer.go
@@ -1,5 +1,5 @@
 // This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
 //
 // gnunet-go is free software: you can redistribute it and/or modify it
 // under the terms of the GNU Affero General Public License as published
@@ -19,14 +19,30 @@
 package core
 
 import (
+       "encoding/base64"
        "fmt"
+       "time"
 
+       "gnunet/config"
        "gnunet/message"
+       "gnunet/service/dht/blocks"
        "gnunet/util"
 
        "github.com/bfix/gospel/crypto/ed25519"
 )
 
+//----------------------------------------------------------------------
+// GNUnet P2P network node (local or remote):
+//
+// * A LOCAL node has a long-term EdDSA key pair used for signing. The
+//   public key is the node identifier (PeerID).
+//   Local nodes hold additional attributes like ephemeral keys for message
+//   exchange or a list of network addresses the node can be reached on.
+//
+// * A REMOTE node only has a public EdDSA key used by the local node
+//   to verify signatures from the remote node.
+//----------------------------------------------------------------------
+
 // Peer represents a node in the GNUnet P2P network.
 type Peer struct {
        prv      *ed25519.PrivateKey      // node private key (long-term 
signing key)
@@ -37,27 +53,87 @@ type Peer struct {
        ephMsg   *message.EphemeralKeyMsg // ephemeral signing key message
 }
 
-// NewPeer instantiates a new peer object from given data. If a local peer
-// is created, the data is the seed for generating the private key of the node;
-// for a remote peer the data is the binary representation of its public key.
-func NewPeer(data []byte, local bool) (p *Peer, err error) {
+//----------------------------------------------------------------------
+// Create new peer objects
+//----------------------------------------------------------------------
+
+// NewLocalPeer creates a new local node from configuration data.
+func NewLocalPeer(cfg *config.NodeConfig) (p *Peer, err error) {
        p = new(Peer)
-       if local {
-               p.prv = ed25519.NewPrivateKeyFromSeed(data)
-               p.pub = p.prv.Public()
-               p.ephPrv, p.ephMsg, err = 
message.NewEphemeralKey(p.pub.Bytes(), p.prv)
-               if err != nil {
+
+       // get the key material for local node
+       var data []byte
+       if data, err = base64.StdEncoding.DecodeString(cfg.PrivateSeed); err != 
nil {
+               return
+       }
+       p.prv = ed25519.NewPrivateKeyFromSeed(data)
+       p.pub = p.prv.Public()
+       p.idString = util.EncodeBinaryToString(p.pub.Bytes())
+       p.ephPrv, p.ephMsg, err = message.NewEphemeralKey(p.pub.Bytes(), p.prv)
+       if err != nil {
+               return
+       }
+       // set the endpoint addresses for local node
+       p.addrList = make([]*util.Address, len(cfg.Endpoints))
+       var addr *util.Address
+       for i, a := range cfg.Endpoints {
+               if addr, err = util.ParseAddress(a); err != nil {
                        return
                }
-       } else {
-               p.prv = nil
-               p.pub = ed25519.NewPublicKeyFromBytes(data)
+               addr.Expires = util.NewAbsoluteTime(time.Now().Add(12 * 
time.Hour))
+               p.addrList[i] = addr
        }
+       return
+}
+
+// NewPeer instantiates a new (remote) peer object from given peer ID string.
+func NewPeer(peerID string) (p *Peer, err error) {
+       p = new(Peer)
+
+       // get the key material for local node
+       var data []byte
+       if data, err = util.DecodeStringToBinary(peerID, 32); err != nil {
+               return
+       }
+       p.prv = nil
+       p.pub = ed25519.NewPublicKeyFromBytes(data)
        p.idString = util.EncodeBinaryToString(p.pub.Bytes())
        p.addrList = make([]*util.Address, 0)
        return
 }
 
+//----------------------------------------------------------------------
+//----------------------------------------------------------------------
+
+// Address returns a peer address for the given transport protocol
+func (p *Peer) Address(transport string) *util.Address {
+       for _, addr := range p.addrList {
+               // skip expired entries
+               if addr.Expires.Expired() {
+                       continue
+               }
+               // filter by transport protocol
+               if len(transport) > 0 && transport != addr.Netw {
+                       continue
+               }
+               return addr
+       }
+       return nil
+}
+
+// HelloData returns the current HELLO data for the peer
+func (p *Peer) HelloData(ttl time.Duration) (h *blocks.HelloBlock, err error) {
+       // assemble HELLO data
+       h = new(blocks.HelloBlock)
+       h.PeerID = p.GetID()
+       h.Expire = util.NewAbsoluteTime(time.Now().Add(ttl))
+       h.SetAddresses(p.addrList)
+
+       // sign data
+       err = h.Sign(p.prv)
+       return
+}
+
 // EphKeyMsg returns a new initialized message to negotiate session keys.
 func (p *Peer) EphKeyMsg() *message.EphemeralKeyMsg {
        return p.ephMsg
@@ -84,8 +160,10 @@ func (p *Peer) PubKey() *ed25519.PublicKey {
 }
 
 // GetID returns the node ID (public key) in binary format
-func (p *Peer) GetID() []byte {
-       return p.pub.Bytes()
+func (p *Peer) GetID() *util.PeerID {
+       return &util.PeerID{
+               Key: util.Clone(p.pub.Bytes()),
+       }
 }
 
 // GetIDString returns the string representation of the public key of the node.
diff --git a/src/gnunet/core/peer_test.go b/src/gnunet/core/peer_test.go
new file mode 100644
index 0000000..28b328f
--- /dev/null
+++ b/src/gnunet/core/peer_test.go
@@ -0,0 +1,72 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package core
+
+import (
+       "gnunet/config"
+       "gnunet/service/dht/blocks"
+       "testing"
+       "time"
+)
+
+// test data
+var (
+       cfg = &config.NodeConfig{
+               PrivateSeed: "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=",
+               Endpoints: []string{
+                       "r5n+ip+udp://127.0.0.1:6666",
+               },
+       }
+       TTL = 6 * time.Hour
+)
+
+func TestPeerHello(t *testing.T) {
+
+       // generate new local node
+       node, err := NewLocalPeer(cfg)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       // get HELLO data for the node
+       h, err := node.HelloData(TTL)
+
+       // convert to URL and back
+       u := h.URL()
+       t.Log(u)
+       h2, err := blocks.ParseHelloURL(u)
+       if err != nil {
+               t.Fatal(err)
+       }
+       u2 := h2.URL()
+       t.Log(u2)
+
+       // check if HELLO data is the same
+       if !h.Equals(h2) {
+               t.Fatal("HELLO data mismatch")
+       }
+       // verify signature
+       ok, err := h.Verify()
+       if err != nil {
+               t.Fatal(err)
+       }
+       if !ok {
+               t.Fatal("failed to verify signature")
+       }
+}
diff --git a/src/gnunet/crypto/gns.go b/src/gnunet/crypto/gns.go
index 6aa7972..f4c5627 100644
--- a/src/gnunet/crypto/gns.go
+++ b/src/gnunet/crypto/gns.go
@@ -58,7 +58,8 @@ import (
 // example the RSA crypto scheme is outlined:
 //
 //   (1) Register/define a new GNS_TYPE_RSAKEY
-//   (2) Add ZONE_RSAKEY to the "Zone types" declarations below.
+//   (2) Add ZONE_RSAKEY and GNS_TYPE_RSAKEY to the "Zone types"
+//       declarations in this file.
 //   (3) Code the implementation in a file named `gns_rsakey.go`:
 //       You have to implement three interfaces (ZonePrivateImpl,
 //       ZoneKeyImpl and ZoneSigImpl) in three separate custom types.
@@ -145,6 +146,12 @@ type ZoneSigImpl interface {
 var (
        ZONE_PKEY  = uint32(enums.GNS_TYPE_PKEY)
        ZONE_EDKEY = uint32(enums.GNS_TYPE_EDKEY)
+
+       // register available zone types for BlockHandler
+       ZoneTypes = []int{
+               enums.GNS_TYPE_PKEY,
+               enums.GNS_TYPE_EDKEY,
+       }
 )
 
 //----------------------------------------------------------------------
diff --git a/src/gnunet/crypto/hash.go b/src/gnunet/crypto/hash.go
index bc715d4..049a8ef 100644
--- a/src/gnunet/crypto/hash.go
+++ b/src/gnunet/crypto/hash.go
@@ -19,6 +19,7 @@
 package crypto
 
 import (
+       "bytes"
        "crypto/sha512"
 
        "gnunet/util"
@@ -29,11 +30,20 @@ type HashCode struct {
        Bits []byte `size:"64"`
 }
 
-// NewHashCode creates a new, uninitalized hash value
-func NewHashCode() *HashCode {
-       return &HashCode{
+// Equals tests if two hash results are equal.
+func (hc *HashCode) Equals(n *HashCode) bool {
+       return bytes.Equal(hc.Bits, n.Bits)
+}
+
+// NewHashCode creates a new (initalized) hash value
+func NewHashCode(buf []byte) *HashCode {
+       hc := &HashCode{
                Bits: make([]byte, 64),
        }
+       if buf != nil {
+               util.CopyAlignedBlock(hc.Bits, buf)
+       }
+       return hc
 }
 
 // Hash returns the SHA-512 hash value of a given blob
diff --git a/src/gnunet/go.mod b/src/gnunet/go.mod
index 8527379..7383eaf 100644
--- a/src/gnunet/go.mod
+++ b/src/gnunet/go.mod
@@ -1,21 +1,25 @@
 module gnunet
 
-go 1.17
+go 1.18
 
 require (
-       github.com/bfix/gospel v1.2.10
-       github.com/go-redis/redis/v8 v8.5.0
-       github.com/go-sql-driver/mysql v1.5.0
+       github.com/bfix/gospel v1.2.11
+       github.com/go-redis/redis/v8 v8.11.5
+       github.com/go-sql-driver/mysql v1.6.0
        github.com/gorilla/mux v1.8.0
-       github.com/mattn/go-sqlite3 v1.14.6
-       github.com/miekg/dns v1.1.26
-       golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed
+       github.com/mattn/go-sqlite3 v1.14.13
+       github.com/miekg/dns v1.1.49
+       golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898
 )
 
 require (
-       github.com/cespare/xxhash/v2 v2.1.1 // indirect
+       github.com/cespare/xxhash/v2 v2.1.2 // indirect
        github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 
indirect
-       go.opentelemetry.io/otel v0.16.0 // indirect
-       golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
-       golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
+       golang.org/x/mod v0.4.2 // indirect
+       golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
+       golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
+       golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
+       golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 )
+
+replace github.com/bfix/gospel v1.2.11 => /vault/prj/libs/Go/Gospel
diff --git a/src/gnunet/go.sum b/src/gnunet/go.sum
index 4fa501c..ea3c328 100644
--- a/src/gnunet/go.sum
+++ b/src/gnunet/go.sum
@@ -1,123 +1,67 @@
-github.com/bfix/gospel v1.2.10 h1:a8l/sET2y+FVKIO5M1l5hdTlqLxstvkhp+b6FpAkxOU=
-github.com/bfix/gospel v1.2.10/go.mod 
h1:cdu63bA9ZdfeDoqZ+vnWOcbY9Puwdzmf5DMxMGMznRI=
-github.com/cespare/xxhash/v2 v2.1.1 
h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/davecgh/go-spew v1.1.0 
h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/bfix/gospel v1.2.11 h1:z/c6MFNq/lz4mO8+PK60a3NvH+lbTKAlLCShuFFZUvg=
+github.com/bfix/gospel v1.2.11/go.mod 
h1:cdu63bA9ZdfeDoqZ+vnWOcbY9Puwdzmf5DMxMGMznRI=
+github.com/cespare/xxhash/v2 v2.1.2 
h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f 
h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod 
h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/fsnotify/fsnotify v1.4.7/go.mod 
h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 
h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
-github.com/fsnotify/fsnotify v1.4.9/go.mod 
h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/go-redis/redis/v8 v8.5.0 
h1:L3r1Q3I5WOUdXZGCP6g44EruKh0u3n6co5Hl5xWkdGA=
-github.com/go-redis/redis/v8 v8.5.0/go.mod 
h1:YmEcgBDttjnkbMzDAhDtQxY9yVA7jMN6PCR5HeMvqFE=
-github.com/go-sql-driver/mysql v1.5.0 
h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
-github.com/go-sql-driver/mysql v1.5.0/go.mod 
h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/golang/protobuf v1.2.0/go.mod 
h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod 
h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod 
h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod 
h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod 
h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod 
h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.2/go.mod 
h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/google/go-cmp v0.3.0/go.mod 
h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod 
h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
-github.com/google/go-cmp v0.5.4/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/go-redis/redis/v8 v8.11.5 
h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod 
h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-sql-driver/mysql v1.6.0 
h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod 
h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod 
h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/hpcloud/tail v1.0.0/go.mod 
h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/huin/goupnp v1.0.0/go.mod 
h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
 github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod 
h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
-github.com/mattn/go-sqlite3 v1.14.6 
h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
-github.com/mattn/go-sqlite3 v1.14.6/go.mod 
h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
-github.com/miekg/dns v1.1.26/go.mod 
h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
-github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
-github.com/nxadm/tail v1.4.4/go.mod 
h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/onsi/ginkgo v1.6.0/go.mod 
h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.1/go.mod 
h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
-github.com/onsi/ginkgo v1.15.0/go.mod 
h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
-github.com/onsi/gomega v1.7.1/go.mod 
h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.10.1/go.mod 
h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
-github.com/onsi/gomega v1.10.5/go.mod 
h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
-github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1 
h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
-github.com/stretchr/testify v1.6.1/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/yuin/goldmark v1.2.1/go.mod 
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.opentelemetry.io/otel v0.16.0 
h1:uIWEbdeb4vpKPGITLsRVUS44L5oDbDUCZxn8lkxhmgw=
-go.opentelemetry.io/otel v0.16.0/go.mod 
h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
+github.com/mattn/go-sqlite3 v1.14.13 
h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
+github.com/mattn/go-sqlite3 v1.14.13/go.mod 
h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8=
+github.com/miekg/dns v1.1.49/go.mod 
h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
+github.com/yuin/goldmark v1.3.5/go.mod 
h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod 
h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod 
h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod 
h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod 
h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod 
h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed 
h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA=
-golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 
h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0=
+golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod 
h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod 
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod 
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod 
h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod 
h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod 
h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 
h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod 
h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 
h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
+golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod 
h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 
h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 
h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 
h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a 
h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod 
h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod 
h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod 
h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod 
h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 
h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod 
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 
h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod 
h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod 
h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod 
h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod 
h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod 
h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.23.0/go.mod 
h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod 
h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 
h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod 
h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c 
h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod 
h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
diff --git a/src/gnunet/message/factory.go b/src/gnunet/message/factory.go
index 8cbf9ce..6681eae 100644
--- a/src/gnunet/message/factory.go
+++ b/src/gnunet/message/factory.go
@@ -56,12 +56,16 @@ func NewEmptyMessage(msgType uint16) (Message, error) {
        //------------------------------------------------------------------
        // DHT
        //------------------------------------------------------------------
+       case DHT_CLIENT_PUT:
+               return NewDHTClientPutMsg(nil, 0, nil), nil
        case DHT_CLIENT_GET:
                return NewDHTClientGetMsg(nil), nil
        case DHT_CLIENT_GET_STOP:
                return NewDHTClientGetStopMsg(nil), nil
        case DHT_CLIENT_RESULT:
                return NewDHTClientResultMsg(nil), nil
+       case DHT_CLIENT_GET_RESULTS_KNOWN:
+               return NewDHTClientGetResultsKnownMsg(nil), nil
 
        //------------------------------------------------------------------
        // GNS
diff --git a/src/gnunet/message/message.go b/src/gnunet/message/message.go
index 1b826d7..60e36c9 100644
--- a/src/gnunet/message/message.go
+++ b/src/gnunet/message/message.go
@@ -29,11 +29,19 @@ var (
        ErrMsgHeaderTooSmall = errors.New("Message header too small")
 )
 
+//----------------------------------------------------------------------
+
 // Message is an interface for all GNUnet-specific messages.
 type Message interface {
+       // Header of message
        Header() *Header
+
+       // String returns a human-readable message
+       String() string
 }
 
+//----------------------------------------------------------------------
+
 // Header encapsulates the common part of all GNUnet messages (at the
 // beginning of the data).
 type Header struct {
diff --git a/src/gnunet/message/msg_dht.go b/src/gnunet/message/msg_dht.go
index 9b73557..bb8fc1a 100644
--- a/src/gnunet/message/msg_dht.go
+++ b/src/gnunet/message/msg_dht.go
@@ -27,6 +27,54 @@ import (
        "gnunet/util"
 )
 
+//----------------------------------------------------------------------
+// DHT_CLIENT_PUT
+//----------------------------------------------------------------------
+
+// DHTClientPutMsg is the message for putting values into the DHT
+type DHTClientPutMsg struct {
+       MsgSize   uint16            `order:"big"` // total size of message
+       MsgType   uint16            `order:"big"` // DHT_CLIENT_PUT (142)
+       Type      uint32            `order:"big"` // The type of the data 
(BLOCK_TYPE_???)
+       Options   uint32            `order:"big"` // Message options 
(DHT_RO_???)
+       ReplLevel uint32            `order:"big"` // Replication level for this 
message
+       Expire    util.AbsoluteTime // Expiration time
+       Key       *crypto.HashCode  // The key to be used
+       Data      []byte            `size:"*"` // Block data
+}
+
+// NewDHTClientPutMsg creates a new default DHTClientPutMsg object.
+func NewDHTClientPutMsg(key *crypto.HashCode, btype int, data []byte) 
*DHTClientPutMsg {
+       if key == nil {
+               key = new(crypto.HashCode)
+       }
+       var size uint16 = 88
+       if data != nil {
+               size += uint16(len(data))
+       }
+       return &DHTClientPutMsg{
+               MsgSize:   size,
+               MsgType:   DHT_CLIENT_PUT,
+               Type:      uint32(btype),
+               Options:   uint32(enums.DHT_RO_NONE),
+               ReplLevel: 1,
+               Expire:    util.AbsoluteTimeNever(),
+               Key:       key,
+               Data:      data,
+       }
+}
+
+// String returns a human-readable representation of the message.
+func (m *DHTClientPutMsg) String() string {
+       return 
fmt.Sprintf("DHTClientPutMsg{Type=%d,Expire=%s,Options=%d,Repl=%d,Key=%s}",
+               m.Type, m.Expire, m.Options, m.ReplLevel, 
hex.EncodeToString(m.Key.Bits))
+}
+
+// Header returns the message header in a separate instance.
+func (m *DHTClientPutMsg) Header() *Header {
+       return &Header{m.MsgSize, m.MsgType}
+}
+
 //----------------------------------------------------------------------
 // DHT_CLIENT_GET
 //----------------------------------------------------------------------
@@ -102,7 +150,7 @@ type DHTClientResultMsg struct {
 // NewDHTClientResultMsg creates a new default DHTClientResultMsg object.
 func NewDHTClientResultMsg(key *crypto.HashCode) *DHTClientResultMsg {
        if key == nil {
-               key = crypto.NewHashCode()
+               key = crypto.NewHashCode(nil)
        }
        return &DHTClientResultMsg{
                MsgSize:    64, // empty message size (no data)
@@ -163,3 +211,48 @@ func (m *DHTClientGetStopMsg) String() string {
 func (m *DHTClientGetStopMsg) Header() *Header {
        return &Header{m.MsgSize, m.MsgType}
 }
+
+//----------------------------------------------------------------------
+// DHT_CLIENT_GET_RESULTS_KNOWN
+//----------------------------------------------------------------------
+
+// DHTClientGetResultsKnownMsg is the message for putting values into the DHT
+type DHTClientGetResultsKnownMsg struct {
+       MsgSize  uint16             `order:"big"` // total size of message
+       MsgType  uint16             `order:"big"` // 
DHT_CLIENT_GET_RESULTS_KNOWN (156)
+       Reserved uint32             `order:"big"` // Reserved for further use
+       Key      *crypto.HashCode   // The key to search for
+       ID       uint64             `order:"big"` // Unique ID identifying this 
request
+       Known    []*crypto.HashCode `size:"*"`    // list of known results
+}
+
+// NewDHTClientPutMsg creates a new default DHTClientPutMsg object.
+func NewDHTClientGetResultsKnownMsg(key *crypto.HashCode) 
*DHTClientGetResultsKnownMsg {
+       if key == nil {
+               key = new(crypto.HashCode)
+       }
+       return &DHTClientGetResultsKnownMsg{
+               MsgSize: 80,
+               MsgType: DHT_CLIENT_GET_RESULTS_KNOWN,
+               Key:     key,
+               ID:      0,
+               Known:   make([]*crypto.HashCode, 0),
+       }
+}
+
+// AddKnown adds a known result to the list
+func (m *DHTClientGetResultsKnownMsg) AddKnown(hc *crypto.HashCode) {
+       m.Known = append(m.Known, hc)
+       m.MsgSize += 64
+}
+
+// String returns a human-readable representation of the message.
+func (m *DHTClientGetResultsKnownMsg) String() string {
+       return fmt.Sprintf("DHTClientGetResultsKnownMsg{Id:%d,Key=%s,Num=%d}",
+               m.ID, hex.EncodeToString(m.Key.Bits), len(m.Known))
+}
+
+// Header returns the message header in a separate instance.
+func (m *DHTClientGetResultsKnownMsg) Header() *Header {
+       return &Header{m.MsgSize, m.MsgType}
+}
diff --git a/src/gnunet/message/msg_gns.go b/src/gnunet/message/msg_gns.go
index 35630d6..9b85e40 100644
--- a/src/gnunet/message/msg_gns.go
+++ b/src/gnunet/message/msg_gns.go
@@ -25,15 +25,9 @@ import (
        "gnunet/enums"
        "gnunet/util"
 
-       "github.com/bfix/gospel/data"
        "github.com/bfix/gospel/logger"
 )
 
-// Error messages
-var (
-       ErrBlockNotDecrypted = fmt.Errorf("GNS block not decrypted")
-)
-
 //----------------------------------------------------------------------
 // GNS_LOOKUP
 //----------------------------------------------------------------------
@@ -147,94 +141,6 @@ func (rs *RecordSet) Expires() util.AbsoluteTime {
        return expires
 }
 
-// SignedBlockData represents the signed and encrypted list of resource
-// records stored in a GNSRecordSet
-type SignedBlockData struct {
-       Purpose *crypto.SignaturePurpose ``         // Size and purpose of 
signature (8 bytes)
-       Expire  util.AbsoluteTime        ``         // Expiration time of the 
block.
-       EncData []byte                   `size:"*"` // encrypted GNSRecordSet
-
-       // transient data (not serialized)
-       data []byte // decrypted GNSRecord set
-}
-
-// Block is the result of GNS lookups for a given label in a zone.
-// An encrypted and signed container for GNS resource records that represents
-// the "atomic" data structure associated with a GNS label in a given zone.
-type Block struct {
-       DerivedKeySig *crypto.ZoneSignature // Derived key used for signing
-       Block         *SignedBlockData
-
-       // transient data (not serialized)
-       checked   bool // block integrity checked
-       verified  bool // block signature verified (internal)
-       decrypted bool // block data decrypted (internal)
-}
-
-// String returns the human-readable representation of a GNSBlock
-func (b *Block) String() string {
-       return fmt.Sprintf("GNSBlock{Verified=%v,Decrypted=%v,data=[%d]}",
-               b.verified, b.decrypted, len(b.Block.EncData))
-}
-
-// Records returns the list of resource records in a block.
-func (b *Block) Records() ([]*ResourceRecord, error) {
-       // check if block is decrypted
-       if !b.decrypted {
-               return nil, ErrBlockNotDecrypted
-       }
-       // parse block data into record set
-       rs := NewRecordSet()
-       if err := data.Unmarshal(rs, b.Block.data); err != nil {
-               return nil, err
-       }
-       return rs.Records, nil
-}
-
-// Verify the integrity of the block data from a signature.
-func (b *Block) Verify(zkey *crypto.ZoneKey, label string) (err error) {
-       // Integrity check performed
-       b.checked = true
-
-       // verify derived key
-       dkey := b.DerivedKeySig.ZoneKey
-       dkey2, _ := zkey.Derive(label, "gns")
-       if !dkey.Equal(dkey2) {
-               return fmt.Errorf("invalid signature key for GNS Block")
-       }
-       // verify signature
-       var buf []byte
-       if buf, err = data.Marshal(b.Block); err != nil {
-               return
-       }
-       b.verified, err = b.DerivedKeySig.Verify(buf)
-       return
-}
-
-// Decrypt block data with a key derived from zone key and label.
-func (b *Block) Decrypt(zkey *crypto.ZoneKey, label string) (err error) {
-       // decrypt payload
-       b.Block.data, err = zkey.Decrypt(b.Block.EncData, label, b.Block.Expire)
-       b.decrypted = true
-       return
-}
-
-// NewBlock instantiates an empty GNS block
-func NewBlock() *Block {
-       return &Block{
-               DerivedKeySig: nil,
-               Block: &SignedBlockData{
-                       Purpose: new(crypto.SignaturePurpose),
-                       Expire:  *new(util.AbsoluteTime),
-                       EncData: nil,
-                       data:    nil,
-               },
-               checked:   false,
-               verified:  false,
-               decrypted: false,
-       }
-}
-
 // ResourceRecord is the GNUnet-specific representation of resource
 // records (not to be confused with DNS resource records).
 type ResourceRecord struct {
diff --git a/src/gnunet/message/msg_hello.go b/src/gnunet/message/msg_hello.go
new file mode 100644
index 0000000..18fe9d5
--- /dev/null
+++ b/src/gnunet/message/msg_hello.go
@@ -0,0 +1,103 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package message
+
+import (
+       "fmt"
+       "gnunet/util"
+)
+
+//----------------------------------------------------------------------
+// HELLO
+//
+// A HELLO message is used to exchange information about transports with
+// other peers. This struct is always followed by the actual network
+// addresses which have the format:
+//
+// 1) transport-name (0-terminated)
+// 2) address-length (uint16_t, network byte order)
+// 3) address expiration
+// 4) address (address-length bytes)
+//----------------------------------------------------------------------
+
+// HelloAddress represents a (generic) peer address with expiration date
+type HelloAddress struct {
+       Transport string            // Name of transport
+       AddrSize  uint16            `order:"big"` // Size of address entry
+       ExpireOn  util.AbsoluteTime // Expiry date
+       Address   []byte            `size:"AddrSize"` // Address specification
+}
+
+// NewHelloAddress create a new HELLO address from the given address
+func NewHelloAddress(a *util.Address) *HelloAddress {
+       addr := &HelloAddress{
+               Transport: a.Netw,
+               AddrSize:  uint16(len(a.Address)),
+               ExpireOn:  a.Expires,
+               Address:   make([]byte, len(a.Address)),
+       }
+       copy(addr.Address, a.Address)
+       return addr
+}
+
+// String returns a human-readable representation of the message.
+func (a *HelloAddress) String() string {
+       return fmt.Sprintf("Address{%s,expire=%s}",
+               util.AddressString(a.Transport, a.Address), a.ExpireOn)
+}
+
+// HelloMsg is a message send by peers to announce their presence
+type HelloMsg struct {
+       MsgSize    uint16          `order:"big"` // total size of message
+       MsgType    uint16          `order:"big"` // HELLO (17)
+       FriendOnly uint32          `order:"big"` // =1: do not gossip this HELLO
+       PeerID     *util.PeerID    // EdDSA public key (long-term)
+       Addresses  []*HelloAddress `size:"*"` // List of end-point addressess
+}
+
+// NewHelloMsg creates a new HELLO msg for a given peer.
+func NewHelloMsg(peerid *util.PeerID) *HelloMsg {
+       if peerid == nil {
+               peerid = util.NewPeerID(nil)
+       }
+       return &HelloMsg{
+               MsgSize:    40,
+               MsgType:    HELLO,
+               FriendOnly: 0,
+               PeerID:     peerid,
+               Addresses:  make([]*HelloAddress, 0),
+       }
+}
+
+// String returns a human-readable representation of the message.
+func (m *HelloMsg) String() string {
+       return fmt.Sprintf("HelloMsg{peer=%s,friendsonly=%d,addr=%v}",
+               m.PeerID, m.FriendOnly, m.Addresses)
+}
+
+// AddAddress adds a new address to the HELLO message.
+func (m *HelloMsg) AddAddress(a *HelloAddress) {
+       m.Addresses = append(m.Addresses, a)
+       m.MsgSize += uint16(len(a.Transport)) + a.AddrSize + 11
+}
+
+// Header returns the message header in a separate instance.
+func (m *HelloMsg) Header() *Header {
+       return &Header{m.MsgSize, m.MsgType}
+}
diff --git a/src/gnunet/message/msg_namecache.go 
b/src/gnunet/message/msg_namecache.go
index c3a0ac7..5acb807 100644
--- a/src/gnunet/message/msg_namecache.go
+++ b/src/gnunet/message/msg_namecache.go
@@ -21,8 +21,8 @@ package message
 import (
        "encoding/hex"
        "fmt"
-
        "gnunet/crypto"
+       "gnunet/service/dht/blocks"
        "gnunet/util"
 )
 
@@ -41,7 +41,7 @@ type NamecacheLookupMsg struct {
 // NewNamecacheLookupMsg creates a new default message.
 func NewNamecacheLookupMsg(query *crypto.HashCode) *NamecacheLookupMsg {
        if query == nil {
-               query = crypto.NewHashCode()
+               query = crypto.NewHashCode(nil)
        }
        return &NamecacheLookupMsg{
                MsgSize: 72,
@@ -114,7 +114,7 @@ type NamecacheCacheMsg struct {
 }
 
 // NewNamecacheCacheMsg creates a new default message.
-func NewNamecacheCacheMsg(block *Block) *NamecacheCacheMsg {
+func NewNamecacheCacheMsg(block *blocks.GNSBlock) *NamecacheCacheMsg {
        msg := &NamecacheCacheMsg{
                MsgSize:       108,
                MsgType:       NAMECACHE_BLOCK_CACHE,
@@ -125,10 +125,10 @@ func NewNamecacheCacheMsg(block *Block) 
*NamecacheCacheMsg {
        }
        if block != nil {
                msg.DerivedKeySig = block.DerivedKeySig
-               msg.Expire = block.Block.Expire
-               size := len(block.Block.EncData)
+               msg.Expire = block.Body.Expire
+               size := len(block.Body.Data)
                msg.EncData = make([]byte, size)
-               copy(msg.EncData, block.Block.EncData)
+               copy(msg.EncData, block.Body.Data)
                msg.MsgSize += uint16(size)
        }
        return msg
diff --git a/src/gnunet/message/msg_transport.go 
b/src/gnunet/message/msg_transport.go
index 6e64c4d..d0e927b 100644
--- a/src/gnunet/message/msg_transport.go
+++ b/src/gnunet/message/msg_transport.go
@@ -1,5 +1,5 @@
 // This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
 //
 // gnunet-go is free software: you can redistribute it and/or modify it
 // under the terms of the GNU Affero General Public License as published
@@ -224,86 +224,6 @@ func (m *TransportPongMsg) Verify(pub *ed25519.PublicKey) 
(bool, error) {
        return pub.EdVerify(data, sig)
 }
 
-//----------------------------------------------------------------------
-// HELLO
-//
-// A HELLO message is used to exchange information about
-// transports with other peers.  This struct is always
-// followed by the actual network addresses which have
-// the format:
-//
-// 1) transport-name (0-terminated)
-// 2) address-length (uint16_t, network byte order)
-// 3) address expiration
-// 4) address (address-length bytes)
-//----------------------------------------------------------------------
-
-// HelloAddress represents a (generic) peer address with expiration date
-type HelloAddress struct {
-       Transport string            // Name of transport
-       AddrSize  uint16            `order:"big"` // Size of address entry
-       ExpireOn  util.AbsoluteTime // Expiry date
-       Address   []byte            `size:"AddrSize"` // Address specification
-}
-
-// NewHelloAddress create a new HELLO address from the given address
-func NewHelloAddress(a *util.Address) *HelloAddress {
-       addr := &HelloAddress{
-               Transport: a.Transport,
-               AddrSize:  uint16(len(a.Address)),
-               ExpireOn:  util.AbsoluteTimeNow().Add(12 * time.Hour),
-               Address:   make([]byte, len(a.Address)),
-       }
-       copy(addr.Address, a.Address)
-       return addr
-}
-
-// String returns a human-readable representation of the message.
-func (a *HelloAddress) String() string {
-       return fmt.Sprintf("Address{%s,expire=%s}",
-               util.AddressString(a.Transport, a.Address), a.ExpireOn)
-}
-
-// HelloMsg is a message send by peers to announce their presence
-type HelloMsg struct {
-       MsgSize    uint16          `order:"big"` // total size of message
-       MsgType    uint16          `order:"big"` // HELLO (17)
-       FriendOnly uint32          `order:"big"` // =1: do not gossip this HELLO
-       PeerID     *util.PeerID    // EdDSA public key (long-term)
-       Addresses  []*HelloAddress `size:"*"` // List of end-point addressess
-}
-
-// NewHelloMsg creates a new HELLO msg for a given peer.
-func NewHelloMsg(peerid *util.PeerID) *HelloMsg {
-       if peerid == nil {
-               peerid = util.NewPeerID(nil)
-       }
-       return &HelloMsg{
-               MsgSize:    40,
-               MsgType:    HELLO,
-               FriendOnly: 0,
-               PeerID:     peerid,
-               Addresses:  make([]*HelloAddress, 0),
-       }
-}
-
-// String returns a human-readable representation of the message.
-func (m *HelloMsg) String() string {
-       return fmt.Sprintf("HelloMsg{peer=%s,friendsonly=%d,addr=%v}",
-               m.PeerID, m.FriendOnly, m.Addresses)
-}
-
-// AddAddress adds a new address to the HELLO message.
-func (m *HelloMsg) AddAddress(a *HelloAddress) {
-       m.Addresses = append(m.Addresses, a)
-       m.MsgSize += uint16(len(a.Transport)) + a.AddrSize + 11
-}
-
-// Header returns the message header in a separate instance.
-func (m *HelloMsg) Header() *Header {
-       return &Header{m.MsgSize, m.MsgType}
-}
-
 //----------------------------------------------------------------------
 // TRANSPORT_SESSION_ACK
 //----------------------------------------------------------------------
diff --git a/src/gnunet/modules.go b/src/gnunet/modules.go
index f4fdb1b..e47699f 100644
--- a/src/gnunet/modules.go
+++ b/src/gnunet/modules.go
@@ -52,29 +52,33 @@ func (inst Instances) Register() {
        rpc.Register(inst.Revocation)
 }
 
-// Local reference to instance list
+// Local reference to instance list. The list is initialized
+// by core.
 var (
        Modules Instances
 )
 
+/* TODO: implement
 // Initialize instance list and link module functions as required.
-func init() {
+// This function is called by core on start-up.
+func Init(ctx context.Context) {
 
        // Namecache (no calls to other modules)
-       Modules.Namecache = new(namecache.NamecacheModule)
+       Modules.Namecache = namecache.NewModule(ctx, c)
 
        // DHT (no calls to other modules)
-       Modules.DHT = new(dht.Module)
+       Modules.DHT = dht.NewModule(ctx, c)
 
        // Revocation (no calls to other modules)
-       Modules.Revocation = revocation.NewModule()
+       Modules.Revocation = revocation.NewModule(ctx, c)
 
        // GNS (calls Namecache, DHT and Identity)
-       Modules.GNS = &gns.Module{
-               LookupLocal:      Modules.Namecache.Get,
-               StoreLocal:       Modules.Namecache.Put,
-               LookupRemote:     Modules.DHT.Get,
-               RevocationQuery:  Modules.Revocation.Query,
-               RevocationRevoke: Modules.Revocation.Revoke,
-       }
+       gns := gns.NewModule(ctx, c)
+       Modules.GNS = gns
+       gns.LookupLocal = Modules.Namecache.Get
+       gns.StoreLocal = Modules.Namecache.Put
+       gns.LookupRemote = Modules.DHT.Get
+       gns.RevocationQuery = Modules.Revocation.Query
+       gns.RevocationRevoke = Modules.Revocation.Revoke
 }
+*/
diff --git a/src/gnunet/service/client.go b/src/gnunet/service/client.go
index a196c09..19dc4c4 100644
--- a/src/gnunet/service/client.go
+++ b/src/gnunet/service/client.go
@@ -19,39 +19,39 @@
 package service
 
 import (
+       "context"
        "gnunet/message"
-       "gnunet/transport"
 
        "github.com/bfix/gospel/logger"
 )
 
 // Client type: Use to perform client-side interactions with GNUnet services.
 type Client struct {
-       ch *transport.MsgChannel // channel for message exchange
+       ch *Connection // channel for message exchange
 }
 
-// NewClient creates a new client instance for the given channel endpoint.
-func NewClient(endp string) (*Client, error) {
-       // create a new channel to endpoint.
-       ch, err := transport.NewChannel(endp)
+// NewClient connects to a socket with given path
+func NewClient(ctx context.Context, path string) (*Client, error) {
+       // create a connection
+       ch, err := NewConnection(ctx, path)
        if err != nil {
                return nil, err
        }
        // wrap into a message channel for the client.
        return &Client{
-               ch: transport.NewMsgChannel(ch),
+               ch: ch,
        }, nil
 }
 
-// SendRequest sends a give message to the service.
-func (c *Client) SendRequest(ctx *SessionContext, req message.Message) error {
-       return c.ch.Send(req, ctx.Signaller())
+// SendRequest sends a message to the service.
+func (c *Client) SendRequest(ctx context.Context, req message.Message) error {
+       return c.ch.Send(ctx, req)
 }
 
 // ReceiveResponse waits for a response from the service; it can be interrupted
 // by sending "false" to the cmd channel.
-func (c *Client) ReceiveResponse(ctx *SessionContext) (message.Message, error) 
{
-       return c.ch.Receive(ctx.Signaller())
+func (c *Client) ReceiveResponse(ctx context.Context) (message.Message, error) 
{
+       return c.ch.Receive(ctx)
 }
 
 // Close a client; no further message exchange is possible.
@@ -62,15 +62,15 @@ func (c *Client) Close() error {
 // RequestResponse is a helper method for a one request - one response
 // secenarios of client/serice interactions.
 func RequestResponse(
-       ctx *SessionContext,
+       ctx context.Context,
        caller string,
        callee string,
-       endp string,
+       path string,
        req message.Message) (message.Message, error) {
 
        // client-connect to the service
        logger.Printf(logger.DBG, "[%s] Connecting to %s service...\n", caller, 
callee)
-       cl, err := NewClient(endp)
+       cl, err := NewClient(ctx, path)
        if err != nil {
                return nil, err
        }
diff --git a/src/gnunet/service/connection.go b/src/gnunet/service/connection.go
new file mode 100644
index 0000000..1c690c5
--- /dev/null
+++ b/src/gnunet/service/connection.go
@@ -0,0 +1,280 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package service
+
+import (
+       "context"
+       "errors"
+       "fmt"
+       "gnunet/message"
+       "net"
+       "os"
+       "strconv"
+
+       "github.com/bfix/gospel/data"
+       "github.com/bfix/gospel/logger"
+)
+
+// Error codes
+var (
+       ErrConnectionNotOpened   = errors.New("channel not opened")
+       ErrConnectionInterrupted = errors.New("channel interrupted")
+)
+
+//======================================================================
+
+// Connection is a channel for GNUnet message exchange (send/receive)
+// based on Unix domain sockets. It is used locally by services and
+// clients in the standard GNUnet environment.
+type Connection struct {
+       path string   // file name of Unix socket
+       conn net.Conn // associated connection
+       buf  []byte   // read/write buffer
+}
+
+// NewConnection creates a new connection to a socket with given path.
+// This is used by clients to connect to a service.
+func NewConnection(ctx context.Context, path string) (s *Connection, err 
error) {
+       var d net.Dialer
+       s = new(Connection)
+       s.path = path
+       s.buf = make([]byte, 65536)
+       s.conn, err = d.DialContext(ctx, "unix", path)
+       return
+}
+
+// Close a socket connection
+func (s *Connection) Close() error {
+       if s.conn != nil {
+               rc := s.conn.Close()
+               s.conn = nil
+               return rc
+       }
+       return ErrConnectionNotOpened
+}
+
+// Send a GNUnet message over a socket.
+func (s *Connection) Send(ctx context.Context, msg message.Message) error {
+       // convert message to binary data
+       data, err := data.Marshal(msg)
+       if err != nil {
+               return err
+       }
+       // check message header size and packet size
+       mh, err := message.GetMsgHeader(data)
+       if err != nil {
+               return err
+       }
+       if len(data) != int(mh.MsgSize) {
+               return errors.New("send: message size mismatch")
+       }
+
+       // send packet
+       n, err := s.write(ctx, data)
+       if err != nil {
+               return err
+       }
+       if n != len(data) {
+               return errors.New("incomplete send")
+       }
+       return nil
+}
+
+// Receive GNUnet messages from socket.
+func (s *Connection) Receive(ctx context.Context) (message.Message, error) {
+       // get bytes from socket
+       get := func(pos, count int) error {
+               n, err := s.read(ctx, s.buf[pos:pos+count])
+               if err != nil {
+                       return err
+               }
+               if n != count {
+                       return errors.New("not enough bytes on network")
+               }
+               return nil
+       }
+       // read header first
+       if err := get(0, 4); err != nil {
+               return nil, err
+       }
+       mh, err := message.GetMsgHeader(s.buf[:4])
+       if err != nil {
+               return nil, err
+       }
+       // get rest of message
+       if err := get(4, int(mh.MsgSize)-4); err != nil {
+               return nil, err
+       }
+       msg, err := message.NewEmptyMessage(mh.MsgType)
+       if err != nil {
+               return nil, err
+       }
+       if msg == nil {
+               return nil, fmt.Errorf("message{%d} is nil", mh.MsgType)
+       }
+       if err = data.Unmarshal(msg, s.buf[:mh.MsgSize]); err != nil {
+               return nil, err
+       }
+       return msg, nil
+}
+
+//----------------------------------------------------------------------
+// internal methods
+//----------------------------------------------------------------------
+
+// result of read/write operations on sockets.
+type result struct {
+       n   int   // number of bytes read/written
+       err error // error (or nil)
+}
+
+// Read bytes from a socket into buffer: Returns the number of read
+// bytes and an error code. Only works on open channels ;)
+func (s *Connection) read(ctx context.Context, buf []byte) (int, error) {
+       // check if the channel is open
+       if s.conn == nil {
+               return 0, ErrConnectionNotOpened
+       }
+       // perform read operation
+       ch := make(chan *result)
+       go func() {
+               n, err := s.conn.Read(buf)
+               ch <- &result{n, err}
+       }()
+       for {
+               select {
+               // terminate on request
+               case <-ctx.Done():
+                       return 0, ErrConnectionInterrupted
+
+               // handle result of read operation
+               case res := <-ch:
+                       return res.n, res.err
+               }
+       }
+}
+
+// Write buffer to socket and returns the number of bytes written and an
+// optional error code.
+func (s *Connection) write(ctx context.Context, buf []byte) (int, error) {
+       // check if we have an open socket to write to.
+       if s.conn == nil {
+               return 0, ErrConnectionNotOpened
+       }
+       // perform write operation
+       ch := make(chan *result)
+       go func() {
+               n, err := s.conn.Write(buf)
+               ch <- &result{n, err}
+       }()
+       for {
+               select {
+               // handle terminate command
+               case <-ctx.Done():
+                       return 0, ErrConnectionInterrupted
+
+               // handle result of write operation
+               case res := <-ch:
+                       return res.n, res.err
+               }
+       }
+}
+
+//======================================================================
+
+// ConnectionManager to handle client connections on a socket.
+type ConnectionManager struct {
+       listener net.Listener // reference to listener object
+       running  bool         // server running?
+}
+
+// NewConnectionManager creates a new socket connection manager. Incoming
+// connections from clients are dispatched to a handler channel.
+func NewConnectionManager(
+       ctx context.Context, // execution context
+       path string, // socket file name
+       params map[string]string, // connection parameters
+       hdlr chan *Connection, // handler for incoming connections
+) (cs *ConnectionManager, err error) {
+
+       // instantiate channel server
+       cs = &ConnectionManager{
+               listener: nil,
+               running:  false,
+       }
+       // create listener
+       var lc net.ListenConfig
+       if cs.listener, err = lc.Listen(ctx, "unix", path); err != nil {
+               return
+       }
+       // handle additional parameters
+       if params != nil {
+               for key, value := range params {
+                       switch key {
+                       case "perm": // set permissions on 'unix'
+                               if perm, err := strconv.ParseInt(value, 8, 32); 
err == nil {
+                                       if err := os.Chmod(path, 
os.FileMode(perm)); err != nil {
+                                               logger.Printf(
+                                                       logger.ERROR,
+                                                       "MsgChannelServer: 
Failed to set permissions %s on %s: %s\n",
+                                                       path, value, 
err.Error())
+
+                                       }
+                               } else {
+                                       logger.Printf(
+                                               logger.ERROR,
+                                               "MsgChannelServer: Invalid 
permissions '%s'\n",
+                                               value)
+                               }
+                       }
+               }
+       }
+       // run go routine to handle channel requests from clients
+       cs.running = true
+       go func() {
+               for cs.running {
+                       conn, err := cs.listener.Accept()
+                       if err != nil {
+                               break
+                       }
+                       // handle connection
+                       c := &Connection{
+                               conn: conn,
+                               path: path,
+                               buf:  make([]byte, 65536),
+                       }
+                       hdlr <- c
+               }
+               if cs.listener != nil {
+                       cs.listener.Close()
+               }
+       }()
+       return cs, nil
+}
+
+// Close a network channel server (= stop the server)
+func (s *ConnectionManager) Close() error {
+       s.running = false
+       if s.listener != nil {
+               err := s.listener.Close()
+               s.listener = nil
+               return err
+       }
+       return nil
+}
diff --git a/src/gnunet/service/context.go b/src/gnunet/service/context.go
deleted file mode 100644
index 4ae786d..0000000
--- a/src/gnunet/service/context.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
-//
-// gnunet-go is free software: you can redistribute it and/or modify it
-// under the terms of the GNU Affero General Public License as published
-// by the Free Software Foundation, either version 3 of the License,
-// or (at your option) any later version.
-//
-// gnunet-go is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-// Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <http://www.gnu.org/licenses/>.
-//
-// SPDX-License-Identifier: AGPL3.0-or-later
-
-package service
-
-import (
-       "sync"
-
-       "gnunet/util"
-
-       "github.com/bfix/gospel/concurrent"
-)
-
-// SessionContext is used to set a context for each client connection handled
-// by a service; the session is handled by the 'ServeClient' method of a
-// service implementation.
-type SessionContext struct {
-       ID       int                   // session identifier
-       wg       *sync.WaitGroup       // wait group for the session
-       sig      *concurrent.Signaller // signaller for the session
-       pending  int                   // number of pending go-routines
-       active   bool                  // is the context active (un-cancelled)?
-       onCancel *sync.Mutex           // only run one Cancel() at a time
-}
-
-// NewSessionContext instantiates a new session context.
-func NewSessionContext() *SessionContext {
-       return &SessionContext{
-               ID:       util.NextID(),
-               wg:       new(sync.WaitGroup),
-               sig:      concurrent.NewSignaller(),
-               pending:  0,
-               active:   true,
-               onCancel: new(sync.Mutex),
-       }
-}
-
-// Cancel all go-routines associated with this context.
-func (ctx *SessionContext) Cancel() {
-       ctx.onCancel.Lock()
-       if ctx.active {
-               // we are going out-of-business
-               ctx.active = false
-               // send signal to terminate...
-               ctx.sig.Send(true)
-               // wait for session go-routines to finish
-               ctx.wg.Wait()
-       }
-       ctx.onCancel.Unlock()
-}
-
-// Add a go-routine to the wait group.
-func (ctx *SessionContext) Add() {
-       ctx.wg.Add(1)
-       ctx.pending++
-}
-
-// Remove a go-routine from the wait group.
-func (ctx *SessionContext) Remove() {
-       ctx.wg.Done()
-       ctx.pending--
-}
-
-// Waiting returns the number of waiting go-routines.
-func (ctx *SessionContext) Waiting() int {
-       return ctx.pending
-}
-
-// Signaller returns the working instance for the context.
-func (ctx *SessionContext) Signaller() *concurrent.Signaller {
-       return ctx.sig
-}
diff --git a/src/gnunet/service/dht/blocks/generic.go 
b/src/gnunet/service/dht/blocks/generic.go
new file mode 100644
index 0000000..6301e3b
--- /dev/null
+++ b/src/gnunet/service/dht/blocks/generic.go
@@ -0,0 +1,196 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package blocks
+
+import (
+       "bytes"
+       "encoding/gob"
+       "encoding/hex"
+       "fmt"
+       "gnunet/crypto"
+       "gnunet/util"
+
+       "github.com/bfix/gospel/data"
+)
+
+//----------------------------------------------------------------------
+// Query/Block interfaces for generic DHT handling
+//----------------------------------------------------------------------
+
+// DHT Query interface
+type Query interface {
+
+       // Key returns the DHT key for a block
+       Key() *crypto.HashCode
+
+       // Get retrieves the value of a named query parameter. The value is
+       // unchanged if the key is not in the map or if the value in the map
+       // has an incompatible type.
+       Get(key string, value any) bool
+
+       // Set stores the value of a named query parameter
+       Set(key string, value any)
+
+       // Verify the integrity of a retrieved block (optional). Override in
+       // custom query types to implement block-specific integrity checks
+       // (see GNSQuery for example).
+       Verify(blk Block) error
+
+       // Decrypt block content (optional). Override in custom query types to
+       // implement block-specific encryption (see GNSQuery for example).
+       Decrypt(blk Block) error
+
+       // String returns the human-readable representation of a query
+       String() string
+}
+
+// DHT Block interface
+type Block interface {
+
+       // Data returns the DHT block data (unstructured without type and
+       // expiration information.
+       Data() []byte
+
+       // Return the block type
+       Type() uint16
+
+       // Expire returns the block expiration
+       Expire() util.AbsoluteTime
+
+       // Verify the integrity of a block (optional). Override in custom query
+       // types to implement block-specific integrity checks (see GNSBlock for
+       // example). This verification is usually weaker than the verification
+       // method from a Query (see GNSBlock.Verify for explanation).
+       Verify() error
+
+       // String returns the human-readable representation of a block
+       String() string
+}
+
+// Unwrap (raw) block to a specific block type
+func Unwrap(blk Block, obj interface{}) error {
+       return data.Unmarshal(obj, blk.Data())
+}
+
+//----------------------------------------------------------------------
+// Generic interface implementations without persistent attributes
+//----------------------------------------------------------------------
+
+// GenericQuery is the binary representation of a DHT key
+type GenericQuery struct {
+       // Key for repository queries (local/remote)
+       key *crypto.HashCode
+
+       // query parameters (binary value representation)
+       params map[string][]byte
+}
+
+// Key interface method implementation
+func (q *GenericQuery) Key() *crypto.HashCode {
+       return q.key
+}
+
+// Get retrieves the value of a named query parameter
+func (q *GenericQuery) Get(key string, value any) bool {
+       data, ok := q.params[key]
+       if !ok {
+               return false
+       }
+       dec := gob.NewDecoder(bytes.NewReader(data))
+       return dec.Decode(value) != nil
+}
+
+// Set stores the value of a named query parameter
+func (q *GenericQuery) Set(key string, value any) {
+       wrt := new(bytes.Buffer)
+       enc := gob.NewEncoder(wrt)
+       if enc.Encode(value) == nil {
+               q.params[key] = wrt.Bytes()
+       }
+}
+
+// Verify interface method implementation
+func (q *GenericQuery) Verify(b Block) error {
+       // no verification, no errors ;)
+       return nil
+}
+
+// Decrypt interface method implementation
+func (q *GenericQuery) Decrypt(b Block) error {
+       // no decryption, no errors ;)
+       return nil
+}
+
+// String returns the human-readable representation of a block
+func (q *GenericQuery) String() string {
+       return fmt.Sprintf("GenericQuery{key=%s}", 
hex.EncodeToString(q.Key().Bits))
+}
+
+// NewGenericQuery creates a simple Query from hash code.
+func NewGenericQuery(buf []byte) *GenericQuery {
+       return &GenericQuery{
+               key:    crypto.NewHashCode(buf),
+               params: make(map[string][]byte),
+       }
+}
+
+//----------------------------------------------------------------------
+
+// GenericBlock is the block in simple binary representation
+type GenericBlock struct {
+       block  []byte            // block data
+       btype  uint16            // block type
+       expire util.AbsoluteTime // expiration date
+}
+
+// Data interface method implementation
+func (b *GenericBlock) Data() []byte {
+       return b.block
+}
+
+// Type returns the block type
+func (b *GenericBlock) Type() uint16 {
+       return b.btype
+}
+
+// Expire returns the block expiration
+func (b *GenericBlock) Expire() util.AbsoluteTime {
+       return b.expire
+}
+
+// String returns the human-readable representation of a block
+func (b *GenericBlock) String() string {
+       return fmt.Sprintf("GenericBlock{type=%d,expires=%s,data=[%d]}",
+               b.btype, b.expire.String(), len(b.block))
+}
+
+// Verify interface method implementation
+func (b *GenericBlock) Verify() error {
+       // no verification, no errors ;)
+       return nil
+}
+
+// NewGenericBlock creates a Block from binary data.
+func NewGenericBlock(buf []byte) *GenericBlock {
+       return &GenericBlock{
+               block:  util.Clone(buf),
+               btype:  DHT_BLOCK_ANY,            // unknown block type
+               expire: util.AbsoluteTimeNever(), // never expires
+       }
+}
diff --git a/src/gnunet/service/dht/blocks/generic_test.go 
b/src/gnunet/service/dht/blocks/generic_test.go
new file mode 100644
index 0000000..51ee5a1
--- /dev/null
+++ b/src/gnunet/service/dht/blocks/generic_test.go
@@ -0,0 +1,67 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package blocks
+
+import (
+       "bytes"
+       "testing"
+)
+
+// Test parameter handling for queries
+func TestQueryParams(t *testing.T) {
+       q := NewGenericQuery(nil)
+
+       // set parameters
+       var (
+               btype uint16 = DHT_BLOCK_ANY
+               flags uint32 = 0
+               name  string = "Test"
+               data         = make([]byte, 8)
+       )
+       q.Set("btype", btype)
+       q.Set("flags", flags)
+       q.Set("name", name)
+       q.Set("data", data)
+
+       // get parameters
+       var (
+               t_btype uint16
+               t_flags uint32
+               t_name  string
+               t_data  []byte
+       )
+       q.Get("btype", &t_btype)
+       q.Get("flags", &t_flags)
+       q.Get("name", &t_name)
+       q.Get("data", &t_data)
+
+       // check for unchanged data
+       if btype != t_btype {
+               t.Fatal("btype mismatch")
+       }
+       if flags != t_flags {
+               t.Fatal("flags mismatch")
+       }
+       if name != t_name {
+               t.Fatal("name mismatch")
+       }
+       if !bytes.Equal(data, t_data) {
+               t.Fatal("data mismatch")
+       }
+}
diff --git a/src/gnunet/service/dht/blocks/gns.go 
b/src/gnunet/service/dht/blocks/gns.go
new file mode 100644
index 0000000..2085677
--- /dev/null
+++ b/src/gnunet/service/dht/blocks/gns.go
@@ -0,0 +1,172 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package blocks
+
+import (
+       "errors"
+       "fmt"
+       "gnunet/crypto"
+       "gnunet/util"
+
+       "github.com/bfix/gospel/data"
+)
+
+// Error messages
+var (
+       ErrBlockNotDecrypted = fmt.Errorf("GNS block not decrypted")
+)
+
+//----------------------------------------------------------------------
+// Query key for GNS lookups
+//----------------------------------------------------------------------
+
+// GNSQuery specifies the context for a basic GNS name lookup of an (atomic)
+// label in a given zone identified by its public key.
+type GNSQuery struct {
+       GenericQuery
+       Zone    *crypto.ZoneKey // Public zone key
+       Label   string          // Atomic label
+       derived *crypto.ZoneKey // Derived zone key from (pkey,label)
+}
+
+// Verify the integrity of the block data from a signature.
+func (q *GNSQuery) Verify(b Block) (err error) {
+       switch blk := b.(type) {
+       case *GNSBlock:
+               // Integrity check performed
+               blk.checked = true
+
+               // verify derived key
+               dkey := blk.DerivedKeySig.ZoneKey
+               dkey2, _ := q.Zone.Derive(q.Label, "gns")
+               if !dkey.Equal(dkey2) {
+                       return fmt.Errorf("invalid signature key for GNS Block")
+               }
+               // verify signature
+               var buf []byte
+               if buf, err = data.Marshal(blk.Body); err != nil {
+                       return
+               }
+               blk.verified, err = blk.DerivedKeySig.Verify(buf)
+
+       default:
+               err = errors.New("can't verify block type")
+       }
+       return
+}
+
+// Decrypt block data with a key derived from zone key and label.
+func (q *GNSQuery) Decrypt(b Block) (err error) {
+       switch blk := b.(type) {
+       case *GNSBlock:
+               // decrypt GNS payload
+               blk.data, err = q.Zone.Decrypt(blk.Body.Data, q.Label, 
blk.Body.Expire)
+               blk.decrypted = true
+               return
+
+       default:
+               err = errors.New("can't decrypt block type")
+       }
+       return
+}
+
+// NewGNSQuery assembles a new Query object for the given zone and label.
+func NewGNSQuery(zkey *crypto.ZoneKey, label string) *GNSQuery {
+       // derive a public key from (pkey,label) and set the repository
+       // key as the SHA512 hash of the binary key representation.
+       // (key blinding)
+       pd, _ := zkey.Derive(label, "gns")
+       gq := crypto.Hash(pd.Bytes()).Bits
+       return &GNSQuery{
+               GenericQuery: *NewGenericQuery(gq),
+               Zone:         zkey,
+               Label:        label,
+               derived:      pd,
+       }
+}
+
+//----------------------------------------------------------------------
+// GNS blocks
+//----------------------------------------------------------------------
+
+// SignedGNSBlockData represents the signed content of a GNS block
+type SignedGNSBlockData struct {
+       Purpose *crypto.SignaturePurpose ``         // Size and purpose of 
signature (8 bytes)
+       Expire  util.AbsoluteTime        ``         // Expiration time of the 
block.
+       Data    []byte                   `size:"*"` // Block data content
+}
+
+// GNSBlock is the result of GNS lookups for a given label in a zone.
+// An encrypted and signed container for GNS resource records that represents
+// the "atomic" data structure associated with a GNS label in a given zone.
+type GNSBlock struct {
+       GenericBlock
+
+       // persistent
+       DerivedKeySig *crypto.ZoneSignature // Derived key used for signing
+       Body          *SignedGNSBlockData
+
+       // transient data (not serialized)
+       checked   bool   // block integrity checked
+       verified  bool   // block signature verified (internal)
+       decrypted bool   // block decrypted (internal)
+       data      []byte // decrypted data
+}
+
+// Data block interface implementation
+func (b *GNSBlock) Data() []byte {
+       buf, _ := data.Marshal(b)
+       return buf
+}
+
+// String returns the human-readable representation of a GNSBlock
+func (b *GNSBlock) String() string {
+       return fmt.Sprintf("GNSBlock{Verified=%v,Decrypted=%v,data=[%d]}",
+               b.verified, b.decrypted, len(b.Body.Data))
+}
+
+// NewBlock instantiates an empty GNS block
+func NewBlock() *GNSBlock {
+       return &GNSBlock{
+               DerivedKeySig: nil,
+               Body: &SignedGNSBlockData{
+                       Purpose: new(crypto.SignaturePurpose),
+                       Expire:  *new(util.AbsoluteTime),
+                       Data:    nil,
+               },
+               checked:   false,
+               verified:  false,
+               decrypted: false,
+               data:      nil,
+       }
+}
+
+// Verify the integrity of the block data from a signature.
+// Only the cryptographic signature is verified; the formal correctness of
+// the association between the block and a GNS label in a GNS zone can't
+// be verified. This is only possible in Query.Verify().
+func (b *GNSBlock) Verify() (err error) {
+       // verify signature
+       var buf []byte
+       if buf, err = data.Marshal(b.Body); err != nil {
+               return
+       }
+       _, err = b.DerivedKeySig.Verify(buf)
+       return
+}
diff --git a/src/gnunet/service/dht/blocks/hello.go 
b/src/gnunet/service/dht/blocks/hello.go
new file mode 100644
index 0000000..77fc2ae
--- /dev/null
+++ b/src/gnunet/service/dht/blocks/hello.go
@@ -0,0 +1,226 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package blocks
+
+import (
+       "bytes"
+       "encoding/binary"
+       "fmt"
+       "gnunet/util"
+       "net/url"
+       "strconv"
+       "strings"
+
+       "github.com/bfix/gospel/crypto/ed25519"
+       "github.com/bfix/gospel/data"
+)
+
+//----------------------------------------------------------------------
+// HELLO URLs are used for bootstrapping a node and for adding nodes
+// outside of GNUnet message exchange (e.g. command-line tools)
+//----------------------------------------------------------------------
+
+const helloPrefix = "gnunet://hello/"
+
+// HelloBlock is the DHT-managed block type for HELLO information.
+// It is used to create and parse HELLO URLs.
+// All addresses expire at the same time /this different from HELLO
+// messages (see message.HeeloMsg).
+type HelloBlock struct {
+       PeerID    *util.PeerID         ``         // peer identifier
+       Signature *ed25519.EdSignature ``         // signature
+       Expire    util.AbsoluteTime    ``         // Expiration date
+       AddrBin   []byte               `size:"*"` // raw address data
+
+       // transient attributes
+       addrs []*util.Address // cooked address data
+}
+
+// SetAddresses adds a bulk of addresses for this HELLO block.
+func (h *HelloBlock) SetAddresses(a []*util.Address) {
+       h.addrs = util.Clone(a)
+       h.finalize()
+}
+
+// Addresses returns the list of addresses
+func (h *HelloBlock) Addresses() []*util.Address {
+       return util.Clone(h.addrs)
+}
+
+// ParseHelloURL parses a HELLO URL of the following form:
+//     gnunet://hello/<PeerID>/<signature>/<expire>?<addrs>
+// The addresses are encoded.
+func ParseHelloURL(u string) (h *HelloBlock, err error) {
+       // check and trim prefix
+       if !strings.HasPrefix(u, helloPrefix) {
+               err = fmt.Errorf("invalid HELLO-URL prefix: '%s'", u)
+               return
+       }
+       u = u[len(helloPrefix):]
+
+       // split remainder into parts
+       p := strings.Split(u, "/")
+       if len(p) != 3 {
+               err = fmt.Errorf("invalid HELLO-URL: '%s'", u)
+               return
+       }
+
+       // assemble HELLO data
+       h = new(HelloBlock)
+
+       // (1) parse peer public key (peer ID)
+       var buf []byte
+       if buf, err = util.DecodeStringToBinary(p[0], 32); err != nil {
+               return
+       }
+       h.PeerID = util.NewPeerID(buf)
+
+       // (2) parse signature
+       if buf, err = util.DecodeStringToBinary(p[1], 64); err != nil {
+               return
+       }
+       if h.Signature, err = ed25519.NewEdSignatureFromBytes(buf); err != nil {
+               return
+       }
+
+       // (3) split last element into parts
+       q := strings.SplitN(p[2], "?", 2)
+
+       // (4) parse expiration date
+       var exp uint64
+       if exp, err = strconv.ParseUint(q[0], 10, 64); err != nil {
+               return
+       }
+       h.Expire = util.NewAbsoluteTimeEpoch(exp)
+
+       // (5) process addresses.
+       h.addrs = make([]*util.Address, 0)
+       var ua string
+       for _, a := range strings.Split(q[1], "&") {
+               // unescape URL query
+               if ua, err = url.QueryUnescape(a); err != nil {
+                       return
+               }
+               // parse address and append it to list
+               var addr *util.Address
+               if addr, err = util.ParseAddress(ua); err != nil {
+                       return
+               }
+               h.addrs = append(h.addrs, addr)
+       }
+
+       // (6) generate raw address data so block is complete
+       h.finalize()
+       return
+}
+
+// ParseHelloFromBytes converts a byte array into a HelloBlock instance.
+func ParseHelloFromBytes(buf []byte) (h *HelloBlock, err error) {
+       h = new(HelloBlock)
+       if err = data.Unmarshal(h, buf); err == nil {
+               err = h.finalize()
+       }
+       return
+}
+
+// finalize block data (generate dependent fields)
+func (h *HelloBlock) finalize() (err error) {
+       if h.addrs == nil {
+               err = data.Unmarshal(h.addrs, h.AddrBin)
+       } else if h.AddrBin == nil {
+               wrt := new(bytes.Buffer)
+               for _, a := range h.addrs {
+                       wrt.WriteString(a.String())
+                       wrt.WriteByte(0)
+               }
+               h.AddrBin = wrt.Bytes()
+       }
+       return
+}
+
+/*
+// Message returns the corresponding HELLO message to be sent to peers.
+func (h *HelloBlock) Message() *message.HelloMsg {
+       msg := message.NewHelloMsg(h.PeerID)
+       for _, a := range h.addrs {
+               msg.AddAddress(message.NewHelloAddress(a, h.Expire))
+       }
+       return msg
+}
+*/
+
+// URL returns the HELLO URL for the data.
+func (h *HelloBlock) URL() string {
+       u := fmt.Sprintf("%s%s/%s/%d?",
+               helloPrefix,
+               h.PeerID.String(),
+               util.EncodeBinaryToString(h.Signature.Bytes()),
+               h.Expire.Epoch(),
+       )
+       for i, a := range h.addrs {
+               if i > 0 {
+                       u += "&"
+               }
+               u += url.QueryEscape(a.String())
+       }
+       return u
+}
+
+// Equals returns true if two HELLOs are the same. The expiration
+// timestamp is ignored in the comparision.
+func (h *HelloBlock) Equals(g *HelloBlock) bool {
+       if !h.PeerID.Equals(g.PeerID) ||
+               !util.Equals(h.Signature.Bytes(), g.Signature.Bytes()) ||
+               len(h.addrs) != len(g.addrs) {
+               return false
+       }
+       for i, a := range h.addrs {
+               if !a.Equals(g.addrs[i]) {
+                       return false
+               }
+       }
+       return true
+}
+
+// Verify the integrity of the HELLO data
+func (h *HelloBlock) Verify() (bool, error) {
+       // assemble signed data and public key
+       sd := h.signedData()
+       pub := ed25519.NewPublicKeyFromBytes(h.PeerID.Key)
+       return pub.EdVerify(sd, h.Signature)
+}
+
+// Sign the HELLO data with private key
+func (h *HelloBlock) Sign(prv *ed25519.PrivateKey) (err error) {
+       // assemble signed data
+       sd := h.signedData()
+       h.Signature, err = prv.EdSign(sd)
+       return
+}
+
+// signedData assembles a data block for sign and verify operations.
+func (h *HelloBlock) signedData() []byte {
+       buf := new(bytes.Buffer)
+       buf.Write(h.PeerID.Key)
+       binary.Write(buf, binary.BigEndian, h.Expire)
+       for _, a := range h.addrs {
+               buf.Write(a.Address)
+       }
+       return buf.Bytes()
+}
diff --git a/src/gnunet/util/fs.go b/src/gnunet/service/dht/blocks/hello_test.go
similarity index 60%
copy from src/gnunet/util/fs.go
copy to src/gnunet/service/dht/blocks/hello_test.go
index 009ef62..089259a 100644
--- a/src/gnunet/util/fs.go
+++ b/src/gnunet/service/dht/blocks/hello_test.go
@@ -1,5 +1,5 @@
 // This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
 //
 // gnunet-go is free software: you can redistribute it and/or modify it
 // under the terms of the GNU Affero General Public License as published
@@ -16,28 +16,29 @@
 //
 // SPDX-License-Identifier: AGPL3.0-or-later
 
-package util
+package blocks
 
-import (
-       "fmt"
-       "os"
+import "testing"
 
-       "github.com/bfix/gospel/logger"
+const (
+       helloURL = "gnunet://hello" +
+               "/7KTBJ90340HF1Q2GB0A57E2XJER4FDHX8HP5GHEB9125VPWPD27G" +
+
+               "/BNMDFN6HJCPWSPNBSEC06MC1K8QN1Z2DHRQSRXDTFR7FTBD4JHN" +
+               "BJ2RJAAEZ31FWG1Q3PMN3PXGZQ3Q7NTNEKQZFA7TE2Y46FM8E20R" +
+               "/1653499308" +
+               "?r5n%2Bip%2Budp%3A1.2.3.4%3A6789" +
+               "&gnunet%2Btcp%3A12.3.4.5"
 )
 
-// EnforceDirExists make sure that the path
-func EnforceDirExists(path string) error {
-       logger.Printf(logger.DBG, "[util] Checking directory '%s'...\n", path)
-       fi, err := os.Lstat(path)
+func TestHelloURL(t *testing.T) {
+
+       hd, err := ParseHelloURL(helloURL)
        if err != nil {
-               if os.IsNotExist(err) {
-                       logger.Printf(logger.DBG, "[util] Creating directory 
'%s'...\n", path)
-                       return os.Mkdir(path, 0770)
-               }
-               return err
+               t.Fatal(err)
        }
-       if !fi.IsDir() {
-               return fmt.Errorf("Not a directory (%s)", path)
+       u := hd.URL()
+       if u != helloURL {
+               t.Fatal("urls don't match")
        }
-       return nil
 }
diff --git a/src/gnunet/transport/session.go 
b/src/gnunet/service/dht/blocks/types.go
similarity index 63%
rename from src/gnunet/transport/session.go
rename to src/gnunet/service/dht/blocks/types.go
index f5a0787..04edb6e 100644
--- a/src/gnunet/transport/session.go
+++ b/src/gnunet/service/dht/blocks/types.go
@@ -1,5 +1,5 @@
 // This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
 //
 // gnunet-go is free software: you can redistribute it and/or modify it
 // under the terms of the GNU Affero General Public License as published
@@ -16,14 +16,11 @@
 //
 // SPDX-License-Identifier: AGPL3.0-or-later
 
-package transport
+package blocks
 
-// Session states
+// DHT Block types
 const (
-       KxStateDown        = iota // No handshake yet.
-       KxStateKeySent            // We've sent our session key.
-       KxStateKeyReceived        // We've received the other peers session key.
-       KxStateUp                 // Key exchange is done.
-       KxStateRekeySent          // We're rekeying (or had a timeout).
-       KxPeerDisconnect          // Last state of a KX (when it is being 
terminated).
+       DHT_BLOCK_ANY   = 0
+       DHT_BLOCK_HELLO = 7  // Type of a block that contains a HELLO for a peer
+       DHT_BLOCK_GNS   = 11 // Block for storing record data
 )
diff --git a/src/gnunet/service/dht/bloomfilter.go 
b/src/gnunet/service/dht/bloomfilter.go
new file mode 100644
index 0000000..dcfd935
--- /dev/null
+++ b/src/gnunet/service/dht/bloomfilter.go
@@ -0,0 +1,123 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package dht
+
+import (
+       "bytes"
+       "crypto/sha512"
+       "encoding/binary"
+)
+
+//======================================================================
+// Generic BloomFilter
+//======================================================================
+
+// BloomFilter parameter
+var (
+       bfNumBits = 128
+       bfHash    = sha512.New
+)
+
+// BloomFilter is a space-efficient probabilistic datastructure to test if
+// an element is part of a set of elementsis defined as a string of bits
+// always initially empty.
+type BloomFilter struct {
+       data []byte // filter bits
+       salt []byte // salt for hashing
+}
+
+// NewBloomFilter cretes a new filter using the specified salt. An unused
+// salt is set to nil.
+func NewBloomFilter(salt []byte) *BloomFilter {
+       return &BloomFilter{
+               data: make([]byte, (bfNumBits+7)/8),
+               salt: salt,
+       }
+}
+
+// Add entry (binary representation):
+// When adding an element to the Bloom filter bf using BF-SET(bf,e), each
+// integer n of the mapping M(e) is interpreted as a bit offset n mod L
+// within bf and set to 1.
+func (bf *BloomFilter) Add(e []byte) {
+       for _, idx := range bf.indices(e) {
+               bf.data[idx/8] |= (1 << (idx % 7))
+       }
+}
+
+// Contains returns true if the entry is most likely to be included:
+// When testing if an element may be in the Bloom filter bf using
+// BF-TEST(bf,e), each bit offset n mod L within bf MUST have been set to 1.
+// Otherwise, the element is not considered to be in the Bloom filter.
+func (bf *BloomFilter) Contains(e []byte) bool {
+       for _, idx := range bf.indices(e) {
+               if bf.data[idx/8]&(1<<(idx%7)) == 0 {
+                       return false
+               }
+       }
+       return true
+}
+
+// indices returns the list of bit indices for antry e:
+// The element e is prepended with a salt (pütional) and hashed using SHA-512.
+// The resulting byte string is interpreted as a list of 16 32-bit integers
+// in network byte order.
+func (bf *BloomFilter) indices(e []byte) []int {
+       // hash the entry (with optional salt prepended)
+       hsh := bfHash()
+       if bf.salt != nil {
+               hsh.Write(bf.salt)
+       }
+       hsh.Write(e)
+       h := hsh.Sum(nil)
+
+       // compute the indices for the entry
+       idx := make([]int, len(h)/2)
+       buf := bytes.NewReader(h)
+       for i := range idx {
+               binary.Read(buf, binary.BigEndian, &idx[i])
+       }
+       return idx
+}
+
+//======================================================================
+// BloomFilter for peer addresses
+//======================================================================
+
+// PeerBloomFilter implements specific Add/Contains functions.
+type PeerBloomFilter struct {
+       BloomFilter
+}
+
+// NewPeerBloomFilter creates a new filter for peer addresses.
+func NewPeerBloomFilter() *PeerBloomFilter {
+       return &PeerBloomFilter{
+               BloomFilter: *NewBloomFilter(nil),
+       }
+}
+
+// Add peer address to the filter.
+func (bf *PeerBloomFilter) Add(p *PeerAddress) {
+       bf.BloomFilter.Add(p.addr[:])
+}
+
+// Contains returns true if the peer address is most likely to be included.
+func (bf *PeerBloomFilter) Contains(p *PeerAddress) bool {
+       return bf.BloomFilter.Contains(p.addr[:])
+}
diff --git a/src/gnunet/service/dht/dhtstore_test.go 
b/src/gnunet/service/dht/dhtstore_test.go
new file mode 100644
index 0000000..3cb8080
--- /dev/null
+++ b/src/gnunet/service/dht/dhtstore_test.go
@@ -0,0 +1,89 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package dht
+
+import (
+       "encoding/hex"
+       "gnunet/crypto"
+       "gnunet/service"
+       blocks "gnunet/service/dht/blocks"
+       "math/rand"
+       "testing"
+)
+
+// test constants
+const (
+       fsNumBlocks = 5
+)
+
+// TestDHTFileStore generates 'fsNumBlocks' fully-random blocks
+// and stores them under their SHA512 key. It than retrieves
+// each block from storage and checks for matching hash.
+func TestDHTFilesStore(t *testing.T) {
+
+       // create file store
+       fs, err := service.NewFileCache("/var/lib/gnunet/dht/cache", "100")
+       if err != nil {
+               t.Fatal(err)
+       }
+       // allocate keys
+       keys := make([]blocks.Query, 0, fsNumBlocks)
+
+       // First round: save blocks
+       for i := 0; i < fsNumBlocks; i++ {
+               // generate random block
+               size := 20 // 1024 + rand.Intn(62000)
+               buf := make([]byte, size)
+               rand.Read(buf)
+               val := blocks.NewGenericBlock(buf)
+               // generate associated key
+               k := crypto.Hash(buf).Bits
+               key := blocks.NewGenericQuery(k)
+               t.Logf("> %d: %s -- %s", i, hex.EncodeToString(k), 
hex.EncodeToString(buf))
+
+               // store block
+               if err := fs.Put(key, val); err != nil {
+                       t.Fatal(err)
+               }
+
+               // remember key
+               keys = append(keys, key)
+       }
+
+       // Second round: retrieve blocks and check
+       for i, key := range keys {
+               // get block
+               val, err := fs.Get(key)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               buf := val.Data()
+               t.Logf("< %d: %s -- %s", i, hex.EncodeToString(key.Key().Bits), 
hex.EncodeToString(buf))
+
+               // re-create key
+               k := crypto.Hash(buf)
+
+               // do the keys match?
+               if !k.Equals(key.Key()) {
+                       t.Log(hex.EncodeToString(k.Bits))
+                       t.Log(hex.EncodeToString(key.Key().Bits))
+                       t.Fatal("key/value mismatch")
+               }
+       }
+}
diff --git a/src/gnunet/service/dht/module.go b/src/gnunet/service/dht/module.go
index 1588a06..d369f3f 100644
--- a/src/gnunet/service/dht/module.go
+++ b/src/gnunet/service/dht/module.go
@@ -19,9 +19,13 @@
 package dht
 
 import (
+       "context"
+       "gnunet/config"
+       "gnunet/core"
        "gnunet/message"
        "gnunet/service"
-       "gnunet/service/gns"
+       "gnunet/service/dht/blocks"
+       "net/http"
 )
 
 //======================================================================
@@ -32,16 +36,99 @@ import (
 // Put and get blocks into/from a DHT.
 //----------------------------------------------------------------------
 
-// Module handles the permanent storage of blocks under the query key.
+// Module handles the permanent storage of blocks under a query key.
 type Module struct {
+       service.ModuleImpl
+
+       store service.DHTStore // reference to the block storage mechanism
+       cache service.DHTStore // transient block cache
+       core  *core.Core       // reference to core services
+
+       rtable *RoutingTable // routing table
+}
+
+// NewModule returns a new module instance. It initializes the storage
+// mechanism for persistence.
+func NewModule(ctx context.Context, c *core.Core) (m *Module) {
+       // create permanent storage handler
+       store, err := service.NewDHTStore(config.Cfg.DHT.Storage)
+       if err != nil {
+               return nil
+       }
+       // create cache handler
+       cache, err := service.NewDHTStore(config.Cfg.DHT.Cache)
+       if err != nil {
+               return nil
+       }
+       // create routing table
+       rt := NewRoutingTable(NewPeerAddress(c.PeerID()))
+
+       // return module instance
+       m = &Module{
+               ModuleImpl: *service.NewModuleImpl(),
+               store:      store,
+               cache:      cache,
+               core:       c,
+               rtable:     rt,
+       }
+       // register as listener for core events
+       listener := m.Run(ctx, m.event, m.Filter())
+       c.Register("dht", listener)
+
+       return
 }
 
-// Get a GNS block from the DHT
-func (nc *Module) Get(ctx *service.SessionContext, query *gns.Query) 
(*message.Block, error) {
+//----------------------------------------------------------------------
+
+// Get a block from the DHT
+func (nc *Module) Get(ctx context.Context, query blocks.Query) (block 
blocks.Block, err error) {
+
+       // check if we have the requested block in cache or permanent storage.
+       block, err = nc.cache.Get(query)
+       if err == nil {
+               // yes: we are done
+               return
+       }
+       block, err = nc.store.Get(query)
+       if err == nil {
+               // yes: we are done
+               return
+       }
+       // retrieve the block from the DHT
+
        return nil, nil
 }
 
-// Put a GNS block into the DHT
-func (nc *Module) Put(ctx *service.SessionContext, block *message.Block) error 
{
+// Put a block into the DHT
+func (nc *Module) Put(ctx context.Context, key blocks.Query, block 
blocks.Block) error {
        return nil
 }
+
+//----------------------------------------------------------------------
+
+// Filter returns the event filter for the module
+func (m *Module) Filter() *core.EventFilter {
+       f := core.NewEventFilter()
+       f.AddEvent(core.EV_CONNECT)
+       f.AddEvent(core.EV_DISCONNECT)
+       f.AddMsgType(message.DHT_CLIENT_GET)
+       f.AddMsgType(message.DHT_CLIENT_GET_RESULTS_KNOWN)
+       f.AddMsgType(message.DHT_CLIENT_GET_STOP)
+       f.AddMsgType(message.DHT_CLIENT_PUT)
+       f.AddMsgType(message.DHT_CLIENT_RESULT)
+       return f
+}
+
+// Event handler
+func (nc *Module) event(ctx context.Context, ev *core.Event) {
+
+}
+
+//----------------------------------------------------------------------
+
+// RPC returns the route and handler function for a JSON-RPC request
+func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) {
+       return "/gns/", func(wrt http.ResponseWriter, req *http.Request) {
+               wrt.Write([]byte(`{"msg": "This is DHT" }`))
+       }
+}
diff --git a/src/gnunet/service/dht/routingtable.go 
b/src/gnunet/service/dht/routingtable.go
new file mode 100644
index 0000000..895a1b2
--- /dev/null
+++ b/src/gnunet/service/dht/routingtable.go
@@ -0,0 +1,305 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package dht
+
+import (
+       "bytes"
+       "crypto/sha512"
+       "encoding/hex"
+       "gnunet/util"
+       "math/rand"
+       "sync"
+
+       "github.com/bfix/gospel/math"
+)
+
+var (
+       // routing table hash function: defines number of
+       // buckets and size of peer addresses
+       rtHash = sha512.New
+)
+
+// Routing table contants (adjust with changing hash function)
+const (
+       numBuckets = 512 // number of bits of hash function result
+       numK       = 20  // number of entries per k-bucket
+       sizeAddr   = 64  // size of peer address in bytes
+)
+
+//======================================================================
+//======================================================================
+
+// PeerAddress is the identifier for a peer in the DHT network.
+// It is the SHA-512 hash of the PeerID (public Ed25519 key).
+type PeerAddress struct {
+       addr [sizeAddr]byte
+}
+
+// NewPeerAddress returns the DHT address of a peer.
+func NewPeerAddress(peer *util.PeerID) *PeerAddress {
+       r := new(PeerAddress)
+       h := rtHash()
+       h.Write(peer.Key)
+       copy(r.addr[:], h.Sum(nil))
+       return r
+}
+
+// String returns a human-readble representation of an address.
+func (addr *PeerAddress) String() string {
+       return hex.EncodeToString(addr.addr[:])
+}
+
+// Equals returns true if two peer addresses are the same.
+func (addr *PeerAddress) Equals(p *PeerAddress) bool {
+       return bytes.Equal(addr.addr[:], p.addr[:])
+}
+
+// Distance between two addresses: returns a distance value and a
+// bucket index (smaller index = less distant).
+func (addr *PeerAddress) Distance(p *PeerAddress) (*math.Int, int) {
+       var d PeerAddress
+       for i := range d.addr {
+               d.addr[i] = addr.addr[i] ^ p.addr[i]
+       }
+       r := math.NewIntFromBytes(d.addr[:])
+       return r, numBuckets - r.BitLen()
+}
+
+//======================================================================
+// Routing table implementation
+//======================================================================
+
+// RoutingTable holds the (local) routing table for a node.
+// The index of of an address is the number of bits in the
+// distance to the reference address, so smaller index means
+// "nearer" to the reference address.
+type RoutingTable struct {
+       ref     *PeerAddress          // reference address for distance
+       buckets []*Bucket             // list of buckets
+       list    map[*PeerAddress]bool // keep list of peers
+       rwlock  sync.RWMutex          // lock for write operations
+       l2nse   float64               // log2 of estimated network size
+}
+
+// NewRoutingTable creates a new routing table for the reference address.
+func NewRoutingTable(ref *PeerAddress) *RoutingTable {
+       rt := new(RoutingTable)
+       rt.ref = ref
+       rt.list = make(map[*PeerAddress]bool)
+       rt.buckets = make([]*Bucket, numBuckets)
+       for i := range rt.buckets {
+               rt.buckets[i] = NewBucket(numK)
+       }
+       return rt
+}
+
+// Add new peer address to routing table.
+// Returns true if the entry was added, false otherwise.
+func (rt *RoutingTable) Add(p *PeerAddress, connected bool) bool {
+       // ensure one write and no readers
+       rt.rwlock.Lock()
+       defer rt.rwlock.Unlock()
+
+       // compute distance (bucket index) and insert address.
+       _, idx := p.Distance(rt.ref)
+       if rt.buckets[idx].Add(p, connected) {
+               rt.list[p] = true
+               return true
+       }
+       // Full bucket: we did not add the address to the routing table.
+       return false
+}
+
+// Remove peer address from routing table.
+// Returns true if the entry was removed, false otherwise.
+func (rt *RoutingTable) Remove(p *PeerAddress) bool {
+       // ensure one write and no readers
+       rt.rwlock.Lock()
+       defer rt.rwlock.Unlock()
+
+       // compute distance (bucket index) and remove entry from bucket
+       _, idx := p.Distance(rt.ref)
+       if rt.buckets[idx].Remove(p) {
+               delete(rt.list, p)
+               return true
+       }
+       return false
+}
+
+//----------------------------------------------------------------------
+// routing functions
+//----------------------------------------------------------------------
+
+// SelectClosestPeer for a given peer address and bloomfilter.
+func (rt *RoutingTable) SelectClosestPeer(p *PeerAddress, bf *PeerBloomFilter) 
(n *PeerAddress) {
+       // no writer allowed
+       rt.rwlock.RLock()
+       defer rt.rwlock.RUnlock()
+
+       // find closest address
+       var dist *math.Int
+       for _, b := range rt.buckets {
+               if k, d := b.SelectClosestPeer(p, bf); n == nil || (d != nil && 
d.Cmp(dist) < 0) {
+                       dist = d
+                       n = k
+               }
+       }
+       return
+}
+
+// SelectRandomPeer returns a random address from table (that is not
+// included in the bloomfilter)
+func (rt *RoutingTable) SelectRandomPeer(bf *PeerBloomFilter) *PeerAddress {
+       // no writer allowed
+       rt.rwlock.RLock()
+       defer rt.rwlock.RUnlock()
+
+       // select random entry from list
+       if size := len(rt.list); size > 0 {
+               idx := rand.Intn(size)
+               for k := range rt.list {
+                       if idx == 0 {
+                               return k
+                       }
+                       idx--
+               }
+       }
+       return nil
+}
+
+// SelectPeer selects a neighbor depending on the number of hops parameter.
+// If hops < NSE this function MUST return SelectRandomPeer() and
+// SelectClosestpeer() otherwise.
+func (rt *RoutingTable) SelectPeer(p *PeerAddress, hops int, bf 
*PeerBloomFilter) *PeerAddress {
+       if float64(hops) < rt.l2nse {
+               return rt.SelectRandomPeer(bf)
+       }
+       return rt.SelectClosestPeer(p, bf)
+}
+
+// IsClosestPeer returns true if p is the closest peer for k. Peers with a
+// positive test in the Bloom filter  are not considered.
+func (rt *RoutingTable) IsClosestPeer(p, k *PeerAddress, bf *PeerBloomFilter) 
bool {
+       n := rt.SelectClosestPeer(k, bf)
+       return n.Equals(p)
+}
+
+// ComputeOutDegree computes the number of neighbors that a message should be 
forwarded to.
+// The arguments are the desired replication level, the hop count of the 
message so far,
+// and the base-2 logarithm of the current network size estimate (L2NSE) as 
provided by the
+// underlay. The result is the non-negative number of next hops to select.
+func (rt *RoutingTable) ComputeOutDegree(repl, hop int) int {
+       hf := float64(hop)
+       if hf > 4*rt.l2nse {
+               return 0
+       }
+       if hf > 2*rt.l2nse {
+               return 1
+       }
+       if repl == 0 {
+               repl = 1
+       } else if repl > 16 {
+               repl = 16
+       }
+       rm1 := float64(repl - 1)
+       return 1 + int(rm1/(rt.l2nse+rm1*hf))
+}
+
+//======================================================================
+// Routing table buckets
+//======================================================================
+
+// PeerEntry in a k-Bucket: use routing specific attributes
+// for book-keeping
+type PeerEntry struct {
+       addr      *PeerAddress // peer address
+       connected bool         // is peer connected?
+}
+
+// Bucket holds peer entries with approx. same distance from node
+type Bucket struct {
+       list   []*PeerEntry
+       rwlock sync.RWMutex
+}
+
+// NewBucket creates a new entry list of given size
+func NewBucket(n int) *Bucket {
+       return &Bucket{
+               list: make([]*PeerEntry, 0, n),
+       }
+}
+
+// Add peer address to the bucket if there is free space.
+// Returns true if entry is added, false otherwise.
+func (b *Bucket) Add(p *PeerAddress, connected bool) bool {
+       // only one writer and no readers
+       b.rwlock.Lock()
+       defer b.rwlock.Unlock()
+
+       // check for free space in bucket
+       if len(b.list) < numK {
+               // append entry at the end
+               pe := &PeerEntry{
+                       addr:      p,
+                       connected: connected,
+               }
+               b.list = append(b.list, pe)
+               return true
+       }
+       return false
+}
+
+// Remove peer address from the bucket.
+// Returns true if entry is removed (found), false otherwise.
+func (b *Bucket) Remove(p *PeerAddress) bool {
+       // only one writer and no readers
+       b.rwlock.Lock()
+       defer b.rwlock.Unlock()
+
+       for i, pe := range b.list {
+               if pe.addr.Equals(p) {
+                       // found entry: remove it
+                       b.list = append(b.list[:i], b.list[i+1:]...)
+                       return true
+               }
+       }
+       return false
+}
+
+// SelectClosestPeer returns the entry with minimal distance to the given
+// peer address; entries included in the bloom flter are ignored.
+func (b *Bucket) SelectClosestPeer(p *PeerAddress, bf *PeerBloomFilter) (n 
*PeerAddress, dist *math.Int) {
+       // no writer allowed
+       b.rwlock.RLock()
+       defer b.rwlock.RUnlock()
+
+       for _, pe := range b.list {
+               // skip addresses in bloomfilter
+               if bf.Contains(pe.addr) {
+                       continue
+               }
+               // check for shorter distance
+               if d, _ := p.Distance(pe.addr); n == nil || d.Cmp(dist) < 0 {
+                       // remember best match
+                       dist = d
+                       n = pe.addr
+               }
+       }
+       return
+}
diff --git a/src/gnunet/service/dht/routingtable_test.go 
b/src/gnunet/service/dht/routingtable_test.go
new file mode 100644
index 0000000..2579356
--- /dev/null
+++ b/src/gnunet/service/dht/routingtable_test.go
@@ -0,0 +1,140 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package dht
+
+import (
+       "gnunet/config"
+       "gnunet/core"
+       "gnunet/util"
+       "math/rand"
+       "testing"
+)
+
+const (
+       NUMP   = 1000  // Total number of peers
+       EPOCHS = 10000 // number of epochs to run
+)
+
+type Entry struct {
+       addr   *PeerAddress // address of peer
+       ttl    int64        // time to live (in epochs)
+       born   int64        // epoch of birth
+       last   int64        // last action
+       drop   int64        // drop (in epochs)
+       revive int64        // revive dropped (in epochs)
+       online bool         // peer connected?
+}
+
+// test data
+var (
+       cfg = &config.NodeConfig{
+               PrivateSeed: "YGoe6XFH3XdvFRl+agx9gIzPTvxA229WFdkazEMdcOs=",
+               Endpoints: []string{
+                       "r5n+ip+udp://127.0.0.1:6666",
+               },
+       }
+)
+
+// TestRT connects and disconnects random peers to test the base
+// functionality of the routing table algorithms.
+func TestRT(t *testing.T) {
+       // start deterministic randomizer
+       rand.Seed(19031962)
+
+       // helper functions
+       genRemotePeer := func() *PeerAddress {
+               d := make([]byte, 32)
+               if _, err := rand.Read(d); err != nil {
+                       panic(err)
+               }
+               return NewPeerAddress(util.NewPeerID(d))
+       }
+
+       // create routing table and start command handler
+       local, err := core.NewLocalPeer(cfg)
+       if err != nil {
+               t.Fatal(err)
+       }
+       rt := NewRoutingTable(NewPeerAddress(local.GetID()))
+
+       // create a task list
+       tasks := make([]*Entry, NUMP)
+       for i := range tasks {
+               tasks[i] = new(Entry)
+               tasks[i].addr = genRemotePeer()
+               tasks[i].born = rand.Int63n(EPOCHS)
+               tasks[i].ttl = 1000 + rand.Int63n(7000)
+               tasks[i].drop = 2000 + rand.Int63n(3000)
+               tasks[i].revive = rand.Int63n(2000)
+               tasks[i].online = false
+       }
+
+       // actions:
+       connected := func(task *Entry, e int64, msg string) {
+               rt.Add(task.addr, true)
+               task.online = true
+               task.last = e
+               t.Logf("[%6d] %s %s\n", e, task.addr, msg)
+       }
+       disconnected := func(task *Entry, e int64, msg string) {
+               rt.Remove(task.addr)
+               task.online = false
+               task.last = e
+               t.Logf("[%6d] %s %s\n", e, task.addr, msg)
+       }
+
+       // run epochs
+       var e int64
+       for e = 0; e < EPOCHS; e++ {
+               for _, task := range tasks {
+                       // birth
+                       if task.born == e {
+                               connected(task, e, "connected")
+                               continue
+                       }
+                       // death
+                       if task.born+task.ttl == e {
+                               disconnected(task, e, "disconnected")
+                               continue
+                       }
+                       if task.online {
+                               // drop out
+                               if task.last+task.drop == e {
+                                       disconnected(task, e, "dropped out")
+                                       continue
+                               }
+                       } else {
+                               // drop in
+                               if task.last+task.drop == e {
+                                       connected(task, e, "dropped in")
+                                       continue
+                               }
+                       }
+               }
+       }
+
+       // execute some routing functions on remaining table
+       k := genRemotePeer()
+       bf := NewPeerBloomFilter()
+       n := rt.SelectClosestPeer(k, bf)
+       t.Logf("Closest: %s -> %s\n", k, n)
+
+       n = rt.SelectRandomPeer(bf)
+       t.Logf("Random: %s\n", n)
+}
diff --git a/src/gnunet/service/dht/service.go 
b/src/gnunet/service/dht/service.go
new file mode 100644
index 0000000..2a189bb
--- /dev/null
+++ b/src/gnunet/service/dht/service.go
@@ -0,0 +1,134 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package dht
+
+import (
+       "context"
+       "fmt"
+       "io"
+
+       "gnunet/core"
+       "gnunet/message"
+       "gnunet/service"
+
+       "github.com/bfix/gospel/logger"
+)
+
+// Error codes
+var (
+       ErrInvalidID           = fmt.Errorf("invalid/unassociated ID")
+       ErrBlockExpired        = fmt.Errorf("block expired")
+       ErrInvalidResponseType = fmt.Errorf("invald response type")
+)
+
+//----------------------------------------------------------------------
+// "GNUnet R5N DHT" service implementation
+//----------------------------------------------------------------------
+
+// Service implements a DHT service
+type Service struct {
+       Module
+}
+
+// NewService creates a new DHT service instance
+func NewService(ctx context.Context, c *core.Core) service.Service {
+       return &Service{
+               Module: *NewModule(ctx, c),
+       }
+}
+
+// ServeClient processes a client channel.
+func (s *Service) ServeClient(ctx context.Context, id int, mc 
*service.Connection) {
+       reqID := 0
+       var cancel context.CancelFunc
+       ctx, cancel = context.WithCancel(ctx)
+
+loop:
+       for {
+               // receive next message from client
+               reqID++
+               logger.Printf(logger.DBG, "[dht:%d:%d] Waiting for client 
request...\n", id, reqID)
+               msg, err := mc.Receive(ctx)
+               if err != nil {
+                       if err == io.EOF {
+                               logger.Printf(logger.INFO, "[dht:%d:%d] Client 
channel closed.\n", id, reqID)
+                       } else if err == service.ErrConnectionInterrupted {
+                               logger.Printf(logger.INFO, "[dht:%d:%d] Service 
operation interrupted.\n", id, reqID)
+                       } else {
+                               logger.Printf(logger.ERROR, "[dht:%d:%d] 
Message-receive failed: %s\n", id, reqID, err.Error())
+                       }
+                       break loop
+               }
+               logger.Printf(logger.INFO, "[dht:%d:%d] Received request: 
%v\n", id, reqID, msg)
+
+               // handle message
+               s.HandleMessage(context.WithValue(ctx, "label", 
fmt.Sprintf(":%d:%d", id, reqID)), msg, mc)
+       }
+       // close client connection
+       mc.Close()
+
+       // cancel all tasks running for this session/connection
+       logger.Printf(logger.INFO, "[dht:%d] Start closing session...\n", id)
+       cancel()
+}
+
+// HandleMessage handles a DHT request/response message. If the transport 
channel
+// is nil, responses are send directly via the transport layer.
+func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back 
service.Responder) bool {
+       // assemble log label
+       label := ""
+       if v := ctx.Value("label"); v != nil {
+               label = v.(string)
+       }
+       // process message
+       switch msg.(type) {
+       case *message.DHTClientPutMsg:
+               //----------------------------------------------------------
+               // DHT PUT
+               //----------------------------------------------------------
+
+       case *message.DHTClientGetMsg:
+               //----------------------------------------------------------
+               // DHT GET
+               //----------------------------------------------------------
+
+       case *message.DHTClientGetResultsKnownMsg:
+               //----------------------------------------------------------
+               // DHT GET-RESULTS-KNOWN
+               //----------------------------------------------------------
+
+       case *message.DHTClientGetStopMsg:
+               //----------------------------------------------------------
+               // DHT GET-STOP
+               //----------------------------------------------------------
+
+       case *message.DHTClientResultMsg:
+               //----------------------------------------------------------
+               // DHT RESULT
+               //----------------------------------------------------------
+
+       default:
+               //----------------------------------------------------------
+               // UNKNOWN message type received
+               //----------------------------------------------------------
+               logger.Printf(logger.ERROR, "[dht%s] Unhandled message of type 
(%d)\n", label, msg.Header().MsgType)
+               return false
+       }
+       return true
+}
diff --git a/src/gnunet/service/gns/block_handler.go 
b/src/gnunet/service/gns/block_handler.go
index b05c59e..c93fca1 100644
--- a/src/gnunet/service/gns/block_handler.go
+++ b/src/gnunet/service/gns/block_handler.go
@@ -76,7 +76,7 @@ type BlockHandler interface {
        // resource records in the same block. 'cm' maps the resource type
        // to an integer count (how many records of a type are present in the
        // GNS block).
-       Coexist(cm util.CounterMap) bool
+       Coexist(cm util.Counter[int]) bool
 
        // Records returns a list of RR of the given types associated with
        // the custom handler
@@ -103,7 +103,7 @@ type BlockHandler interface {
 // BlockHandlerList is a list of block handlers instantiated.
 type BlockHandlerList struct {
        list   map[int]BlockHandler // list of handler instances
-       counts util.CounterMap      // count number of RRs by type
+       counts util.Counter[int]    // count number of RRs by type
 }
 
 // NewBlockHandlerList instantiates an a list of active block handlers
@@ -112,7 +112,7 @@ func NewBlockHandlerList(records []*message.ResourceRecord, 
labels []string) (*B
        // initialize block handler list
        hl := &BlockHandlerList{
                list:   make(map[int]BlockHandler),
-               counts: make(util.CounterMap),
+               counts: make(util.Counter[int]),
        }
 
        // first pass: build list of shadow records in this block
@@ -260,7 +260,7 @@ func (h *ZoneKeyHandler) AddRecord(rec 
*message.ResourceRecord, labels []string)
 
 // Coexist return a flag indicating how a resource record of a given type
 // is to be treated (see BlockHandler interface)
-func (h *ZoneKeyHandler) Coexist(cm util.CounterMap) bool {
+func (h *ZoneKeyHandler) Coexist(cm util.Counter[int]) bool {
        // only one type (GNS_TYPE_PKEY) is present
        return len(cm) == 1 && cm.Num(enums.GNS_TYPE_PKEY) == 1
 }
@@ -335,7 +335,7 @@ func (h *Gns2DnsHandler) AddRecord(rec 
*message.ResourceRecord, labels []string)
 
 // Coexist return a flag indicating how a resource record of a given type
 // is to be treated (see BlockHandler interface)
-func (h *Gns2DnsHandler) Coexist(cm util.CounterMap) bool {
+func (h *Gns2DnsHandler) Coexist(cm util.Counter[int]) bool {
        // only one type (GNS_TYPE_GNS2DNS) is present
        return len(cm) == 1 && cm.Num(enums.GNS_TYPE_GNS2DNS) > 0
 }
@@ -405,7 +405,7 @@ func (h *BoxHandler) AddRecord(rec *message.ResourceRecord, 
labels []string) err
 
 // Coexist return a flag indicating how a resource record of a given type
 // is to be treated (see BlockHandler interface)
-func (h *BoxHandler) Coexist(cm util.CounterMap) bool {
+func (h *BoxHandler) Coexist(cm util.Counter[int]) bool {
        // anything goes...
        return true
 }
@@ -469,7 +469,7 @@ func (h *LehoHandler) AddRecord(rec 
*message.ResourceRecord, labels []string) er
 
 // Coexist return a flag indicating how a resource record of a given type
 // is to be treated (see BlockHandler interface)
-func (h *LehoHandler) Coexist(cm util.CounterMap) bool {
+func (h *LehoHandler) Coexist(cm util.Counter[int]) bool {
        // requires exactly one LEHO and any number of other records.
        return cm.Num(enums.GNS_TYPE_LEHO) == 1
 }
@@ -527,7 +527,7 @@ func (h *CnameHandler) AddRecord(rec 
*message.ResourceRecord, labels []string) e
 
 // Coexist return a flag indicating how a resource record of a given type
 // is to be treated (see BlockHandler interface)
-func (h *CnameHandler) Coexist(cm util.CounterMap) bool {
+func (h *CnameHandler) Coexist(cm util.Counter[int]) bool {
        // only a single CNAME allowed
        return len(cm) == 1 && cm.Num(enums.GNS_TYPE_DNS_CNAME) == 1
 }
@@ -581,7 +581,7 @@ func (h *VpnHandler) AddRecord(rec *message.ResourceRecord, 
labels []string) err
 
 // Coexist return a flag indicating how a resource record of a given type
 // is to be treated (see BlockHandler interface)
-func (h *VpnHandler) Coexist(cm util.CounterMap) bool {
+func (h *VpnHandler) Coexist(cm util.Counter[int]) bool {
        // anything goes
        return true
 }
diff --git a/src/gnunet/service/gns/dns.go b/src/gnunet/service/gns/dns.go
index 3bcc35e..8818ca6 100644
--- a/src/gnunet/service/gns/dns.go
+++ b/src/gnunet/service/gns/dns.go
@@ -19,6 +19,7 @@
 package gns
 
 import (
+       "context"
        "fmt"
        "net"
        "strings"
@@ -27,7 +28,6 @@ import (
        "gnunet/crypto"
        "gnunet/enums"
        "gnunet/message"
-       "gnunet/service"
        "gnunet/util"
 
        "github.com/bfix/gospel/logger"
@@ -205,7 +205,7 @@ func QueryDNS(id int, name string, server net.IP, kind 
RRTypeList) *message.Reco
 // parallel; the first result delivered by any of the servers is returned
 // as the result list of matching resource records.
 func (gns *Module) ResolveDNS(
-       ctx *service.SessionContext,
+       ctx context.Context,
        name string,
        servers []string,
        kind RRTypeList,
diff --git a/src/gnunet/service/gns/module.go b/src/gnunet/service/gns/module.go
index 5ff81f2..31ad6c7 100644
--- a/src/gnunet/service/gns/module.go
+++ b/src/gnunet/service/gns/module.go
@@ -19,18 +19,22 @@
 package gns
 
 import (
+       "context"
        "fmt"
        "net/http"
        "strings"
 
        "gnunet/config"
+       "gnunet/core"
        "gnunet/crypto"
        "gnunet/enums"
        "gnunet/message"
        "gnunet/service"
+       "gnunet/service/dht/blocks"
        "gnunet/service/revocation"
        "gnunet/util"
 
+       "github.com/bfix/gospel/data"
        "github.com/bfix/gospel/logger"
 )
 
@@ -44,34 +48,6 @@ var (
        ErrGNSRecursionExceeded = fmt.Errorf("recursion depth exceeded")
 )
 
-//----------------------------------------------------------------------
-// Query for simple GNS lookups
-//----------------------------------------------------------------------
-
-// Query specifies the context for a basic GNS name lookup of an (atomic)
-// label in a given zone identified by its public key.
-type Query struct {
-       Zone    *crypto.ZoneKey  // Public zone key
-       Label   string           // Atomic label
-       Derived *crypto.ZoneKey  // Derived key from (pkey,label)
-       Key     *crypto.HashCode // Key for repository queries (local/remote)
-}
-
-// NewQuery assembles a new Query object for the given zone and label.
-func NewQuery(zkey *crypto.ZoneKey, label string) *Query {
-       // derive a public key from (pkey,label) and set the repository
-       // key as the SHA512 hash of the binary key representation.
-       // (key blinding)
-       pd, _ := zkey.Derive(label, "gns")
-       key := crypto.Hash(pd.Bytes())
-       return &Query{
-               Zone:    zkey,
-               Label:   label,
-               Derived: pd,
-               Key:     key,
-       }
-}
-
 //----------------------------------------------------------------------
 // The GNS module (recursively) resolves GNS names:
 // Resolves DNS-like names (e.g. "minecraft.servers.bob.games"; a name is
@@ -112,25 +88,50 @@ func NewQuery(zkey *crypto.ZoneKey, label string) *Query {
 
 // Module handles the resolution of GNS names to RRs bundled in a block.
 type Module struct {
+       service.ModuleImpl
+
        // Use function references for calls to methods in other modules:
-       LookupLocal      func(ctx *service.SessionContext, query *Query) 
(*message.Block, error)
-       StoreLocal       func(ctx *service.SessionContext, block 
*message.Block) error
-       LookupRemote     func(ctx *service.SessionContext, query *Query) 
(*message.Block, error)
-       RevocationQuery  func(ctx *service.SessionContext, zkey 
*crypto.ZoneKey) (valid bool, err error)
-       RevocationRevoke func(ctx *service.SessionContext, rd 
*revocation.RevData) (success bool, err error)
+       LookupLocal      func(ctx context.Context, query *blocks.GNSQuery) 
(*blocks.GNSBlock, error)
+       StoreLocal       func(ctx context.Context, query *blocks.GNSQuery, 
block *blocks.GNSBlock) error
+       LookupRemote     func(ctx context.Context, query blocks.Query) 
(blocks.Block, error)
+       RevocationQuery  func(ctx context.Context, zkey *crypto.ZoneKey) (valid 
bool, err error)
+       RevocationRevoke func(ctx context.Context, rd *revocation.RevData) 
(success bool, err error)
 }
 
-// RPC returns the route and handler function for a JSON-RPC request
-func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) {
-       return "/gns/", func(wrt http.ResponseWriter, req *http.Request) {
-               wrt.Write([]byte(`{"msg": "This is GNS" }`))
+func NewModule(ctx context.Context, c *core.Core) (m *Module) {
+       m = &Module{
+               ModuleImpl: *service.NewModuleImpl(),
        }
+       // register as listener for core events
+       listener := m.Run(ctx, m.event, m.Filter())
+       c.Register("gns", listener)
+
+       return
+}
+
+//----------------------------------------------------------------------
+
+// Filter returns the event filter for the service
+func (m *Module) Filter() *core.EventFilter {
+       f := core.NewEventFilter()
+       f.AddMsgType(message.GNS_LOOKUP)
+       f.AddMsgType(message.GNS_LOOKUP_RESULT)
+       f.AddMsgType(message.GNS_REVERSE_LOOKUP)
+       f.AddMsgType(message.GNS_REVERSE_LOOKUP_RESULT)
+       return f
 }
 
+// Event handler
+func (m *Module) event(ctx context.Context, ev *core.Event) {
+
+}
+
+//----------------------------------------------------------------------
+
 // Resolve a GNS name with multiple labels. If pkey is not nil, the name
 // is interpreted as "relative to current zone".
 func (m *Module) Resolve(
-       ctx *service.SessionContext,
+       ctx context.Context,
        path string,
        zkey *crypto.ZoneKey,
        kind RRTypeList,
@@ -142,7 +143,7 @@ func (m *Module) Resolve(
                return nil, ErrGNSRecursionExceeded
        }
        // get the labels in reverse order
-       names := util.ReverseStringList(strings.Split(path, "."))
+       names := util.Reverse(strings.Split(path, "."))
        logger.Printf(logger.DBG, "[gns] Resolver called for %v\n", names)
 
        // check for relative path
@@ -157,7 +158,7 @@ func (m *Module) Resolve(
 // ResolveAbsolute resolves a fully qualified GNS absolute name
 // (with multiple labels).
 func (m *Module) ResolveAbsolute(
-       ctx *service.SessionContext,
+       ctx context.Context,
        labels []string,
        kind RRTypeList,
        mode int,
@@ -184,7 +185,7 @@ func (m *Module) ResolveAbsolute(
 // processing simple (PKEY,Label) lookups in sequence and handle intermediate
 // GNS record types
 func (m *Module) ResolveRelative(
-       ctx *service.SessionContext,
+       ctx context.Context,
        labels []string,
        zkey *crypto.ZoneKey,
        kind RRTypeList,
@@ -200,7 +201,7 @@ func (m *Module) ResolveRelative(
                logger.Printf(logger.DBG, "[gns] ResolveRelative '%s' in 
'%s'\n", labels[0], util.EncodeBinaryToString(zkey.Bytes()))
 
                // resolve next level
-               var block *message.Block
+               var block *blocks.GNSBlock
                if block, err = m.Lookup(ctx, zkey, labels[0], mode); err != 
nil {
                        // failed to resolve name
                        return
@@ -221,7 +222,7 @@ func (m *Module) ResolveRelative(
                }
                // post-process block by inspecting contained resource records 
for
                // special GNS types
-               if records, err = block.Records(); err != nil {
+               if records, err = m.records(block.Body.Data); err != nil {
                        return
                }
                // assemble a list of block handlers for this block: if multiple
@@ -238,10 +239,7 @@ func (m *Module) ResolveRelative(
                // handle special block cases in priority order:
                //--------------------------------------------------------------
 
-               if hdlr := hdlrs.GetHandler(
-                       enums.GNS_TYPE_PKEY,
-                       enums.GNS_TYPE_EDKEY,
-               ); hdlr != nil {
+               if hdlr := hdlrs.GetHandler(crypto.ZoneTypes...); hdlr != nil {
                        // (1) zone key record:
                        inst := hdlr.(*ZoneKeyHandler)
                        // if labels are pending, set new zone and continue 
resolution;
@@ -267,7 +265,7 @@ func (m *Module) ResolveRelative(
                        }
                        // ... otherwise we need to handle delegation to DNS: 
returns a
                        // list of found resource records in DNS (filter by 
'kind')
-                       lbls := 
strings.Join(util.ReverseStringList(labels[1:]), ".")
+                       lbls := strings.Join(util.Reverse(labels[1:]), ".")
                        if len(lbls) > 0 {
                                lbls += "."
                        }
@@ -361,7 +359,7 @@ func (m *Module) ResolveRelative(
 // a PKEY TLD), it is also resolved with GNS. All other names are resolved
 // via DNS queries.
 func (m *Module) ResolveUnknown(
-       ctx *service.SessionContext,
+       ctx context.Context,
        name string,
        labels []string,
        zkey *crypto.ZoneKey,
@@ -372,7 +370,7 @@ func (m *Module) ResolveUnknown(
        if strings.HasSuffix(name, ".+") {
                // resolve server name relative to current zone
                name = strings.TrimSuffix(name, ".+")
-               for _, label := range util.ReverseStringList(labels) {
+               for _, label := range util.Reverse(labels) {
                        name += "." + label
                }
                if set, err = m.Resolve(ctx, name, zkey, kind, 
enums.GNS_LO_DEFAULT, depth+1); err != nil {
@@ -397,7 +395,7 @@ func (m *Module) ResolveUnknown(
 
 // GetZoneKey returns the zone key (or nil) from an absolute GNS path.
 func (m *Module) GetZoneKey(path string) *crypto.ZoneKey {
-       labels := util.ReverseStringList(strings.Split(path, "."))
+       labels := util.Reverse(strings.Split(path, "."))
        if len(labels[0]) == 52 {
                if data, err := util.DecodeStringToBinary(labels[0], 32); err 
== nil {
                        if zkey, err := crypto.NewZoneKey(data); err == nil {
@@ -410,13 +408,13 @@ func (m *Module) GetZoneKey(path string) *crypto.ZoneKey {
 
 // Lookup name in GNS.
 func (m *Module) Lookup(
-       ctx *service.SessionContext,
+       ctx context.Context,
        zkey *crypto.ZoneKey,
        label string,
-       mode int) (block *message.Block, err error) {
+       mode int) (block *blocks.GNSBlock, err error) {
 
        // create query (lookup key)
-       query := NewQuery(zkey, label)
+       query := blocks.NewGNSQuery(zkey, label)
 
        // try local lookup first
        if block, err = m.LookupLocal(ctx, query); err != nil {
@@ -427,7 +425,8 @@ func (m *Module) Lookup(
        if block == nil {
                if mode == enums.GNS_LO_DEFAULT {
                        // get the block from a remote lookup
-                       if block, err = m.LookupRemote(ctx, query); err != nil 
|| block == nil {
+                       var blk blocks.Block
+                       if blk, err = m.LookupRemote(ctx, query); err != nil || 
blk == nil {
                                if err != nil {
                                        logger.Printf(logger.ERROR, "[gns] 
remote Lookup failed: %s\n", err.Error())
                                        block = nil
@@ -437,8 +436,13 @@ func (m *Module) Lookup(
                                // lookup fails completely -- no result
                                return
                        }
+                       // convert to GNSBlock
+                       if err = blocks.Unwrap(blk, &block); err != nil {
+                               logger.Println(logger.DBG, "[gns] remote 
Lookup: GNS unwrap failed")
+                               return
+                       }
                        // store RRs from remote locally.
-                       m.StoreLocal(ctx, block)
+                       m.StoreLocal(ctx, query, block)
                }
        }
        return
@@ -456,3 +460,22 @@ func (m *Module) newLEHORecord(name string, expires 
util.AbsoluteTime) *message.
        rr.Data[len(name)] = 0
        return rr
 }
+
+// Records returns the list of resource records from binary data.
+func (m *Module) records(buf []byte) ([]*message.ResourceRecord, error) {
+       // parse  data into record set
+       rs := message.NewRecordSet()
+       if err := data.Unmarshal(rs, buf); err != nil {
+               return nil, err
+       }
+       return rs.Records, nil
+}
+
+//----------------------------------------------------------------------
+
+// RPC returns the route and handler function for a JSON-RPC request
+func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) {
+       return "/gns/", func(wrt http.ResponseWriter, req *http.Request) {
+               wrt.Write([]byte(`{"msg": "This is GNS" }`))
+       }
+}
diff --git a/src/gnunet/service/gns/service.go 
b/src/gnunet/service/gns/service.go
index 6fa1eb2..326d831 100644
--- a/src/gnunet/service/gns/service.go
+++ b/src/gnunet/service/gns/service.go
@@ -19,6 +19,7 @@
 package gns
 
 import (
+       "context"
        "encoding/hex"
        "fmt"
        "io"
@@ -28,8 +29,8 @@ import (
        "gnunet/enums"
        "gnunet/message"
        "gnunet/service"
+       "gnunet/service/dht/blocks"
        "gnunet/service/revocation"
-       "gnunet/transport"
        "gnunet/util"
 
        "github.com/bfix/gospel/data"
@@ -64,115 +65,117 @@ func NewService() service.Service {
        return inst
 }
 
-// Start the GNS service
-func (s *Service) Start(spec string) error {
-       return nil
-}
-
-// Stop the GNS service
-func (s *Service) Stop() error {
-       return nil
-}
-
 // ServeClient processes a client channel.
-func (s *Service) ServeClient(ctx *service.SessionContext, mc 
*transport.MsgChannel) {
+func (s *Service) ServeClient(ctx context.Context, id int, mc 
*service.Connection) {
        reqID := 0
-loop:
+       var cancel context.CancelFunc
+       ctx, cancel = context.WithCancel(ctx)
+
        for {
                // receive next message from client
                reqID++
-               logger.Printf(logger.DBG, "[gns:%d:%d] Waiting for client 
request...\n", ctx.ID, reqID)
-               msg, err := mc.Receive(ctx.Signaller())
+               logger.Printf(logger.DBG, "[gns:%d:%d] Waiting for client 
request...\n", id, reqID)
+               msg, err := mc.Receive(ctx)
                if err != nil {
                        if err == io.EOF {
-                               logger.Printf(logger.INFO, "[gns:%d:%d] Client 
channel closed.\n", ctx.ID, reqID)
-                       } else if err == transport.ErrChannelInterrupted {
-                               logger.Printf(logger.INFO, "[gns:%d:%d] Service 
operation interrupted.\n", ctx.ID, reqID)
+                               logger.Printf(logger.INFO, "[gns:%d:%d] Client 
channel closed.\n", id, reqID)
+                       } else if err == service.ErrConnectionInterrupted {
+                               logger.Printf(logger.INFO, "[gns:%d:%d] Service 
operation interrupted.\n", id, reqID)
                        } else {
-                               logger.Printf(logger.ERROR, "[gns:%d:%d] 
Message-receive failed: %s\n", ctx.ID, reqID, err.Error())
+                               logger.Printf(logger.ERROR, "[gns:%d:%d] 
Message-receive failed: %s\n", id, reqID, err.Error())
                        }
-                       break loop
+                       break
                }
-               logger.Printf(logger.INFO, "[gns:%d:%d] Received request: 
%v\n", ctx.ID, reqID, msg)
-
-               // perform lookup
-               switch m := msg.(type) {
-               case *message.LookupMsg:
-                       
//----------------------------------------------------------
-                       // GNS_LOOKUP
-                       
//----------------------------------------------------------
-
-                       // perform lookup on block (locally and remote)
-                       go func(id int, m *message.LookupMsg) {
-                               logger.Printf(logger.INFO, "[gns:%d:%d] Lookup 
request received.\n", ctx.ID, id)
-                               resp := message.NewGNSLookupResultMsg(m.ID)
-                               ctx.Add()
-                               defer func() {
-                                       // send response
-                                       if resp != nil {
-                                               if err := mc.Send(resp, 
ctx.Signaller()); err != nil {
-                                                       
logger.Printf(logger.ERROR, "[gns:%d:%d] Failed to send response: %s\n", 
ctx.ID, id, err.Error())
-                                               }
-                                       }
-                                       // go-routine finished
-                                       logger.Printf(logger.DBG, "[gns:%d:%d] 
Lookup request finished.\n", ctx.ID, id)
-                                       ctx.Remove()
-                               }()
-
-                               label := m.GetName()
-                               kind := NewRRTypeList(int(m.Type))
-                               recset, err := s.Resolve(ctx, label, m.Zone, 
kind, int(m.Options), 0)
-                               if err != nil {
-                                       logger.Printf(logger.ERROR, 
"[gns:%d:%d] Failed to lookup block: %s\n", ctx.ID, id, err.Error())
-                                       if err == 
transport.ErrChannelInterrupted {
-                                               resp = nil
+               logger.Printf(logger.INFO, "[gns:%d:%d] Received request: 
%v\n", id, reqID, msg)
+
+               // handle message
+               s.HandleMessage(context.WithValue(ctx, "label", 
fmt.Sprintf(":%d:%d", id, reqID)), msg, mc)
+       }
+       // close client connection
+       mc.Close()
+
+       // cancel all tasks running for this session/connection
+       logger.Printf(logger.INFO, "[gns:%d] Start closing session...\n", id)
+       cancel()
+}
+
+// Handle a single incoming message
+func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back 
service.Responder) bool {
+       // assemble log label
+       label := ""
+       if v := ctx.Value("label"); v != nil {
+               label = v.(string)
+       }
+       // perform lookup
+       switch m := msg.(type) {
+       case *message.LookupMsg:
+               //----------------------------------------------------------
+               // GNS_LOOKUP
+               //----------------------------------------------------------
+
+               // perform lookup on block (locally and remote)
+               go func(m *message.LookupMsg) {
+                       logger.Printf(logger.INFO, "[gns%s] Lookup request 
received.\n", label)
+                       resp := message.NewGNSLookupResultMsg(m.ID)
+                       defer func() {
+                               // send response
+                               if resp != nil {
+                                       if err := back.Send(ctx, resp); err != 
nil {
+                                               logger.Printf(logger.ERROR, 
"[gns%s] Failed to send response: %s\n", label, err.Error())
                                        }
+                               }
+                               // go-routine finished
+                               logger.Printf(logger.DBG, "[gns%s] Lookup 
request finished.\n", label)
+                       }()
+
+                       label := m.GetName()
+                       kind := NewRRTypeList(int(m.Type))
+                       recset, err := s.Resolve(ctx, label, m.Zone, kind, 
int(m.Options), 0)
+                       if err != nil {
+                               logger.Printf(logger.ERROR, "[gns%s] Failed to 
lookup block: %s\n", label, err.Error())
+                               if err == service.ErrConnectionInterrupted {
+                                       resp = nil
+                               }
+                               return
+                       }
+                       // handle records
+                       if recset != nil {
+                               logger.Printf(logger.DBG, "[gns%s] Received 
record set with %d entries\n", label, recset.Count)
+
+                               // get records from block
+                               if recset.Count == 0 {
+                                       logger.Printf(logger.WARN, "[gns%s] No 
records in block\n", label)
                                        return
                                }
-                               // handle records
-                               if recset != nil {
-                                       logger.Printf(logger.DBG, "[gns:%d:%d] 
Received record set with %d entries\n", ctx.ID, id, recset.Count)
-
-                                       // get records from block
-                                       if recset.Count == 0 {
-                                               logger.Printf(logger.WARN, 
"[gns:%d:%d] No records in block\n", ctx.ID, id)
-                                               return
-                                       }
-                                       // process records
-                                       for i, rec := range recset.Records {
-                                               logger.Printf(logger.DBG, 
"[gns:%d:%d] Record #%d: %v\n", ctx.ID, id, i, rec)
-
-                                               // is this the record type we 
are looking for?
-                                               if rec.Type == m.Type || 
int(m.Type) == enums.GNS_TYPE_ANY {
-                                                       // add it to the 
response message
-                                                       resp.AddRecord(rec)
-                                               }
+                               // process records
+                               for i, rec := range recset.Records {
+                                       logger.Printf(logger.DBG, "[gns%s] 
Record #%d: %v\n", label, i, rec)
+
+                                       // is this the record type we are 
looking for?
+                                       if rec.Type == m.Type || int(m.Type) == 
enums.GNS_TYPE_ANY {
+                                               // add it to the response 
message
+                                               resp.AddRecord(rec)
                                        }
                                }
-                       }(reqID, m)
-
-               default:
-                       
//----------------------------------------------------------
-                       // UNKNOWN message type received
-                       
//----------------------------------------------------------
-                       logger.Printf(logger.ERROR, "[gns:%d:%d] Unhandled 
message of type (%d)\n", ctx.ID, reqID, msg.Header().MsgType)
-                       break loop
-               }
-       }
-       // close client connection
-       mc.Close()
+                       }
+               }(m)
 
-       // cancel all tasks running for this session/connection
-       logger.Printf(logger.INFO, "[gns:%d] Start closing session... [%d]\n", 
ctx.ID, ctx.Waiting())
-       ctx.Cancel()
+       default:
+               //----------------------------------------------------------
+               // UNKNOWN message type received
+               //----------------------------------------------------------
+               logger.Printf(logger.ERROR, "[gns%s] Unhandled message of type 
(%d)\n", label, msg.Header().MsgType)
+               return false
+       }
+       return true
 }
 
 //======================================================================
-// Revocationrelated methods
+// Revocation-related methods
 //======================================================================
 
 // QueryKeyRevocation checks if a key has been revoked
-func (s *Service) QueryKeyRevocation(ctx *service.SessionContext, zkey 
*crypto.ZoneKey) (valid bool, err error) {
+func (s *Service) QueryKeyRevocation(ctx context.Context, zkey 
*crypto.ZoneKey) (valid bool, err error) {
        logger.Printf(logger.DBG, "[gns] QueryKeyRev(%s)...\n", 
util.EncodeBinaryToString(zkey.Bytes()))
 
        // assemble request
@@ -180,7 +183,7 @@ func (s *Service) QueryKeyRevocation(ctx 
*service.SessionContext, zkey *crypto.Z
 
        // get response from Revocation service
        var resp message.Message
-       if resp, err = service.RequestResponse(ctx, "gns", "Revocation", 
config.Cfg.Revocation.Endpoint, req); err != nil {
+       if resp, err = service.RequestResponse(ctx, "gns", "Revocation", 
config.Cfg.Revocation.Service.Socket, req); err != nil {
                return
        }
 
@@ -195,7 +198,7 @@ func (s *Service) QueryKeyRevocation(ctx 
*service.SessionContext, zkey *crypto.Z
 }
 
 // RevokeKey revokes a key with given revocation data
-func (s *Service) RevokeKey(ctx *service.SessionContext, rd 
*revocation.RevData) (success bool, err error) {
+func (s *Service) RevokeKey(ctx context.Context, rd *revocation.RevData) 
(success bool, err error) {
        logger.Printf(logger.DBG, "[gns] RevokeKey(%s)...\n", 
rd.ZoneKeySig.ID())
 
        // assemble request
@@ -206,7 +209,7 @@ func (s *Service) RevokeKey(ctx *service.SessionContext, rd 
*revocation.RevData)
 
        // get response from Revocation service
        var resp message.Message
-       if resp, err = service.RequestResponse(ctx, "gns", "Revocation", 
config.Cfg.Revocation.Endpoint, req); err != nil {
+       if resp, err = service.RequestResponse(ctx, "gns", "Revocation", 
config.Cfg.Revocation.Service.Socket, req); err != nil {
                return
        }
 
@@ -225,17 +228,17 @@ func (s *Service) RevokeKey(ctx *service.SessionContext, 
rd *revocation.RevData)
 //======================================================================
 
 // LookupNamecache returns a cached lookup (if available)
-func (s *Service) LookupNamecache(ctx *service.SessionContext, query *Query) 
(block *message.Block, err error) {
-       logger.Printf(logger.DBG, "[gns] LookupNamecache(%s)...\n", 
hex.EncodeToString(query.Key.Bits))
+func (s *Service) LookupNamecache(ctx context.Context, query *blocks.GNSQuery) 
(block *blocks.GNSBlock, err error) {
+       logger.Printf(logger.DBG, "[gns] LookupNamecache(%s)...\n", 
hex.EncodeToString(query.Key().Bits))
 
        // assemble Namecache request
-       req := message.NewNamecacheLookupMsg(query.Key)
+       req := message.NewNamecacheLookupMsg(query.Key())
        req.ID = uint32(util.NextID())
        block = nil
 
        // get response from Namecache service
        var resp message.Message
-       if resp, err = service.RequestResponse(ctx, "gns", "Namecache", 
config.Cfg.Namecache.Endpoint, req); err != nil {
+       if resp, err = service.RequestResponse(ctx, "gns", "Namecache", 
config.Cfg.Namecache.Service.Socket, req); err != nil {
                return
        }
 
@@ -250,7 +253,7 @@ func (s *Service) LookupNamecache(ctx 
*service.SessionContext, query *Query) (bl
                        break
                }
                // check if block was found
-               if len(m.EncData) == 0 || util.IsNull(m.EncData) {
+               if len(m.EncData) == 0 || util.IsAll(m.EncData, 0) {
                        logger.Println(logger.DBG, "[gns] block not found in 
namecache")
                        break
                }
@@ -262,21 +265,21 @@ func (s *Service) LookupNamecache(ctx 
*service.SessionContext, query *Query) (bl
                }
 
                // assemble GNSBlock from message
-               block = new(message.Block)
+               block = new(blocks.GNSBlock)
                block.DerivedKeySig = m.DerivedKeySig
-               sb := new(message.SignedBlockData)
+               sb := new(blocks.SignedGNSBlockData)
                sb.Purpose = new(crypto.SignaturePurpose)
                sb.Purpose.Purpose = enums.SIG_GNS_RECORD_SIGN
                sb.Purpose.Size = uint32(16 + len(m.EncData))
                sb.Expire = m.Expire
-               sb.EncData = m.EncData
-               block.Block = sb
+               sb.Data = m.EncData
+               block.Body = sb
 
                // verify and decrypt block
-               if err = block.Verify(query.Zone, query.Label); err != nil {
+               if err = query.Verify(block); err != nil {
                        break
                }
-               if err = block.Decrypt(query.Zone, query.Label); err != nil {
+               if err = query.Decrypt(block); err != nil {
                        break
                }
        default:
@@ -287,7 +290,7 @@ func (s *Service) LookupNamecache(ctx 
*service.SessionContext, query *Query) (bl
 }
 
 // StoreNamecache stores a lookup in the local namecache.
-func (s *Service) StoreNamecache(ctx *service.SessionContext, block 
*message.Block) (err error) {
+func (s *Service) StoreNamecache(ctx context.Context, query *blocks.GNSQuery, 
block *blocks.GNSBlock) (err error) {
        logger.Println(logger.DBG, "[gns] StoreNamecache()...")
 
        // assemble Namecache request
@@ -296,7 +299,7 @@ func (s *Service) StoreNamecache(ctx 
*service.SessionContext, block *message.Blo
 
        // get response from Namecache service
        var resp message.Message
-       if resp, err = service.RequestResponse(ctx, "gns", "Namecache", 
config.Cfg.Namecache.Endpoint, req); err != nil {
+       if resp, err = service.RequestResponse(ctx, "gns", "Namecache", 
config.Cfg.Namecache.Service.Socket, req); err != nil {
                return
        }
 
@@ -327,13 +330,13 @@ func (s *Service) StoreNamecache(ctx 
*service.SessionContext, block *message.Blo
 //======================================================================
 
 // LookupDHT gets a GNS block from the DHT for the given query key.
-func (s *Service) LookupDHT(ctx *service.SessionContext, query *Query) (block 
*message.Block, err error) {
-       logger.Printf(logger.DBG, "[gns] LookupDHT(%s)...\n", 
hex.EncodeToString(query.Key.Bits))
+func (s *Service) LookupDHT(ctx context.Context, query blocks.Query) (block 
blocks.Block, err error) {
+       logger.Printf(logger.DBG, "[gns] LookupDHT(%s)...\n", 
hex.EncodeToString(query.Key().Bits))
        block = nil
 
        // client-connect to the DHT service
        logger.Println(logger.DBG, "[gns] Connecting to DHT service...")
-       cl, err := service.NewClient(config.Cfg.DHT.Endpoint)
+       cl, err := service.NewClient(ctx, config.Cfg.DHT.Service.Socket)
        if err != nil {
                return nil, err
        }
@@ -360,7 +363,7 @@ func (s *Service) LookupDHT(ctx *service.SessionContext, 
query *Query) (block *m
        )
 
        // send DHT GET request and wait for response
-       reqGet := message.NewDHTClientGetMsg(query.Key)
+       reqGet := message.NewDHTClientGetMsg(query.Key())
        reqGet.ID = uint64(util.NextID())
        reqGet.ReplLevel = uint32(enums.DHT_GNS_REPLICATION_LEVEL)
        reqGet.Type = uint32(enums.BLOCK_TYPE_GNS_NAMERECORD)
@@ -368,16 +371,16 @@ func (s *Service) LookupDHT(ctx *service.SessionContext, 
query *Query) (block *m
 
        if err = interact(reqGet, true); err != nil {
                // check for aborted remote lookup: we need to cancel the query
-               if err == transport.ErrChannelInterrupted {
+               if err == service.ErrConnectionInterrupted {
                        logger.Println(logger.WARN, "[gns] remote Lookup 
aborted -- cleaning up.")
 
                        // send DHT GET_STOP request and terminate
-                       reqStop := message.NewDHTClientGetStopMsg(query.Key)
+                       reqStop := message.NewDHTClientGetStopMsg(query.Key())
                        reqStop.ID = reqGet.ID
                        if err = interact(reqStop, false); err != nil {
                                logger.Printf(logger.ERROR, "[gns] remote 
Lookup abort failed: %s\n", err.Error())
                        }
-                       return nil, transport.ErrChannelInterrupted
+                       return nil, service.ErrConnectionInterrupted
                }
        }
 
@@ -407,22 +410,24 @@ func (s *Service) LookupDHT(ctx *service.SessionContext, 
query *Query) (block *m
                }
 
                // get GNSBlock from message
-               block = message.NewBlock()
+               qGNS := query.(*blocks.GNSQuery)
+               block = new(blocks.GNSBlock)
                if err = data.Unmarshal(block, m.Data); err != nil {
                        logger.Printf(logger.ERROR, "[gns] can't read GNS 
block: %s\n", err.Error())
                        break
                }
+
                // verify and decrypt block
-               if err = block.Verify(query.Zone, query.Label); err != nil {
+               if err = qGNS.Verify(block); err != nil {
                        break
                }
-               if err = block.Decrypt(query.Zone, query.Label); err != nil {
+               if err = qGNS.Decrypt(block); err != nil {
                        break
                }
 
                // we got a result from DHT that was not in the namecache,
                // so store it there now.
-               if err = s.StoreNamecache(ctx, block); err != nil {
+               if err = s.StoreNamecache(ctx, qGNS, block.(*blocks.GNSBlock)); 
err != nil {
                        logger.Printf(logger.ERROR, "[gns] can't store block in 
Namecache: %s\n", err.Error())
                }
        }
diff --git a/src/gnunet/service/module.go b/src/gnunet/service/module.go
new file mode 100644
index 0000000..98307d6
--- /dev/null
+++ b/src/gnunet/service/module.go
@@ -0,0 +1,70 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package service
+
+import (
+       "context"
+       "gnunet/core"
+       "net/http"
+)
+
+// Module is an interface for GNUnet service modules (workers).
+type Module interface {
+       // RPC returns the route and handler for JSON-RPC requests
+       RPC() (string, func(http.ResponseWriter, *http.Request))
+
+       // Filter returns the event filter for the module
+       Filter() *core.EventFilter
+}
+
+// EventHandler is a function prototype for event handling
+type EventHandler func(context.Context, *core.Event)
+
+// ModuleImpl is an event-handling type used by Module implementations.
+type ModuleImpl struct {
+       ch chan *core.Event // channel for core events.
+}
+
+// NewModuleImplementation returns a new base module and starts
+func NewModuleImpl() (m *ModuleImpl) {
+       return &ModuleImpl{
+               ch: make(chan *core.Event),
+       }
+}
+
+// Run event handling loop
+func (m *ModuleImpl) Run(ctx context.Context, hdlr EventHandler, filter 
*core.EventFilter) (listener *core.Listener) {
+       // listener for registration
+       listener = core.NewListener(m.ch, filter)
+       // run event loop
+       go func() {
+               for {
+                       select {
+                       // Handle events
+                       case event := <-m.ch:
+                               hdlr(ctx, event)
+
+                       // wait for terminate signal
+                       case <-ctx.Done():
+                               return
+                       }
+               }
+       }()
+       return
+}
diff --git a/src/gnunet/service/namecache/module.go 
b/src/gnunet/service/namecache/module.go
index d5aa014..9d5bca1 100644
--- a/src/gnunet/service/namecache/module.go
+++ b/src/gnunet/service/namecache/module.go
@@ -19,9 +19,11 @@
 package namecache
 
 import (
-       "gnunet/message"
+       "context"
+       "gnunet/config"
+       "gnunet/core"
        "gnunet/service"
-       "gnunet/service/gns"
+       "gnunet/service/dht/blocks"
 )
 
 //======================================================================
@@ -34,12 +36,29 @@ import (
 
 // Namecache handles the transient storage of GNS blocks under the query key.
 type NamecacheModule struct {
+       service.ModuleImpl
+
+       cache service.DHTStore // transient block cache
+}
+
+// NewModule creates a new module instance.
+func NewModule(ctx context.Context, c *core.Core) (m *NamecacheModule) {
+       m = &NamecacheModule{
+               ModuleImpl: *service.NewModuleImpl(),
+       }
+       m.cache, _ = service.NewDHTStore(config.Cfg.Namecache.Storage)
+       return
 }
 
-func (nc *NamecacheModule) Get(ctx *service.SessionContext, query *gns.Query) 
(*message.Block, error) {
-       return nil, nil
+// Get an entry from the cache if available.
+func (m *NamecacheModule) Get(ctx context.Context, query *blocks.GNSQuery) 
(block *blocks.GNSBlock, err error) {
+       var b blocks.Block
+       b, err = m.cache.Get(query)
+       err = blocks.Unwrap(b, block)
+       return
 }
 
-func (nc *NamecacheModule) Put(ctx *service.SessionContext, block 
*message.Block) error {
-       return nil
+// Put entry into the cache.
+func (m *NamecacheModule) Put(ctx context.Context, query *blocks.GNSQuery, 
block *blocks.GNSBlock) error {
+       return m.cache.Put(query, block)
 }
diff --git a/src/gnunet/service/revocation/module.go 
b/src/gnunet/service/revocation/module.go
index d13c069..37b57ab 100644
--- a/src/gnunet/service/revocation/module.go
+++ b/src/gnunet/service/revocation/module.go
@@ -19,8 +19,11 @@
 package revocation
 
 import (
+       "context"
        "gnunet/config"
+       "gnunet/core"
        "gnunet/crypto"
+       "gnunet/message"
        "gnunet/service"
        "gnunet/util"
        "net/http"
@@ -33,55 +36,71 @@ import (
 // "GNUnet Revocation" implementation
 //======================================================================
 
+// The minimum average difficulty acceptable for a set of revocation PoWs
+const MinAvgDifficulty = 23
+
 // Module handles the revocation-related calls to other modules.
 type Module struct {
-       bloomf *data.BloomFilter  // bloomfilter for fast revocation check
-       kvs    util.KeyValueStore // storage for known revocations
+       service.ModuleImpl
+
+       bloomf *data.BloomFilter // bloomfilter for fast revocation check
+       kvs    service.KVStore   // storage for known revocations
 }
 
-// Init a revocation module
-func (m *Module) Init() error {
-       // Initialize access to revocation data storage
-       var err error
-       if m.kvs, err = util.OpenKVStore(config.Cfg.Revocation.Storage); err != 
nil {
-               return err
-       }
-       // traverse the storage and build bloomfilter for all keys
-       m.bloomf = data.NewBloomFilter(1000000, 1e-8)
-       keys, err := m.kvs.List()
-       if err != nil {
-               return err
+// NewModule returns an initialized revocation module
+func NewModule(ctx context.Context, c *core.Core) (m *Module) {
+       // create and init instance
+       m = &Module{
+               ModuleImpl: *service.NewModuleImpl(),
        }
-       for _, key := range keys {
-               buf, err := util.DecodeStringToBinary(key, 32)
-               if err != nil {
-                       return err
+       init := func() (err error) {
+               // Initialize access to revocation data storage
+               if m.kvs, err = 
service.NewKVStore(config.Cfg.Revocation.Storage); err != nil {
+                       return
                }
-               m.bloomf.Add(buf)
+               // traverse the storage and build bloomfilter for all keys
+               m.bloomf = data.NewBloomFilter(1000000, 1e-8)
+               var keys []string
+               if keys, err = m.kvs.List(); err != nil {
+                       return
+               }
+               for _, key := range keys {
+                       m.bloomf.Add([]byte(key))
+               }
+               return
        }
-       return nil
-}
-
-// NewModule returns an initialized revocation module
-func NewModule() *Module {
-       m := new(Module)
-       if err := m.Init(); err != nil {
+       if err := init(); err != nil {
                logger.Printf(logger.ERROR, "[revocation] Failed to initialize 
module: %s\n", err.Error())
                return nil
        }
+       // register as listener for core events
+       listener := m.Run(ctx, m.event, m.Filter())
+       c.Register("gns", listener)
        return m
 }
 
-// RPC returns the route and handler function for a JSON-RPC request
-func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) {
-       return "/revocation/", func(wrt http.ResponseWriter, req *http.Request) 
{
-               wrt.Write([]byte(`{"msg": "This is REVOCATION" }`))
-       }
+//----------------------------------------------------------------------
+
+// Filter returns the event filter for the service
+func (m *Module) Filter() *core.EventFilter {
+       f := core.NewEventFilter()
+       f.AddMsgType(message.REVOCATION_QUERY)
+       f.AddMsgType(message.REVOCATION_QUERY_RESPONSE)
+       f.AddMsgType(message.REVOCATION_REVOKE)
+       f.AddMsgType(message.REVOCATION_REVOKE_RESPONSE)
+       return f
 }
 
+// Event handler
+func (m *Module) event(ctx context.Context, ev *core.Event) {
+
+}
+
+//----------------------------------------------------------------------
+
 // Query return true if the pkey is valid (not revoked) and false
 // if the pkey has been revoked.
-func (m *Module) Query(ctx *service.SessionContext, zkey *crypto.ZoneKey) 
(valid bool, err error) {
+func (m *Module) Query(ctx context.Context, zkey *crypto.ZoneKey) (valid bool, 
err error) {
        // fast check first: is the key in the bloomfilter?
        data := zkey.Bytes()
        if !m.bloomf.Contains(data) {
@@ -100,9 +119,9 @@ func (m *Module) Query(ctx *service.SessionContext, zkey 
*crypto.ZoneKey) (valid
 }
 
 // Revoke a key with given revocation data
-func (m *Module) Revoke(ctx *service.SessionContext, rd *RevData) (success 
bool, err error) {
+func (m *Module) Revoke(ctx context.Context, rd *RevData) (success bool, err 
error) {
        // verify the revocation data
-       rc := rd.Verify(true)
+       diff, rc := rd.Verify(true)
        switch {
        case rc == -1:
                logger.Println(logger.WARN, "[revocation] Revoke: 
Missing/invalid signature")
@@ -113,10 +132,12 @@ func (m *Module) Revoke(ctx *service.SessionContext, rd 
*RevData) (success bool,
        case rc == -3:
                logger.Println(logger.WARN, "[revocation] Revoke: Wrong PoW 
sequence order")
                return false, nil
-       case rc < 25:
+       }
+       if diff < float64(MinAvgDifficulty) {
                logger.Println(logger.WARN, "[revocation] Revoke: Difficulty to 
small")
                return false, nil
        }
+
        // store the revocation data
        // (1) add it to the bloomfilter
        m.bloomf.Add(rd.ZoneKeySig.KeyData)
@@ -129,3 +150,12 @@ func (m *Module) Revoke(ctx *service.SessionContext, rd 
*RevData) (success bool,
        err = m.kvs.Put(rd.ZoneKeySig.ID(), value)
        return true, err
 }
+
+//----------------------------------------------------------------------
+
+// RPC returns the route and handler function for a JSON-RPC request
+func (m *Module) RPC() (string, func(http.ResponseWriter, *http.Request)) {
+       return "/revocation/", func(wrt http.ResponseWriter, req *http.Request) 
{
+               wrt.Write([]byte(`{"msg": "This is REVOCATION" }`))
+       }
+}
diff --git a/src/gnunet/service/revocation/pow.go 
b/src/gnunet/service/revocation/pow.go
index 04bbb13..57ddad7 100644
--- a/src/gnunet/service/revocation/pow.go
+++ b/src/gnunet/service/revocation/pow.go
@@ -40,6 +40,11 @@ import (
 // Proof-of-Work data
 //----------------------------------------------------------------------
 
+const (
+       // MinDifficulty for revocations -> expires in ~1year
+       MinDifficulty = 23
+)
+
 // PoWData is the proof-of-work data
 type PoWData struct {
        PoW       uint64            `order:"big"` // start with this PoW value
@@ -143,6 +148,11 @@ func NewRevDataFromMsg(m *message.RevocationRevokeMsg) 
*RevData {
        return rd
 }
 
+// Size of a serialized RevData object.
+func (rd *RevData) Size() int {
+       return 16 + 8*len(rd.PoWs) + int(rd.ZoneKeySig.SigSize())
+}
+
 // Sign the revocation data
 func (rd *RevData) Sign(skey *crypto.ZonePrivate) (err error) {
        sigBlock := &SignedRevData{
@@ -160,12 +170,10 @@ func (rd *RevData) Sign(skey *crypto.ZonePrivate) (err 
error) {
        return
 }
 
-// Verify a revocation object: returns the (smallest) number of leading
-// zero-bits in the PoWs of this revocation; a number > 0, but smaller
-// than the minimum (25) indicates invalid PoWs; a value of -1 indicates
-// a failed signature; -2 indicates an expired revocation and -3 for a
-// "out-of-order" PoW sequence.
-func (rd *RevData) Verify(withSig bool) int {
+// Verify a revocation object and return the average difficulty of the PoWs
+// in this revocation and a verification status (-1=failed signature, -2=
+// expired revocation, -3="out-of-order" PoW sequence).
+func (rd *RevData) Verify(withSig bool) (zbits float64, rc int) {
 
        // (1) check signature
        if withSig {
@@ -179,39 +187,36 @@ func (rd *RevData) Verify(withSig bool) int {
                }
                sigData, err := data.Marshal(sigBlock)
                if err != nil {
-                       return -1
+                       return 0., -1
                }
                valid, err := rd.ZoneKeySig.Verify(sigData)
                if err != nil || !valid {
-                       return -1
+                       return 0., -1
                }
        }
 
        // (2) check PoWs
-       var (
-               zbits float64 = 0
-               last  uint64  = 0
-       )
+       var last uint64 = 0
        for _, pow := range rd.PoWs {
                // check sequence order
                if pow <= last {
-                       return -3
+                       return 0., -3
                }
                last = pow
                // compute number of leading zero-bits
                work := NewPoWData(pow, rd.Timestamp, &rd.ZoneKeySig.ZoneKey)
                zbits += float64(512 - work.Compute().BitLen())
        }
-       zbits /= 32.0
+       zbits /= float64(len(rd.PoWs))
 
        // (3) check expiration
-       if zbits > 24.0 {
-               ttl := time.Duration(int((zbits-24)*365*24)) * time.Hour
+       if zbits >= 23.0 {
+               ttl := time.Duration(int((zbits-22)*365*24*1.1)) * time.Hour
                if util.AbsoluteTimeNow().Add(ttl).Expired() {
-                       return -2
+                       return zbits, -2
                }
        }
-       return int(zbits)
+       return zbits, 0
 }
 
 //----------------------------------------------------------------------
@@ -240,6 +245,11 @@ func NewRevDataCalc(zkey *crypto.ZoneKey) *RevDataCalc {
        return rd
 }
 
+// Size of a serialized RevData object.
+func (rdc *RevDataCalc) Size() int {
+       return rdc.RevData.Size() + 2*len(rdc.Bits) + 1
+}
+
 // Average number of leading zero-bits in current list
 func (rdc *RevDataCalc) Average() float64 {
        var sum uint16 = 0
diff --git a/src/gnunet/service/revocation/pow_test.go 
b/src/gnunet/service/revocation/pow_test.go
index 8280402..a59f92b 100644
--- a/src/gnunet/service/revocation/pow_test.go
+++ b/src/gnunet/service/revocation/pow_test.go
@@ -16,7 +16,6 @@ func TestRevocationRFC(t *testing.T) {
        var (
                D     = 
"6fea32c05af58bfa979553d188605fd57d8bf9cc263b78d5f7478c07b998ed70"
                ZKEY  = 
"000100002ca223e879ecc4bbdeb5da17319281d63b2e3b6955f1c3775c804a98d5f8ddaa"
-               DIFF  = 7
                PROOF = "" +
                        "0005d66da3598127" +
                        "0000395d1827c000" +
@@ -142,8 +141,11 @@ func TestRevocationRFC(t *testing.T) {
        }
 
        // verify revocation data object
-       rc := revData.Verify(true)
-       if rc != DIFF {
+       diff, rc := revData.Verify(true)
+       if testing.Verbose() {
+               t.Logf("Average difficulty of PoWs = %f\n", diff)
+       }
+       if rc != 0 {
                t.Fatalf("REV_Verify (pkey): %d\n", rc)
        }
 }
diff --git a/src/gnunet/service/revocation/service.go 
b/src/gnunet/service/revocation/service.go
index a82e582..4d48d40 100644
--- a/src/gnunet/service/revocation/service.go
+++ b/src/gnunet/service/revocation/service.go
@@ -19,11 +19,12 @@
 package revocation
 
 import (
+       "context"
+       "fmt"
        "io"
 
        "gnunet/message"
        "gnunet/service"
-       "gnunet/transport"
 
        "github.com/bfix/gospel/logger"
 )
@@ -44,114 +45,113 @@ func NewService() service.Service {
        return inst
 }
 
-// Start the Revocation service
-func (s *Service) Start(spec string) error {
-       return nil
-}
-
-// Stop the Revocation service
-func (s *Service) Stop() error {
-       return nil
-}
-
 // ServeClient processes a client channel.
-func (s *Service) ServeClient(ctx *service.SessionContext, mc 
*transport.MsgChannel) {
+func (s *Service) ServeClient(ctx context.Context, id int, mc 
*service.Connection) {
        reqID := 0
-loop:
+       var cancel context.CancelFunc
+       ctx, cancel = context.WithCancel(ctx)
+
        for {
                // receive next message from client
                reqID++
-               logger.Printf(logger.DBG, "[revocation:%d:%d] Waiting for 
client request...\n", ctx.ID, reqID)
-               msg, err := mc.Receive(ctx.Signaller())
+               logger.Printf(logger.DBG, "[revocation:%d:%d] Waiting for 
client request...\n", id, reqID)
+               msg, err := mc.Receive(ctx)
                if err != nil {
                        if err == io.EOF {
-                               logger.Printf(logger.INFO, "[revocation:%d:%d] 
Client channel closed.\n", ctx.ID, reqID)
-                       } else if err == transport.ErrChannelInterrupted {
-                               logger.Printf(logger.INFO, "[revocation:%d:%d] 
Service operation interrupted.\n", ctx.ID, reqID)
+                               logger.Printf(logger.INFO, "[revocation:%d:%d] 
Client channel closed.\n", id, reqID)
+                       } else if err == service.ErrConnectionInterrupted {
+                               logger.Printf(logger.INFO, "[revocation:%d:%d] 
Service operation interrupted.\n", id, reqID)
                        } else {
-                               logger.Printf(logger.ERROR, "[revocation:%d:%d] 
Message-receive failed: %s\n", ctx.ID, reqID, err.Error())
+                               logger.Printf(logger.ERROR, "[revocation:%d:%d] 
Message-receive failed: %s\n", id, reqID, err.Error())
                        }
-                       break loop
-               }
-               logger.Printf(logger.INFO, "[revocation:%d:%d] Received 
request: %v\n", ctx.ID, reqID, msg)
-
-               // handle request
-               switch m := msg.(type) {
-               case *message.RevocationQueryMsg:
-                       
//----------------------------------------------------------
-                       // REVOCATION_QUERY
-                       
//----------------------------------------------------------
-                       go func(id int, m *message.RevocationQueryMsg) {
-                               logger.Printf(logger.INFO, "[revocation:%d:%d] 
Query request received.\n", ctx.ID, id)
-                               var resp *message.RevocationQueryResponseMsg
-                               ctx.Add()
-                               defer func() {
-                                       // send response
-                                       if resp != nil {
-                                               if err := mc.Send(resp, 
ctx.Signaller()); err != nil {
-                                                       
logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to send response: %s\n", 
ctx.ID, id, err.Error())
-                                               }
-                                       }
-                                       // go-routine finished
-                                       logger.Printf(logger.DBG, 
"[revocation:%d:%d] Query request finished.\n", ctx.ID, id)
-                                       ctx.Remove()
-                               }()
-
-                               valid, err := s.Query(ctx, m.Zone)
-                               if err != nil {
-                                       logger.Printf(logger.ERROR, 
"[revocation:%d:%d] Failed to query revocation status: %s\n", ctx.ID, id, 
err.Error())
-                                       if err == 
transport.ErrChannelInterrupted {
-                                               resp = nil
-                                       }
-                                       return
-                               }
-                               resp = 
message.NewRevocationQueryResponseMsg(valid)
-                       }(reqID, m)
-
-               case *message.RevocationRevokeMsg:
-                       
//----------------------------------------------------------
-                       // REVOCATION_REVOKE
-                       
//----------------------------------------------------------
-                       go func(id int, m *message.RevocationRevokeMsg) {
-                               logger.Printf(logger.INFO, "[revocation:%d:%d] 
Revoke request received.\n", ctx.ID, id)
-                               var resp *message.RevocationRevokeResponseMsg
-                               ctx.Add()
-                               defer func() {
-                                       // send response
-                                       if resp != nil {
-                                               if err := mc.Send(resp, 
ctx.Signaller()); err != nil {
-                                                       
logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to send response: %s\n", 
ctx.ID, id, err.Error())
-                                               }
-                                       }
-                                       // go-routine finished
-                                       logger.Printf(logger.DBG, 
"[revocation:%d:%d] Revoke request finished.\n", ctx.ID, id)
-                                       ctx.Remove()
-                               }()
-
-                               rd := NewRevDataFromMsg(m)
-                               valid, err := s.Revoke(ctx, rd)
-                               if err != nil {
-                                       logger.Printf(logger.ERROR, 
"[revocation:%d:%d] Failed to revoke key: %s\n", ctx.ID, id, err.Error())
-                                       if err == 
transport.ErrChannelInterrupted {
-                                               resp = nil
-                                       }
-                                       return
-                               }
-                               resp = 
message.NewRevocationRevokeResponseMsg(valid)
-                       }(reqID, m)
-
-               default:
-                       
//----------------------------------------------------------
-                       // UNKNOWN message type received
-                       
//----------------------------------------------------------
-                       logger.Printf(logger.ERROR, "[revocation:%d:%d] 
Unhandled message of type (%d)\n", ctx.ID, reqID, msg.Header().MsgType)
-                       break loop
+                       break
                }
+               logger.Printf(logger.INFO, "[revocation:%d:%d] Received 
request: %v\n", id, reqID, msg)
+
+               // handle message
+               s.HandleMessage(context.WithValue(ctx, "label", 
fmt.Sprintf(":%d:%d", id, reqID)), msg, mc)
        }
        // close client connection
        mc.Close()
 
        // cancel all tasks running for this session/connection
-       logger.Printf(logger.INFO, "[revocation:%d] Start closing session... 
[%d]\n", ctx.ID, ctx.Waiting())
-       ctx.Cancel()
+       logger.Printf(logger.INFO, "[revocation:%d] Start closing 
session...\n", id)
+       cancel()
+}
+
+// Handle a single incoming message
+func (s *Service) HandleMessage(ctx context.Context, msg message.Message, back 
service.Responder) bool {
+       // assemble log label
+       label := ""
+       if v := ctx.Value("label"); v != nil {
+               label = v.(string)
+       }
+       switch m := msg.(type) {
+       case *message.RevocationQueryMsg:
+               //----------------------------------------------------------
+               // REVOCATION_QUERY
+               //----------------------------------------------------------
+               go func(m *message.RevocationQueryMsg) {
+                       logger.Printf(logger.INFO, "[revocation%s] Query 
request received.\n", label)
+                       var resp *message.RevocationQueryResponseMsg
+                       defer func() {
+                               // send response
+                               if resp != nil {
+                                       if err := back.Send(ctx, resp); err != 
nil {
+                                               logger.Printf(logger.ERROR, 
"[revocation%s] Failed to send response: %s\n", label, err.Error())
+                                       }
+                               }
+                               // go-routine finished
+                               logger.Printf(logger.DBG, "[revocation%s] Query 
request finished.\n", label)
+                       }()
+
+                       valid, err := s.Query(ctx, m.Zone)
+                       if err != nil {
+                               logger.Printf(logger.ERROR, "[revocation%s] 
Failed to query revocation status: %s\n", label, err.Error())
+                               if err == service.ErrConnectionInterrupted {
+                                       resp = nil
+                               }
+                               return
+                       }
+                       resp = message.NewRevocationQueryResponseMsg(valid)
+               }(m)
+
+       case *message.RevocationRevokeMsg:
+               //----------------------------------------------------------
+               // REVOCATION_REVOKE
+               //----------------------------------------------------------
+               go func(m *message.RevocationRevokeMsg) {
+                       logger.Printf(logger.INFO, "[revocation%s] Revoke 
request received.\n", label)
+                       var resp *message.RevocationRevokeResponseMsg
+                       defer func() {
+                               // send response
+                               if resp != nil {
+                                       if err := back.Send(ctx, resp); err != 
nil {
+                                               logger.Printf(logger.ERROR, 
"[revocation%s] Failed to send response: %s\n", label, err.Error())
+                                       }
+                               }
+                               // go-routine finished
+                               logger.Printf(logger.DBG, "[revocation%s] 
Revoke request finished.\n", label)
+                       }()
+
+                       rd := NewRevDataFromMsg(m)
+                       valid, err := s.Revoke(ctx, rd)
+                       if err != nil {
+                               logger.Printf(logger.ERROR, "[revocation%s] 
Failed to revoke key: %s\n", label, err.Error())
+                               if err == service.ErrConnectionInterrupted {
+                                       resp = nil
+                               }
+                               return
+                       }
+                       resp = message.NewRevocationRevokeResponseMsg(valid)
+               }(m)
+
+       default:
+               //----------------------------------------------------------
+               // UNKNOWN message type received
+               //----------------------------------------------------------
+               logger.Printf(logger.ERROR, "[revocation%s] Unhandled message 
of type (%d)\n", label, msg.Header().MsgType)
+               return false
+       }
+       return true
 }
diff --git a/src/gnunet/service/service.go b/src/gnunet/service/service.go
index c996e0c..32ccf67 100644
--- a/src/gnunet/service/service.go
+++ b/src/gnunet/service/service.go
@@ -1,5 +1,5 @@
 // This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
 //
 // gnunet-go is free software: you can redistribute it and/or modify it
 // under the terms of the GNU Affero General Public License as published
@@ -19,145 +19,133 @@
 package service
 
 import (
+       "context"
+       "errors"
        "fmt"
-       "net/http"
-       "sync"
-
-       "gnunet/transport"
+       "gnunet/message"
+       "gnunet/util"
 
        "github.com/bfix/gospel/logger"
 )
 
-// Module is an interface for GNUnet service modules (workers).
-type Module interface {
-       // RPC returns the route and handler for JSON-RPC requests
-       RPC() (string, func(http.ResponseWriter, *http.Request))
+//----------------------------------------------------------------------
+
+// Responder is a back-channel for messages generated during
+// message processing. The Connection type is a responder
+// and used as such in ServeClient().
+type Responder interface {
+       // Handle outgoing message
+       Send(ctx context.Context, msg message.Message) error
+}
+
+// TransportResponder is used as a responder in message handling for
+// messages received from Transport.
+type TransportResponder struct {
+       Peer    *util.PeerID
+       SendFcn func(context.Context, *util.PeerID, message.Message) error
 }
 
-// Service is an interface for GNUnet services. Every service has one channel
-// end-point it listens to for incoming channel requests (network-based
-// channels established by service clients). The end-point is specified in
-// Channel semantics in the specification string.
+// Send a message back to caller.
+func (r *TransportResponder) Send(ctx context.Context, msg message.Message) 
error {
+       if r.SendFcn == nil {
+               return errors.New("no send function defined")
+       }
+       return r.SendFcn(ctx, r.Peer, msg)
+}
+
+//----------------------------------------------------------------------
+
+// Service is an interface for GNUnet services
 type Service interface {
        Module
-       // Start a service on the given endpoint
-       Start(spec string) error
-       // Serve a client session
-       ServeClient(ctx *SessionContext, ch *transport.MsgChannel)
-       // Stop the service
-       Stop() error
+
+       // Serve a client session: A service has a socket it listens to for
+       // incoming connections (sessions) which are used for message exchange
+       // with local GNUnet services or clients.
+       ServeClient(ctx context.Context, id int, mc *Connection)
+
+       // Handle a single incoming message (either locally from a socket
+       // connection or from Transport). Response messages can be send
+       // via a Responder. Returns true if message was processed.
+       HandleMessage(ctx context.Context, msg message.Message, resp Responder) 
bool
 }
 
-// Impl is an implementation of generic service functionality.
-type Impl struct {
-       impl    Service                 // Specific service implementation
-       hdlr    chan transport.Channel  // Channel from listener
-       ctrl    chan bool               // Control channel
-       drop    chan int                // Channel to drop a session from 
pending list
-       srvc    transport.ChannelServer // multi-user service
-       wg      *sync.WaitGroup         // wait group for go routine 
synchronization
-       name    string                  // service name
-       running bool                    // service currently running?
-       pending map[int]*SessionContext // list of pending sessions
+// SocketHandler handles incoming connections on the local service socket.
+// It delegates calls to ServeClient() and HandleMessage() methods
+// to a custom service 'srv'.
+type SocketHandler struct {
+       srv  Service            // Specific service implementation
+       hdlr chan *Connection   // handler for incoming connections
+       cmgr *ConnectionManager // manager for client connections
+       name string             // service name
 }
 
-// NewServiceImpl instantiates a new ServiceImpl object.
-func NewServiceImpl(name string, srv Service) *Impl {
-       return &Impl{
-               impl:    srv,
-               hdlr:    make(chan transport.Channel),
-               ctrl:    make(chan bool),
-               drop:    make(chan int),
-               srvc:    nil,
-               wg:      new(sync.WaitGroup),
-               name:    name,
-               running: false,
-               pending: make(map[int]*SessionContext),
+// NewSocketHandler instantiates a new socket handler.
+func NewSocketHandler(name string, srv Service) *SocketHandler {
+       return &SocketHandler{
+               srv:  srv,
+               hdlr: make(chan *Connection),
+               cmgr: nil,
+               name: name,
        }
 }
 
-// Start a service
-func (si *Impl) Start(spec string) (err error) {
+// Start the socket handler by listening on a Unix domain socket specified
+// by its path and additional parameters. Incoming connections from clients
+// are dispatched to 'hdlr'. Stopped socket handlers can be re-started.
+func (h *SocketHandler) Start(ctx context.Context, path string, params 
map[string]string) (err error) {
        // check if we are already running
-       if si.running {
-               logger.Printf(logger.ERROR, "Service '%s' already running.\n", 
si.name)
+       if h.cmgr != nil {
+               logger.Printf(logger.ERROR, "Service '%s' already running.\n", 
h.name)
                return fmt.Errorf("service already running")
        }
-
-       // start channel server
-       logger.Printf(logger.INFO, "[%s] Service starting.\n", si.name)
-       if si.srvc, err = transport.NewChannelServer(spec, si.hdlr); err != nil 
{
+       // start connection manager
+       logger.Printf(logger.INFO, "[%s] Service starting.\n", h.name)
+       if h.cmgr, err = NewConnectionManager(ctx, path, params, h.hdlr); err 
!= nil {
                return
        }
-       si.running = true
 
-       // handle clients
-       si.wg.Add(1)
+       // handle client connections
        go func() {
-               defer si.wg.Done()
        loop:
-               for si.running {
+               for {
                        select {
 
-                       // handle incoming connections
-                       case in := <-si.hdlr:
-                               if in == nil {
-                                       logger.Printf(logger.INFO, "[%s] 
Listener terminated.\n", si.name)
-                                       break loop
-                               }
-                               switch ch := in.(type) {
-                               case transport.Channel:
-                                       // run a new session with context
-                                       ctx := NewSessionContext()
-                                       sessID := ctx.ID
-                                       si.pending[sessID] = ctx
-                                       logger.Printf(logger.INFO, "[%s] 
Session '%d' started.\n", si.name, sessID)
-
-                                       go func() {
-                                               // serve client on the message 
channel
-                                               si.impl.ServeClient(ctx, 
transport.NewMsgChannel(ch))
-                                               // session is done now.
-                                               logger.Printf(logger.INFO, 
"[%s] Session with client '%d' ended.\n", si.name, sessID)
-                                               si.drop <- sessID
-                                       }()
-                               }
-
-                       // handle session removal
-                       case sessID := <-si.drop:
-                               delete(si.pending, sessID)
-
-                       // handle cancelation signal on listener.
-                       case <-si.ctrl:
+                       // handle incoming connection
+                       case conn := <-h.hdlr:
+                               // run a new session with context
+                               id := util.NextID()
+                               logger.Printf(logger.INFO, "[%s] Session '%d' 
started.\n", h.name, id)
+
+                               go func() {
+                                       // serve client on the message channel
+                                       h.srv.ServeClient(ctx, id, conn)
+                                       // session is done now.
+                                       logger.Printf(logger.INFO, "[%s] 
Session with client '%d' ended.\n", h.name, id)
+                               }()
+
+                       // handle termination
+                       case <-ctx.Done():
+                               logger.Printf(logger.INFO, "[%s] Listener 
terminated.\n", h.name)
                                break loop
                        }
                }
 
-               // terminate pending sessions
-               for _, ctx := range si.pending {
-                       logger.Printf(logger.DBG, "[%s] Session '%d' 
closing...\n", si.name, ctx.ID)
-                       ctx.Cancel()
-               }
-
                // close-down service
-               logger.Printf(logger.INFO, "[%s] Service closing.\n", si.name)
-               si.srvc.Close()
-               si.running = false
+               logger.Printf(logger.INFO, "[%s] Service closing.\n", h.name)
+               h.cmgr.Close()
        }()
-
-       return si.impl.Start(spec)
+       return nil
 }
 
-// Stop a service
-func (si *Impl) Stop() error {
-       if !si.running {
-               logger.Printf(logger.WARN, "Service '%s' not running.\n", 
si.name)
+// Stop socket handler.
+func (h *SocketHandler) Stop() error {
+       if h.cmgr == nil {
+               logger.Printf(logger.WARN, "Service '%s' not running.\n", 
h.name)
                return fmt.Errorf("service not running")
        }
-       si.running = false
-       si.ctrl <- true
-       logger.Printf(logger.INFO, "[%s] Service terminating.\n", si.name)
-
-       err := si.impl.Stop()
-       si.wg.Wait()
-       return err
+       logger.Printf(logger.INFO, "[%s] Service terminating.\n", h.name)
+       h.cmgr.Close()
+       h.cmgr = nil
+       return nil
 }
diff --git a/src/gnunet/service/store.go b/src/gnunet/service/store.go
new file mode 100644
index 0000000..1e5af8b
--- /dev/null
+++ b/src/gnunet/service/store.go
@@ -0,0 +1,379 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package service
+
+import (
+       "context"
+       "database/sql"
+       "encoding/binary"
+       "encoding/hex"
+       "errors"
+       "fmt"
+       "gnunet/crypto"
+       "gnunet/service/dht/blocks"
+       "gnunet/util"
+       "io/ioutil"
+       "os"
+       "strconv"
+       "strings"
+
+       redis "github.com/go-redis/redis/v8"
+)
+
+// Error messages related to the key/value-store implementations
+var (
+       ErrStoreInvalidSpec  = fmt.Errorf("Invalid Store specification")
+       ErrStoreUnknown      = fmt.Errorf("Unknown Store type")
+       ErrStoreNotAvailable = fmt.Errorf("Store not available")
+)
+
+//------------------------------------------------------------
+// Generic storage interface. Can be used for persistent or
+// transient (caching) storage of key/value data.
+// One set of methods (Get/Put) work on DHT queries and blocks,
+// the other set (GetS, PutS) work on key/value strings.
+// Each custom implementation can decide which sets to support.
+//------------------------------------------------------------
+
+// Store is a key/value storage where the type of the key is either
+// a SHA512 hash value or a string and the value is either a DHT
+// block or a string.
+type Store[K, V any] interface {
+       // Put block into storage under given key
+       Put(key K, val V) error
+
+       // Get block with given key from storage
+       Get(key K) (V, error)
+
+       // List all store queries
+       List() ([]K, error)
+}
+
+//------------------------------------------------------------
+// Types for custom store requirements
+//------------------------------------------------------------
+
+// DHTStore for DHT queries and blocks
+type DHTStore Store[blocks.Query, blocks.Block]
+
+// KVStore for key/value string pairs
+type KVStore Store[string, string]
+
+//------------------------------------------------------------
+// NewDHTStore creates a new storage handler with given spec
+// for use with DHT queries and blocks
+func NewDHTStore(spec string) (DHTStore, error) {
+       specs := strings.SplitN(spec, ":", 2)
+       if len(specs) < 2 {
+               return nil, ErrStoreInvalidSpec
+       }
+       switch specs[0] {
+       //------------------------------------------------------------------
+       // File-base storage
+       //------------------------------------------------------------------
+       case "file_store":
+               return NewFileStore(specs[1])
+       case "file_cache":
+               if len(specs) < 3 {
+                       return nil, ErrStoreInvalidSpec
+               }
+               return NewFileCache(specs[1], specs[2])
+       }
+       return nil, ErrStoreUnknown
+}
+
+//------------------------------------------------------------
+// NewKVStore creates a new storage handler with given spec
+// for use with key/value string pairs.
+func NewKVStore(spec string) (KVStore, error) {
+       specs := strings.SplitN(spec, ":", 2)
+       if len(specs) < 2 {
+               return nil, ErrStoreInvalidSpec
+       }
+       switch specs[0] {
+       //--------------------------------------------------------------
+       // Redis service
+       //--------------------------------------------------------------
+       case "redis":
+               if len(specs) < 4 {
+                       return nil, ErrStoreInvalidSpec
+               }
+               return NewRedisStore(specs[1], specs[2], specs[3])
+
+       //--------------------------------------------------------------
+       // SQL database service
+       //--------------------------------------------------------------
+       case "sql":
+               if len(specs) < 4 {
+                       return nil, ErrStoreInvalidSpec
+               }
+               return NewSQLStore(specs[1])
+       }
+       return nil, errors.New("unknown storage mechanism")
+}
+
+//------------------------------------------------------------
+// Filesystem-based storage
+//------------------------------------------------------------
+
+// FileStore implements a filesystem-based storage mechanism for
+// DHT queries and blocks.
+type FileStore struct {
+       path   string             // storage path
+       cached []*crypto.HashCode // list of cached entries (key)
+       wrPos  int                // write position in cyclic list
+}
+
+// NewFileStore instantiates a new file storage.
+func NewFileStore(path string) (DHTStore, error) {
+       // create file store
+       return &FileStore{
+               path: path,
+       }, nil
+}
+
+// NewFileCache instantiates a new file-based cache.
+func NewFileCache(path, param string) (DHTStore, error) {
+       // remove old cache content
+       os.RemoveAll(path)
+
+       // get number of cache entries
+       num, err := strconv.ParseUint(param, 10, 32)
+       if err != nil {
+               return nil, err
+       }
+       // create file store
+       return &FileStore{
+               path:   path,
+               cached: make([]*crypto.HashCode, num),
+               wrPos:  0,
+       }, nil
+}
+
+// Put block into storage under given key
+func (s *FileStore) Put(query blocks.Query, block blocks.Block) (err error) {
+       // get query parameters for entry
+       var (
+               btype  uint16            // block type
+               expire util.AbsoluteTime // block expiration
+       )
+       query.Get("blkType", &btype)
+       query.Get("expire", &expire)
+
+       // are we in caching mode?
+       if s.cached != nil {
+               // release previous block if defined
+               if key := s.cached[s.wrPos]; key != nil {
+                       // get path and filename from key
+                       path, fname := s.expandPath(key)
+                       if err = os.Remove(path + "/" + fname); err != nil {
+                               return
+                       }
+                       // free slot
+                       s.cached[s.wrPos] = nil
+               }
+       }
+       // get path and filename from key
+       path, fname := s.expandPath(query.Key())
+       // make sure the path exists
+       if err = os.MkdirAll(path, 0755); err != nil {
+               return
+       }
+       // write to file for storage
+       var fp *os.File
+       if fp, err = os.Create(path + "/" + fname); err == nil {
+               defer fp.Close()
+               // write block data
+               if err = binary.Write(fp, binary.BigEndian, btype); err == nil {
+                       if err = binary.Write(fp, binary.BigEndian, expire); 
err == nil {
+                               _, err = fp.Write(block.Data())
+                       }
+               }
+       }
+       // update cache list
+       if s.cached != nil {
+               s.cached[s.wrPos] = query.Key()
+               s.wrPos = (s.wrPos + 1) % len(s.cached)
+       }
+       return
+}
+
+// Get block with given key from storage
+func (s *FileStore) Get(query blocks.Query) (block blocks.Block, err error) {
+       // get requested block type
+       var (
+               btype  uint16            = blocks.DHT_BLOCK_ANY
+               blkt   uint16            // actual block type
+               expire util.AbsoluteTime // expiration date
+               data   []byte            // block data
+       )
+       query.Get("blkType", &btype)
+
+       // get path and filename from key
+       path, fname := s.expandPath(query.Key())
+       // read file content (block data)
+       var file *os.File
+       if file, err = os.Open(path + "/" + fname); err != nil {
+               return
+       }
+       // read block data
+       if err = binary.Read(file, binary.BigEndian, &blkt); err == nil {
+               if btype != blocks.DHT_BLOCK_ANY && btype != blkt {
+                       // block types not matching
+                       return
+               }
+               if err = binary.Read(file, binary.BigEndian, &expire); err == 
nil {
+                       if data, err = ioutil.ReadAll(file); err == nil {
+                               block = blocks.NewGenericBlock(data)
+                       }
+               }
+       }
+       return
+}
+
+// Get a list of all stored block keys (generic query).
+func (s *FileStore) List() ([]blocks.Query, error) {
+       return make([]blocks.Query, 0), nil
+}
+
+// expandPath returns the full path to the file for given key.
+func (s *FileStore) expandPath(key *crypto.HashCode) (string, string) {
+       h := hex.EncodeToString(key.Bits)
+       return fmt.Sprintf("%s/%s/%s", s.path, h[:2], h[2:4]), h[4:]
+}
+
+//------------------------------------------------------------
+// Redis: only use for caching purposes on key/value strings
+//------------------------------------------------------------
+
+// RedisStore uses a (local) Redis server for key/value storage
+type RedisStore struct {
+       client *redis.Client // client connection
+       db     int           // index to database
+}
+
+// NewRedisStore creates a Redis service client instance.
+func NewRedisStore(addr, passwd, db string) (s KVStore, err error) {
+       kvs := new(RedisStore)
+       if kvs.db, err = strconv.Atoi(db); err != nil {
+               err = ErrStoreInvalidSpec
+               return
+       }
+       kvs.client = redis.NewClient(&redis.Options{
+               Addr:     addr,
+               Password: passwd,
+               DB:       kvs.db,
+       })
+       if kvs.client == nil {
+               err = ErrStoreNotAvailable
+       }
+       s = kvs
+       return
+}
+
+// Put block into storage under given key
+func (s *RedisStore) Put(key string, value string) (err error) {
+       return s.client.Set(context.TODO(), key, value, 0).Err()
+}
+
+// Get block with given key from storage
+func (s *RedisStore) Get(key string) (value string, err error) {
+       return s.client.Get(context.TODO(), key).Result()
+}
+
+// List all keys in store
+func (s *RedisStore) List() (keys []string, err error) {
+       var (
+               crs  uint64
+               segm []string
+               ctx  = context.TODO()
+       )
+       keys = make([]string, 0)
+       for {
+               segm, crs, err = s.client.Scan(ctx, crs, "*", 10).Result()
+               if err != nil {
+                       return
+               }
+               if crs == 0 {
+                       break
+               }
+               keys = append(keys, segm...)
+       }
+       return
+}
+
+//------------------------------------------------------------
+// SQL-based key-value-store
+//------------------------------------------------------------
+
+// SQLStore for generic SQL database handling
+type SQLStore struct {
+       db *util.DbConn
+}
+
+// NewSQLStore creates a new SQL-based key/value store.
+func NewSQLStore(spec string) (s KVStore, err error) {
+       kvs := new(SQLStore)
+
+       // connect to SQL database
+       kvs.db, err = util.DbPool.Connect(spec)
+       if err != nil {
+               return nil, err
+       }
+       // get number of key/value pairs (as a check for existing table)
+       row := kvs.db.QueryRow("select count(*) from store")
+       var num int
+       if row.Scan(&num) != nil {
+               return nil, ErrStoreNotAvailable
+       }
+       return kvs, nil
+
+}
+
+// Put a key/value pair into the store
+func (s *SQLStore) Put(key string, value string) error {
+       _, err := s.db.Exec("insert into store(key,value) values(?,?)", key, 
value)
+       return err
+}
+
+// Get a value for a given key from store
+func (s *SQLStore) Get(key string) (value string, err error) {
+       row := s.db.QueryRow("select value from store where key=?", key)
+       err = row.Scan(&value)
+       return
+}
+
+// List all keys in store
+func (s *SQLStore) List() (keys []string, err error) {
+       var (
+               rows *sql.Rows
+               key  string
+       )
+       keys = make([]string, 0)
+       rows, err = s.db.Query("select key from store")
+       if err == nil {
+               for rows.Next() {
+                       if err = rows.Scan(&key); err != nil {
+                               break
+                       }
+                       keys = append(keys, key)
+               }
+       }
+       return
+}
diff --git a/src/gnunet/test.sh b/src/gnunet/test.sh
new file mode 100755
index 0000000..78495a8
--- /dev/null
+++ b/src/gnunet/test.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+go test $* -gcflags "-N -l" ./...
diff --git a/src/gnunet/transport/channel.go b/src/gnunet/transport/channel.go
deleted file mode 100644
index 1632aab..0000000
--- a/src/gnunet/transport/channel.go
+++ /dev/null
@@ -1,213 +0,0 @@
-// This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
-//
-// gnunet-go is free software: you can redistribute it and/or modify it
-// under the terms of the GNU Affero General Public License as published
-// by the Free Software Foundation, either version 3 of the License,
-// or (at your option) any later version.
-//
-// gnunet-go is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-// Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <http://www.gnu.org/licenses/>.
-//
-// SPDX-License-Identifier: AGPL3.0-or-later
-
-package transport
-
-import (
-       "encoding/hex"
-       "errors"
-       "fmt"
-       "path"
-       "strings"
-
-       "gnunet/message"
-       "gnunet/util"
-
-       "github.com/bfix/gospel/concurrent"
-       "github.com/bfix/gospel/data"
-       "github.com/bfix/gospel/logger"
-)
-
-// Error codes
-var (
-       ErrChannelNotImplemented = fmt.Errorf("protocol not implemented")
-       ErrChannelNotOpened      = fmt.Errorf("channel not opened")
-       ErrChannelInterrupted    = fmt.Errorf("channel interrupted")
-)
-
-////////////////////////////////////////////////////////////////////////
-// CHANNEL
-
-// Channel is an abstraction for exchanging arbitrary data over various
-// transport protocols and mechanisms. They are created by clients via
-// 'NewChannel()' or by services run via 'NewChannelServer()'.
-// A string specifies the end-point of the channel:
-//     "unix+/tmp/test.sock" -- for UDS channels
-//     "tcp+1.2.3.4:5"       -- for TCP channels
-//     "udp+1.2.3.4:5"       -- for UDP channels
-type Channel interface {
-       Open(spec string) error                           // open channel (for 
read/write)
-       Close() error                                     // close open channel
-       IsOpen() bool                                     // check if channel 
is open
-       Read([]byte, *concurrent.Signaller) (int, error)  // read from channel
-       Write([]byte, *concurrent.Signaller) (int, error) // write to channel
-}
-
-// ChannelFactory instantiates specific Channel imülementations.
-type ChannelFactory func() Channel
-
-// Known channel implementations.
-var channelImpl = map[string]ChannelFactory{
-       "unix": NewSocketChannel,
-       "tcp":  NewTCPChannel,
-       "udp":  NewUDPChannel,
-}
-
-// NewChannel creates a new channel to the specified endpoint.
-// Called by a client to connect to a service.
-func NewChannel(spec string) (Channel, error) {
-       parts := strings.Split(spec, "+")
-       if fac, ok := channelImpl[parts[0]]; ok {
-               inst := fac()
-               err := inst.Open(spec)
-               return inst, err
-       }
-       return nil, ErrChannelNotImplemented
-}
-
-////////////////////////////////////////////////////////////////////////
-// CHANNEL SERVER
-
-// ChannelServer creates a listener for the specified endpoint.
-// The specification string has the same format as for Channel with slightly
-// different semantics (for TCP, and ICMP the address specifies is a mask
-// for client addresses accepted for a channel request).
-type ChannelServer interface {
-       Open(spec string, hdlr chan<- Channel) error
-       Close() error
-}
-
-// ChannelServerFactory instantiates specific ChannelServer imülementations.
-type ChannelServerFactory func() ChannelServer
-
-// Known channel server implementations.
-var channelServerImpl = map[string]ChannelServerFactory{
-       "unix": NewSocketChannelServer,
-       "tcp":  NewTCPChannelServer,
-       "udp":  NewUDPChannelServer,
-}
-
-// NewChannelServer creates a new channel server instance
-func NewChannelServer(spec string, hdlr chan<- Channel) (cs ChannelServer, err 
error) {
-       parts := strings.Split(spec, "+")
-
-       if fac, ok := channelServerImpl[parts[0]]; ok {
-               // check if the basedir for the spec exists...
-               if err = util.EnforceDirExists(path.Dir(parts[1])); err != nil {
-                       return
-               }
-               // instantiate server implementation
-               cs = fac()
-               // create the domain socket and listen to it.
-               err = cs.Open(spec, hdlr)
-               return
-       }
-       return nil, ErrChannelNotImplemented
-}
-
-////////////////////////////////////////////////////////////////////////
-// MESSAGE CHANNEL
-
-// MsgChannel s a wrapper around a generic channel for GNUnet message exchange.
-type MsgChannel struct {
-       ch  Channel
-       buf []byte
-}
-
-// NewMsgChannel wraps a plain Channel for GNUnet message exchange.
-func NewMsgChannel(ch Channel) *MsgChannel {
-       return &MsgChannel{
-               ch:  ch,
-               buf: make([]byte, 65536),
-       }
-}
-
-// Close a MsgChannel by closing the wrapped plain Channel.
-func (c *MsgChannel) Close() error {
-       return c.ch.Close()
-}
-
-// Send a GNUnet message over a channel.
-func (c *MsgChannel) Send(msg message.Message, sig *concurrent.Signaller) 
error {
-       // convert message to binary data
-       data, err := data.Marshal(msg)
-       if err != nil {
-               return err
-       }
-       logger.Printf(logger.DBG, "==> %v\n", msg)
-       logger.Printf(logger.DBG, "    [%s]\n", hex.EncodeToString(data))
-
-       // check message header size and packet size
-       mh, err := message.GetMsgHeader(data)
-       if err != nil {
-               return err
-       }
-       if len(data) != int(mh.MsgSize) {
-               return errors.New("send: message size mismatch")
-       }
-
-       // send packet
-       n, err := c.ch.Write(data, sig)
-       if err != nil {
-               return err
-       }
-       if n != len(data) {
-               return errors.New("incomplete send")
-       }
-       return nil
-}
-
-// Receive GNUnet messages over a plain Channel.
-func (c *MsgChannel) Receive(sig *concurrent.Signaller) (message.Message, 
error) {
-       // get bytes from channel
-       get := func(pos, count int) error {
-               n, err := c.ch.Read(c.buf[pos:pos+count], sig)
-               if err != nil {
-                       return err
-               }
-               if n != count {
-                       return errors.New("not enough bytes on network")
-               }
-               return nil
-       }
-
-       if err := get(0, 4); err != nil {
-               return nil, err
-       }
-       mh, err := message.GetMsgHeader(c.buf[:4])
-       if err != nil {
-               return nil, err
-       }
-
-       if err := get(4, int(mh.MsgSize)-4); err != nil {
-               return nil, err
-       }
-       msg, err := message.NewEmptyMessage(mh.MsgType)
-       if err != nil {
-               return nil, err
-       }
-       if msg == nil {
-               return nil, fmt.Errorf("message{%d} is nil", mh.MsgType)
-       }
-       if err = data.Unmarshal(msg, c.buf[:mh.MsgSize]); err != nil {
-               return nil, err
-       }
-       logger.Printf(logger.DBG, "<== %v\n", msg)
-       logger.Printf(logger.DBG, "    [%s]\n", 
hex.EncodeToString(c.buf[:mh.MsgSize]))
-       return msg, nil
-}
diff --git a/src/gnunet/transport/channel_netw.go 
b/src/gnunet/transport/channel_netw.go
deleted file mode 100644
index c0de978..0000000
--- a/src/gnunet/transport/channel_netw.go
+++ /dev/null
@@ -1,285 +0,0 @@
-// This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
-//
-// gnunet-go is free software: you can redistribute it and/or modify it
-// under the terms of the GNU Affero General Public License as published
-// by the Free Software Foundation, either version 3 of the License,
-// or (at your option) any later version.
-//
-// gnunet-go is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-// Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <http://www.gnu.org/licenses/>.
-//
-// SPDX-License-Identifier: AGPL3.0-or-later
-
-package transport
-
-import (
-       "net"
-       "os"
-       "strconv"
-       "strings"
-
-       "github.com/bfix/gospel/concurrent"
-       "github.com/bfix/gospel/logger"
-)
-
-// ChannelResult for read/write operations on channels.
-type ChannelResult struct {
-       count int   // number of bytes read/written
-       err   error // error (or nil)
-}
-
-// NewChannelResult instanciates a new object with given attributes.
-func NewChannelResult(n int, err error) *ChannelResult {
-       return &ChannelResult{
-               count: n,
-               err:   err,
-       }
-}
-
-// Values returns the attributes of a result instance (for passing up the
-// call stack).
-func (cr *ChannelResult) Values() (int, error) {
-       return cr.count, cr.err
-}
-
-////////////////////////////////////////////////////////////////////////
-// Generic network-based Channel
-
-// NetworkChannel represents a network-based channel
-type NetworkChannel struct {
-       network string   // network protocol identifier ("tcp", "unix", ...)
-       conn    net.Conn // associated connection
-}
-
-// NewNetworkChannel creates a new channel for a given network protocol.
-// The channel is in pending state and need to be opened before use.
-func NewNetworkChannel(netw string) Channel {
-       return &NetworkChannel{
-               network: netw,
-               conn:    nil,
-       }
-}
-
-// Open a network channel based on specification:
-// The specification is a string separated into parts by the '+' delimiter
-// (e.g. "unix+/tmp/gnunet-service-gns-go.sock+perm=0770"). The network
-// identifier (first part) must match the network specification of the
-// underlaying NetworkChannel instance.
-func (c *NetworkChannel) Open(spec string) (err error) {
-       parts := strings.Split(spec, "+")
-       // check for correct protocol
-       if parts[0] != c.network {
-               return ErrChannelNotImplemented
-       }
-       // open connection
-       c.conn, err = net.Dial(c.network, parts[1])
-       return
-}
-
-// Close a network channel
-func (c *NetworkChannel) Close() error {
-       if c.conn != nil {
-               rc := c.conn.Close()
-               c.conn = nil
-               return rc
-       }
-       return ErrChannelNotOpened
-}
-
-// IsOpen returns true if the channel is opened
-func (c *NetworkChannel) IsOpen() bool {
-       return c.conn != nil
-}
-
-// Read bytes from a network channel into buffer: Returns the number of read
-// bytes and an error code. Only works on open channels ;)
-// The read can be aborted by sending 'true' on the cmd interface; the
-// channel is closed after such interruption.
-func (c *NetworkChannel) Read(buf []byte, sig *concurrent.Signaller) (int, 
error) {
-       // check if the channel is open
-       if c.conn == nil {
-               return 0, ErrChannelNotOpened
-       }
-       // perform operation in go-routine
-       result := make(chan *ChannelResult)
-       go func() {
-               result <- NewChannelResult(c.conn.Read(buf))
-       }()
-
-       listener := sig.Listen()
-       defer sig.Drop(listener)
-       for {
-               select {
-               // handle terminate command
-               case x := <-listener:
-                       switch val := x.(type) {
-                       case bool:
-                               if val {
-                                       return 0, ErrChannelInterrupted
-                               }
-                       }
-               // handle result of read operation
-               case res := <-result:
-                       return res.Values()
-               }
-       }
-}
-
-// Write buffer to a network channel: Returns the number of written bytes and
-// an error code. The write operation can be aborted by sending 'true' on the
-// command channel; the network channel is closed after such interrupt.
-func (c *NetworkChannel) Write(buf []byte, sig *concurrent.Signaller) (int, 
error) {
-       // check if we have an open channel to write to.
-       if c.conn == nil {
-               return 0, ErrChannelNotOpened
-       }
-       // perform operation in go-routine
-       result := make(chan *ChannelResult)
-       go func() {
-               result <- NewChannelResult(c.conn.Write(buf))
-       }()
-
-       listener := sig.Listen()
-       defer sig.Drop(listener)
-       for {
-               select {
-               // handle terminate command
-               case x := <-listener:
-                       switch val := x.(type) {
-                       case bool:
-                               if val {
-                                       return 0, ErrChannelInterrupted
-                               }
-                       }
-               // handle result of read operation
-               case res := <-result:
-                       return res.Values()
-               }
-       }
-}
-
-////////////////////////////////////////////////////////////////////////
-// Generic network-based ChannelServer
-
-// NetworkChannelServer represents a network-based channel server
-type NetworkChannelServer struct {
-       network  string       // network protocol to listen on
-       listener net.Listener // reference to listener object
-}
-
-// NewNetworkChannelServer creates a new network-based channel server
-func NewNetworkChannelServer(netw string) ChannelServer {
-       return &NetworkChannelServer{
-               network:  netw,
-               listener: nil,
-       }
-}
-
-// Open a network channel server (= start running it) based on the given
-// specification. For every client connection to the server, the associated
-// network channel for the connection is send via the hdlr channel.
-func (s *NetworkChannelServer) Open(spec string, hdlr chan<- Channel) (err 
error) {
-       parts := strings.Split(spec, "+")
-       // check for correct protocol
-       if parts[0] != s.network {
-               return ErrChannelNotImplemented
-       }
-       // create listener
-       if s.listener, err = net.Listen(s.network, parts[1]); err != nil {
-               return
-       }
-       // handle additional parameters ('key[=value]')
-       for _, param := range parts[2:] {
-               frag := strings.Split(param, "=")
-               switch frag[0] {
-               case "perm": // set permissions on 'unix'
-                       if s.network == "unix" {
-                               if perm, err := strconv.ParseInt(frag[1], 8, 
32); err == nil {
-                                       if err := os.Chmod(parts[1], 
os.FileMode(perm)); err != nil {
-                                               logger.Printf(
-                                                       logger.ERROR,
-                                                       "NetworkChannelServer: 
Failed to set permissions: %s\n",
-                                                       err.Error())
-
-                                       }
-                               } else {
-                                       logger.Printf(
-                                               logger.ERROR,
-                                               "NetworkChannelServer: Invalid 
permissions '%s'\n",
-                                               frag[1])
-                               }
-                       }
-               }
-       }
-       // run go routine to handle channel requests from clients
-       go func() {
-               for {
-                       conn, err := s.listener.Accept()
-                       if err != nil {
-                               // signal failure and terminate
-                               hdlr <- nil
-                               break
-                       }
-                       // send channel to handler
-                       hdlr <- &NetworkChannel{
-                               network: s.network,
-                               conn:    conn,
-                       }
-               }
-               if s.listener != nil {
-                       s.listener.Close()
-               }
-       }()
-
-       return nil
-}
-
-// Close a network channel server (= stop the server)
-func (s *NetworkChannelServer) Close() error {
-       if s.listener != nil {
-               err := s.listener.Close()
-               s.listener = nil
-               return err
-       }
-       return nil
-}
-
-////////////////////////////////////////////////////////////////////////
-// helper functions to instantiate network channels and servers for
-// common network protocols
-
-// NewSocketChannel implements a Unix Domain Socket connection
-func NewSocketChannel() Channel {
-       return NewNetworkChannel("unix")
-}
-
-// NewTCPChannel implements a: TCP connection
-func NewTCPChannel() Channel {
-       return NewNetworkChannel("tcp")
-}
-
-// NewUDPChannel implements an UDP connection
-func NewUDPChannel() Channel {
-       return NewNetworkChannel("udp")
-}
-
-// NewSocketChannelServer implements an Unix Domain Socket listener
-func NewSocketChannelServer() ChannelServer {
-       return NewNetworkChannelServer("unix")
-}
-
-// NewTCPChannelServer implements a TCP listener
-func NewTCPChannelServer() ChannelServer {
-       return NewNetworkChannelServer("tcp")
-}
-
-// NewUDPChannelServer implements an UDP listener
-func NewUDPChannelServer() ChannelServer {
-       return NewNetworkChannelServer("udp")
-}
diff --git a/src/gnunet/transport/channel_test.go 
b/src/gnunet/transport/channel_test.go
deleted file mode 100644
index f028171..0000000
--- a/src/gnunet/transport/channel_test.go
+++ /dev/null
@@ -1,232 +0,0 @@
-// This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
-//
-// gnunet-go is free software: you can redistribute it and/or modify it
-// under the terms of the GNU Affero General Public License as published
-// by the Free Software Foundation, either version 3 of the License,
-// or (at your option) any later version.
-//
-// gnunet-go is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-// Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <http://www.gnu.org/licenses/>.
-//
-// SPDX-License-Identifier: AGPL3.0-or-later
-
-package transport
-
-import (
-       "bytes"
-       "fmt"
-       "testing"
-       "time"
-
-       "github.com/bfix/gospel/concurrent"
-)
-
-// TODO: These test cases fail from time to time for no obvious reason.
-// This needs to be investigated.
-
-const (
-       SockAddr      = "/tmp/gnunet-go-test.sock"
-       TCPAddrClient = "gnunet.org:80"
-       TCPAddrServer = "127.0.0.1:9876"
-)
-
-type TestChannelServer struct {
-       hdlr    chan Channel
-       srvc    ChannelServer
-       running bool
-}
-
-func NewTestChannelServer() *TestChannelServer {
-       return &TestChannelServer{
-               hdlr:    make(chan Channel),
-               srvc:    nil,
-               running: false,
-       }
-}
-
-func (s *TestChannelServer) handle(ch Channel, sig *concurrent.Signaller) {
-       buf := make([]byte, 4096)
-       for {
-               n, err := ch.Read(buf, sig)
-               if err != nil {
-                       break
-               }
-               _, err = ch.Write(buf[:n], sig)
-               if err != nil {
-                       break
-               }
-       }
-       ch.Close()
-}
-
-func (s *TestChannelServer) Start(spec string) (err error) {
-       // check if we are already running
-       if s.running {
-               return fmt.Errorf("Server already running")
-       }
-
-       // start channel server
-       if s.srvc, err = NewChannelServer(spec, s.hdlr); err != nil {
-               return
-       }
-       s.running = true
-
-       // handle clients
-       sig := concurrent.NewSignaller()
-       go func() {
-               for s.running {
-                       in := <-s.hdlr
-                       if in == nil {
-                               break
-                       }
-                       switch x := in.(type) {
-                       case Channel:
-                               go s.handle(x, sig)
-                       }
-               }
-               s.srvc.Close()
-               s.running = false
-       }()
-       return nil
-}
-
-func (s *TestChannelServer) Stop() {
-       s.running = false
-}
-
-func TestChannelServerTCPSingle(t *testing.T) {
-       time.Sleep(time.Second)
-       s := NewTestChannelServer()
-       err := s.Start("tcp+" + TCPAddrServer)
-       defer s.Stop()
-       if err != nil {
-               t.Fatal(err)
-       }
-}
-
-func TestChannelServerTCPTwice(t *testing.T) {
-       time.Sleep(time.Second)
-       s1 := NewTestChannelServer()
-       err := s1.Start("tcp+" + TCPAddrServer)
-       defer s1.Stop()
-       if err != nil {
-               t.Fatal(err)
-       }
-       time.Sleep(time.Second)
-       s2 := NewTestChannelServer()
-       err = s2.Start("tcp+" + TCPAddrServer)
-       defer s2.Stop()
-       if err == nil {
-               t.Fatal("SocketServer started twice!!")
-       }
-}
-
-func TestChannelClientTCP(t *testing.T) {
-       time.Sleep(time.Second)
-       ch, err := NewChannel("tcp+" + TCPAddrClient)
-       if err != nil {
-               t.Fatal(err)
-       }
-       defer ch.Close()
-
-       sig := concurrent.NewSignaller()
-       msg := []byte("GET /\n\n")
-       n, err := ch.Write(msg, sig)
-       if err != nil {
-               t.Fatal(err)
-       }
-       if n != len(msg) {
-               t.Fatal("Send size mismatch")
-       }
-       buf := make([]byte, 4096)
-       n = 0
-       start := time.Now().Unix()
-       for n == 0 && (time.Now().Unix()-start) < 3 {
-               if n, err = ch.Read(buf, sig); err != nil {
-                       t.Fatal(err)
-               }
-       }
-       t.Logf("'%s' [%d]\n", string(buf[:n]), n)
-}
-
-func TestChannelClientServerTCP(t *testing.T) {
-       time.Sleep(time.Second)
-       s := NewTestChannelServer()
-       err := s.Start("tcp+" + TCPAddrServer)
-       defer s.Stop()
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       ch, err := NewChannel("tcp+" + TCPAddrServer)
-       if err != nil {
-               t.Fatal(err)
-       }
-       sig := concurrent.NewSignaller()
-       msg := []byte("GET /\n\n")
-       n, err := ch.Write(msg, sig)
-       if err != nil {
-               t.Fatal(err)
-       }
-       if n != len(msg) {
-               t.Fatal("Send size mismatch")
-       }
-       buf := make([]byte, 4096)
-       n = 0
-       start := time.Now().Unix()
-       for n == 0 && (time.Now().Unix()-start) < 3 {
-               if n, err = ch.Read(buf, sig); err != nil {
-                       t.Fatal(err)
-               }
-       }
-       if err = ch.Close(); err != nil {
-               t.Fatal(err)
-       }
-       if !bytes.Equal(buf[:n], msg) {
-               t.Fatal("message send/receive mismatch")
-       }
-}
-
-func TestChannelClientServerSock(t *testing.T) {
-       time.Sleep(time.Second)
-       s := NewTestChannelServer()
-       if err := s.Start("unix+" + SockAddr); err != nil {
-               t.Fatal(err)
-       }
-
-       ch, err := NewChannel("unix+" + SockAddr)
-       if err != nil {
-               t.Fatal(err)
-       }
-       sig := concurrent.NewSignaller()
-       msg := []byte("This is just a test -- please ignore...")
-       n, err := ch.Write(msg, sig)
-       if err != nil {
-               t.Fatal(err)
-       }
-       if n != len(msg) {
-               t.Fatal("Send size mismatch")
-       }
-       buf := make([]byte, 4096)
-       n = 0
-       start := time.Now().Unix()
-       for n == 0 && (time.Now().Unix()-start) < 3 {
-               if n, err = ch.Read(buf, sig); err != nil {
-                       t.Fatal(err)
-               }
-       }
-       if err = ch.Close(); err != nil {
-               t.Fatal(err)
-       }
-       if !bytes.Equal(buf[:n], msg) {
-               t.Fatal("message send/receive mismatch")
-       }
-
-       s.Stop()
-}
diff --git a/src/gnunet/transport/connection.go 
b/src/gnunet/transport/connection.go
index cc2c909..8337260 100644
--- a/src/gnunet/transport/connection.go
+++ b/src/gnunet/transport/connection.go
@@ -1,5 +1,5 @@
 // This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
 //
 // gnunet-go is free software: you can redistribute it and/or modify it
 // under the terms of the GNU Affero General Public License as published
@@ -19,59 +19,97 @@
 package transport
 
 import (
-       "gnunet/core"
+       "context"
+       "errors"
        "gnunet/message"
+       "net"
+)
 
-       "github.com/bfix/gospel/concurrent"
+// Error codes
+var (
+       ErrConnectionNotOpened   = errors.New("connection not opened")
+       ErrConnectionInterrupted = errors.New("connection interrupted")
 )
 
-// Connection for communicating peers
+//----------------------------------------------------------------------
+
+// Connection is a net.Conn for GNUnet message exchange (send/receive)
 type Connection struct {
-       from, to  *core.Peer
-       ch        *MsgChannel
-       bandwidth uint32
-       state     int
-       shared    []byte
+       conn net.Conn // associated connection
+       buf  []byte   // read/write buffer
 }
 
-// NewConnection instanciates a new connection between peers communicating
-// over a message channel (Connections are authenticated and secured).
-func NewConnection(ch *MsgChannel, from, to *core.Peer) *Connection {
+// NewConnection creates a new connection from an existing net.Conn
+// This is usually used by clients to connect to a service.
+func NewConnection(ctx context.Context, conn net.Conn) *Connection {
        return &Connection{
-               from:  from,
-               to:    to,
-               state: 1,
-               ch:    ch,
+               conn: conn,
+               buf:  make([]byte, 65536),
        }
 }
 
-// SharedSecret computes the shared secret the two endpoints of a connection.
-func (c *Connection) SharedSecret(secret []byte) {
-       c.shared = make([]byte, len(secret))
-       copy(c.shared, secret)
+// Close connection
+func (s *Connection) Close() error {
+       if s.conn != nil {
+               rc := s.conn.Close()
+               s.conn = nil
+               return rc
+       }
+       return ErrConnectionNotOpened
 }
 
-// GetState returns the current state of the connection.
-func (c *Connection) GetState() int {
-       return c.state
+// Send a GNUnet message over connection
+func (s *Connection) Send(ctx context.Context, msg message.Message) error {
+       return WriteMessage(ctx, s.conn, msg)
 }
 
-// SetBandwidth to control transfer rates on the connection
-func (c *Connection) SetBandwidth(bw uint32) {
-       c.bandwidth = bw
+// Receive GNUnet messages from socket.
+func (s *Connection) Receive(ctx context.Context) (message.Message, error) {
+       return ReadMessage(ctx, s.conn, s.buf)
 }
 
-// Close connection between two peers.
-func (c *Connection) Close() error {
-       return c.ch.Close()
+//----------------------------------------------------------------------
+
+// ConnectionManager handles client connections on a net.Listener
+type ConnectionManager struct {
+       listener net.Listener // reference to listener object
 }
 
-// Send a message on the connection
-func (c *Connection) Send(msg message.Message, sig *concurrent.Signaller) 
error {
-       return c.ch.Send(msg, sig)
+// NewConnectionManager creates a new net.Listener connection manager.
+// Incoming connections from clients are dispatched to a handler channel.
+func NewConnectionManager(ctx context.Context, listener net.Listener, hdlr 
chan *Connection) (cs *ConnectionManager, err error) {
+       // instantiate connection manager
+       cs = &ConnectionManager{
+               listener: listener,
+       }
+       // run watch dog for termination
+       go func() {
+               <-ctx.Done()
+               cs.listener.Close()
+       }()
+       // run go routine to handle channel requests from clients
+       go func() {
+               for {
+                       conn, err := cs.listener.Accept()
+                       if err != nil {
+                               return
+                       }
+                       // handle connection
+                       c := &Connection{
+                               conn: conn,
+                               buf:  make([]byte, 65536),
+                       }
+                       hdlr <- c
+               }
+       }()
+       return cs, nil
 }
 
-// Receive a message on the connection
-func (c *Connection) Receive(sig *concurrent.Signaller) (message.Message, 
error) {
-       return c.ch.Receive(sig)
+// Close a connection manager (= stop the server)
+func (s *ConnectionManager) Close() (err error) {
+       if s.listener != nil {
+               err = s.listener.Close()
+               s.listener = nil
+       }
+       return
 }
diff --git a/src/gnunet/transport/endpoint.go b/src/gnunet/transport/endpoint.go
new file mode 100644
index 0000000..b54ee4e
--- /dev/null
+++ b/src/gnunet/transport/endpoint.go
@@ -0,0 +1,282 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package transport
+
+import (
+       "bytes"
+       "context"
+       "errors"
+       "gnunet/message"
+       "gnunet/util"
+       "net"
+)
+
+var (
+       ErrEndpNotAvailable     = errors.New("no endpoint for address 
available")
+       ErrEndpProtocolMismatch = errors.New("transport protocol mismatch")
+)
+
+// Endpoint represents a local endpoint that can send and receive messages.
+// Implementations need to manage the relations between peer IDs and
+// remote endpoints for TCP and UDP traffic.
+type Endpoint interface {
+       // Run the endpoint and send received messages to channel
+       Run(context.Context, chan *TransportMessage) error
+
+       // Send message on endpoint
+       Send(context.Context, net.Addr, *TransportMessage) error
+
+       // Address returns the listening address for the endpoint
+       Address() net.Addr
+
+       // CanSendTo returns true if the endpoint can sent to address
+       CanSendTo(net.Addr) bool
+
+       // Return endpoint identifier
+       ID() int
+}
+
+//----------------------------------------------------------------------
+
+// NewEndpoint returns a suitable endpoint for the address.
+func NewEndpoint(addr net.Addr) (ep Endpoint, err error) {
+       switch epMode(addr.Network()) {
+       case "packet":
+               ep, err = newPacketEndpoint(addr)
+       case "stream":
+               ep, err = newStreamEndpoint(addr)
+       default:
+               err = ErrEndpNotAvailable
+       }
+       return
+}
+
+//----------------------------------------------------------------------
+// Packet-oriented endpoint
+//----------------------------------------------------------------------
+
+// PacketEndpoint for packet-oriented network protocols
+type PaketEndpoint struct {
+       id   int            // endpoint identifier
+       addr net.Addr       // endpoint address
+       conn net.PacketConn // packet connection
+       buf  []byte         // buffer for read/write operations
+}
+
+// Run packet endpoint: send incoming messages to the handler.
+func (ep *PaketEndpoint) Run(ctx context.Context, hdlr chan *TransportMessage) 
(err error) {
+       // create listener
+       var lc net.ListenConfig
+       if ep.conn, err = lc.ListenPacket(ctx, ep.addr.Network(), 
ep.addr.String()); err != nil {
+               return
+       }
+
+       // run watch dog for termination
+       go func() {
+               <-ctx.Done()
+               ep.conn.Close()
+       }()
+       // run go routine to handle messages from clients
+       go func() {
+               for {
+                       // read next message from packet
+                       n, _, err := ep.conn.ReadFrom(ep.buf)
+                       if err != nil {
+                               break
+                       }
+                       rdr := bytes.NewBuffer(util.Clone(ep.buf[:n]))
+                       msg, err := ReadMessageDirect(rdr, ep.buf)
+                       if err != nil {
+                               break
+                       }
+                       // check for transport message
+                       if msg.Header().MsgType == message.DUMMY {
+                               // set transient attributes
+                               tm := msg.(*TransportMessage)
+                               tm.endp = ep.id
+                               tm.conn = 0
+                               // send to handler
+                               go func() {
+                                       hdlr <- tm
+                               }()
+                       }
+               }
+               // connection ended.
+               ep.conn.Close()
+       }()
+       return
+}
+
+// Send message to address from endpoint
+func (ep *PaketEndpoint) Send(ctx context.Context, addr net.Addr, msg 
*TransportMessage) (err error) {
+       var a *net.UDPAddr
+       a, err = net.ResolveUDPAddr(addr.Network(), addr.String())
+       var buf []byte
+       if buf, err = msg.Bytes(); err != nil {
+               return
+       }
+       _, err = ep.conn.WriteTo(buf, a)
+       return
+}
+
+// Address returms the
+func (ep *PaketEndpoint) Address() net.Addr {
+       if ep.conn != nil {
+               return ep.conn.LocalAddr()
+       }
+       return ep.addr
+}
+
+// CanSendTo returns true if the endpoint can sent to address
+func (ep *PaketEndpoint) CanSendTo(addr net.Addr) bool {
+       return epMode(addr.Network()) == "packet"
+}
+
+// ID returns the endpoint identifier
+func (ep *PaketEndpoint) ID() int {
+       return ep.id
+}
+
+func newPacketEndpoint(addr net.Addr) (ep *PaketEndpoint, err error) {
+       // check for matching protocol
+       if epMode(addr.Network()) != "packet" {
+               err = ErrEndpProtocolMismatch
+               return
+       }
+       // create endpoint
+       ep = &PaketEndpoint{
+               id:   util.NextID(),
+               addr: addr,
+               buf:  make([]byte, 65536),
+       }
+       return
+}
+
+//----------------------------------------------------------------------
+// Stream-oriented endpoint
+//----------------------------------------------------------------------
+
+// StreamEndpoint for stream-oriented network protocols
+type StreamEndpoint struct {
+       id       int                      // endpoint identifier
+       addr     net.Addr                 // listening address
+       listener net.Listener             // listener instance
+       conns    *util.Map[int, net.Conn] // active connections
+       buf      []byte                   // read/write buffer
+}
+
+// Run packet endpoint: send incoming messages to the handler.
+func (ep *StreamEndpoint) Run(ctx context.Context, hdlr chan 
*TransportMessage) (err error) {
+       // create listener
+       var lc net.ListenConfig
+       if ep.listener, err = lc.Listen(ctx, ep.addr.Network(), 
ep.addr.String()); err != nil {
+               return
+       }
+       // run watch dog for termination
+       go func() {
+               <-ctx.Done()
+               ep.listener.Close()
+       }()
+       // run go routine to handle messages from clients
+       go func() {
+               for {
+                       // get next client connection
+                       conn, err := ep.listener.Accept()
+                       if err != nil {
+                               return
+                       }
+                       session := util.NextID()
+                       ep.conns.Put(session, conn)
+                       go func() {
+                               for {
+                                       // read next message from connection
+                                       msg, err := ReadMessage(ctx, conn, 
ep.buf)
+                                       if err != nil {
+                                               break
+                                       }
+                                       // check for transport message
+                                       if msg.Header().MsgType == 
message.DUMMY {
+                                               // set transient attributes
+                                               tm := msg.(*TransportMessage)
+                                               tm.endp = ep.id
+                                               tm.conn = session
+                                               // send to handler
+                                               go func() {
+                                                       hdlr <- tm
+                                               }()
+                                       }
+                               }
+                               // connection ended.
+                               conn.Close()
+                               ep.conns.Delete(session)
+                       }()
+               }
+       }()
+       return
+}
+
+// Send message to address from endpoint
+func (ep *StreamEndpoint) Send(ctx context.Context, addr net.Addr, msg 
*TransportMessage) error {
+       return nil
+}
+
+// Address returns the actual listening endpoint address
+func (ep *StreamEndpoint) Address() net.Addr {
+       if ep.listener != nil {
+               return ep.listener.Addr()
+       }
+       return ep.addr
+}
+
+// CanSendTo returns true if the endpoint can sent to address
+func (ep *StreamEndpoint) CanSendTo(addr net.Addr) bool {
+       return epMode(addr.Network()) == "stream"
+}
+
+// ID returns the endpoint identifier
+func (ep *StreamEndpoint) ID() int {
+       return ep.id
+}
+
+func newStreamEndpoint(addr net.Addr) (ep *StreamEndpoint, err error) {
+       // check for matching protocol
+       if epMode(addr.Network()) != "stream" {
+               err = ErrEndpProtocolMismatch
+               return
+       }
+       // create endpoint
+       ep = &StreamEndpoint{
+               id:    util.NextID(),
+               addr:  addr,
+               conns: util.NewMap[int, net.Conn](),
+               buf:   make([]byte, 65536),
+       }
+       return
+}
+
+// epMode returns the endpoint mode (packet or stream) for a given network
+func epMode(netw string) string {
+       switch netw {
+       case "udp", "udp4", "udp6", "r5n+ip+udp":
+               return "packet"
+       case "tcp", "unix":
+               return "stream"
+       }
+       return ""
+}
diff --git a/src/gnunet/transport/reader_writer.go 
b/src/gnunet/transport/reader_writer.go
new file mode 100644
index 0000000..2e5f14a
--- /dev/null
+++ b/src/gnunet/transport/reader_writer.go
@@ -0,0 +1,157 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package transport
+
+import (
+       "context"
+       "errors"
+       "fmt"
+       "gnunet/message"
+       "io"
+
+       "github.com/bfix/gospel/data"
+)
+
+// WriteMessageDirect writes directly to io.Writer
+func WriteMessageDirect(wrt io.Writer, msg message.Message) error {
+       dwc := &directWriteCloser{wrt}
+       return WriteMessage(context.Background(), dwc, msg)
+}
+
+// WriteMessage to io.WriteCloser
+func WriteMessage(ctx context.Context, wrt io.WriteCloser, msg 
message.Message) (err error) {
+       // convert message to binary data
+       var buf []byte
+       if buf, err = data.Marshal(msg); err != nil {
+               return err
+       }
+       // check message header size and packet size
+       mh, err := message.GetMsgHeader(buf)
+       if err != nil {
+               return err
+       }
+       if len(buf) != int(mh.MsgSize) {
+               return errors.New("WriteMessage: message size mismatch")
+       }
+       // watch dog for write operation
+       go func() {
+               for {
+                       select {
+                       case <-ctx.Done():
+                               wrt.Close()
+                       }
+               }
+       }()
+       // perform write operation
+       var n int
+       if n, err = wrt.Write(buf); err != nil {
+               return
+       }
+       if n != len(buf) {
+               err = fmt.Errorf("WriteMessage incomplete (%d of %d)", n, 
len(buf))
+       }
+       return
+}
+
+//----------------------------------------------------------------------
+
+// ReadMessageDirect reads directly from io.Reader
+func ReadMessageDirect(rdr io.Reader, buf []byte) (msg message.Message, err 
error) {
+       drc := &directReadCloser{
+               rdr: rdr,
+       }
+       return ReadMessage(context.Background(), drc, buf)
+}
+
+// ReadMessage from io.ReadCloser
+func ReadMessage(ctx context.Context, rdr io.ReadCloser, buf []byte) (msg 
message.Message, err error) {
+       // watch dog for write operation
+       go func() {
+               for {
+                       select {
+                       case <-ctx.Done():
+                               rdr.Close()
+                       }
+               }
+       }()
+       // get bytes from reader
+       if buf == nil {
+               buf = make([]byte, 65536)
+       }
+       get := func(pos, count int) error {
+               n, err := rdr.Read(buf[pos : pos+count])
+               if err == nil && n != count {
+                       err = fmt.Errorf("not enough bytes on reader (%d of 
%d)", n, count)
+               }
+               return err
+       }
+       // read header first
+       if err := get(0, 4); err != nil {
+               return nil, err
+       }
+       var mh *message.Header
+       if mh, err = message.GetMsgHeader(buf[:4]); err != nil {
+               return nil, err
+       }
+       // get rest of message
+       if err = get(4, int(mh.MsgSize)-4); err != nil {
+               return nil, err
+       }
+       // handle transport message case
+       if mh.MsgType == message.DUMMY {
+               msg = NewTransportMessage(nil, nil)
+       } else if msg, err = message.NewEmptyMessage(mh.MsgType); err != nil {
+               return nil, err
+       }
+       if msg == nil {
+               return nil, fmt.Errorf("message{%d} is nil", mh.MsgType)
+       }
+       if err = data.Unmarshal(msg, buf[:mh.MsgSize]); err != nil {
+               return nil, err
+       }
+       return msg, nil
+}
+
+//----------------------------------------------------------------------
+// helper for wrapped ReadCloser/WriteCloser (close is nop)
+//----------------------------------------------------------------------
+
+type directReadCloser struct {
+       rdr io.Reader
+}
+
+func (drc *directReadCloser) Read(buf []byte) (int, error) {
+       return drc.rdr.Read(buf)
+}
+
+func (drc *directReadCloser) Close() error {
+       return nil
+}
+
+type directWriteCloser struct {
+       wrt io.Writer
+}
+
+func (dwc *directWriteCloser) Write(buf []byte) (int, error) {
+       return dwc.wrt.Write(buf)
+}
+
+func (dwc *directWriteCloser) Close() error {
+       return nil
+}
diff --git a/src/gnunet/transport/transport.go 
b/src/gnunet/transport/transport.go
new file mode 100644
index 0000000..14def98
--- /dev/null
+++ b/src/gnunet/transport/transport.go
@@ -0,0 +1,151 @@
+// This file is part of gnunet-go, a GNUnet-implementation in Golang.
+// Copyright (C) 2022 Bernd Fix  >Y<
+//
+// gnunet-go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// gnunet-go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package transport
+
+import (
+       "bytes"
+       "context"
+       "errors"
+       "gnunet/message"
+       "gnunet/util"
+       "net"
+)
+
+// Trnsport layer error codes
+var (
+       ErrTransNoEndpoint = errors.New("no matching endpoint found")
+)
+
+//======================================================================
+// Network-oriented transport implementation
+//======================================================================
+
+// TransportMessage is the unit processed by the transport mechanism.
+// Peer refers to the remote endpoint (sender/receiver) and
+// Msg is the exchanged GNUnet message. The packet itself satisfies the
+// message.Message interface.
+type TransportMessage struct {
+       Hdr     *message.Header ``         // message header
+       Peer    *util.PeerID    ``         // remote peer
+       Payload []byte          `size:"*"` // GNUnet message
+
+       // package-local attributes (transient)
+       msg  message.Message
+       endp int // id of endpoint (incoming message)
+       conn int // id of connection (optional, incoming message)
+}
+
+func (msg *TransportMessage) Header() *message.Header {
+       return msg.Hdr
+}
+
+func (msg *TransportMessage) Message() (m message.Message, err error) {
+       if m = msg.msg; m == nil {
+               rdr := bytes.NewBuffer(msg.Payload)
+               m, err = ReadMessageDirect(rdr, nil)
+       }
+       return
+}
+
+// Bytes returns the binary representation of a transport message
+func (msg *TransportMessage) Bytes() ([]byte, error) {
+       buf := new(bytes.Buffer)
+       err := WriteMessageDirect(buf, msg)
+       return buf.Bytes(), err
+}
+
+// String returns the message in human-readable form
+func (msg *TransportMessage) String() string {
+       return "TransportMessage{...}"
+}
+
+// NewTransportMessage creates a message suitable for transfer
+func NewTransportMessage(peer *util.PeerID, payload []byte) (tm 
*TransportMessage) {
+       if peer == nil {
+               peer = util.NewPeerID(nil)
+       }
+       msize := 0
+       if payload != nil {
+               msize = len(payload)
+       }
+       tm = &TransportMessage{
+               Hdr: &message.Header{
+                       MsgSize: uint16(36 + msize),
+                       MsgType: message.DUMMY,
+               },
+               Peer:    peer,
+               Payload: payload,
+       }
+       return
+}
+
+//----------------------------------------------------------------------
+
+// Transport enables network-oriented (like IP, UDP, TCP or UDS)
+// message exchange on multiple endpoints.
+type Transport struct {
+       incoming  chan *TransportMessage // messages as received from the 
network
+       endpoints map[int]Endpoint       // list of available endpoints
+}
+
+// NewTransport creates and runs a new transport layer implementation.
+func NewTransport(ctx context.Context, ch chan *TransportMessage) (t 
*Transport) {
+       // create transport instance
+       return &Transport{
+               incoming:  ch,
+               endpoints: make(map[int]Endpoint),
+       }
+}
+
+// Send a message over suitable endpoint
+func (t *Transport) Send(ctx context.Context, addr net.Addr, msg 
*TransportMessage) (err error) {
+       for _, ep := range t.endpoints {
+               if ep.CanSendTo(addr) {
+                       err = ep.Send(ctx, addr, msg)
+                       break
+               }
+       }
+       return
+}
+
+//----------------------------------------------------------------------
+// Endpoint handling
+//----------------------------------------------------------------------
+
+// AddEndpoint instantiates and run a new endpoint handler for the
+// given address (must map to a network interface).
+func (t *Transport) AddEndpoint(ctx context.Context, addr net.Addr) (a 
net.Addr, err error) {
+       // register endpoint
+       var ep Endpoint
+       if ep, err = NewEndpoint(addr); err != nil {
+               return
+       }
+       t.endpoints[ep.ID()] = ep
+       ep.Run(ctx, t.incoming)
+       return ep.Address(), nil
+}
+
+// Endpoints returns a list of listening addresses managed by transport.
+func (t *Transport) Endpoints() (list []net.Addr) {
+       list = make([]net.Addr, 0)
+       for _, ep := range t.endpoints {
+               list = append(list, ep.Address())
+       }
+       return
+}
diff --git a/src/gnunet/util/address.go b/src/gnunet/util/address.go
index d272742..106e671 100644
--- a/src/gnunet/util/address.go
+++ b/src/gnunet/util/address.go
@@ -19,44 +19,81 @@
 package util
 
 import (
-       "encoding/hex"
+       "bytes"
        "fmt"
        "net"
+       "strings"
 )
 
 // Address specifies how a peer is reachable on the network.
 type Address struct {
-       Transport string // transport protocol
-       Options   uint32 `order:"big"` // address options
-       Address   []byte `size:"*"`    // address data (protocol-dependent)
+       Netw    string       ``            // network protocol
+       Options uint32       `order:"big"` // address options
+       Expires AbsoluteTime ``            // expiration date for address
+       Address []byte       `size:"*"`    // address data (protocol-dependent)
 }
 
 // NewAddress returns a new Address for the given transport and specs
 func NewAddress(transport string, addr []byte) *Address {
-       a := &Address{
-               Transport: transport,
-               Options:   0,
-               Address:   make([]byte, len(addr)),
+       return &Address{
+               Netw:    transport,
+               Options: 0,
+               Address: Clone(addr),
+               Expires: AbsoluteTimeNever(),
        }
-       copy(a.Address, addr)
-       return a
 }
 
+func NewAddressWrap(addr net.Addr) *Address {
+       return &Address{
+               Netw:    addr.Network(),
+               Options: 0,
+               Address: []byte(addr.String()),
+               Expires: AbsoluteTimeNever(),
+       }
+}
+
+// ParseAddress translates a GNUnet address string like
+// "r5n+ip+udp://1.2.3.4:6789" or "gnunet+tcp://12.3.4.5/".
+// It can also handle standard strings like "udp:127.0.0.1:6735".
+func ParseAddress(s string) (addr *Address, err error) {
+       p := strings.SplitN(s, ":", 2)
+       if len(p) != 2 {
+               err = fmt.Errorf("invalid address format: '%s'", s)
+               return
+       }
+       addr = NewAddress(p[0], []byte(strings.Trim(p[1], "/")))
+       return
+}
+
+// Equals return true if two addresses match.
+func (a *Address) Equals(b *Address) bool {
+       return a.Netw == b.Netw &&
+               a.Options == b.Options &&
+               bytes.Equal(a.Address, b.Address)
+}
+
+// StringAll returns a human-readable representation of an address.
+func (a *Address) StringAll() string {
+       return a.Netw + "://" + string(a.Address)
+}
+
+// implement net.Addr interface methods:
+
 // String returns a human-readable representation of an address.
 func (a *Address) String() string {
-       return fmt.Sprintf("Address{%s}", AddressString(a.Transport, a.Address))
+       return string(a.Address)
+}
+
+// Network returns the protocol specifier.
+func (a *Address) Network() string {
+       return a.Netw
 }
 
 //----------------------------------------------------------------------
 
 // AddressString returns a string representaion of an address.
-func AddressString(transport string, addr []byte) string {
-       if transport == "tcp" || transport == "udp" {
-               alen := len(addr)
-               port := uint(addr[alen-2])*256 + uint(addr[alen-1])
-               return fmt.Sprintf("%s:%s:%d", transport, 
net.IP(addr[:alen-2]).String(), port)
-       }
-       return fmt.Sprintf("%s:%s", transport, hex.EncodeToString(addr))
+func AddressString(network string, addr []byte) string {
+       return network + "://" + string(addr)
 }
 
 //----------------------------------------------------------------------
@@ -76,3 +113,71 @@ func NewIPAddress(host []byte, port uint16) *IPAddress {
        copy(ip.Host, host)
        return ip
 }
+
+//----------------------------------------------------------------------
+
+// PeerAddrList is a list of addresses per peer ID.
+type PeerAddrList struct {
+       list *Map[string, []*Address]
+}
+
+// NewPeerAddrList returns a new and empty address list.
+func NewPeerAddrList() *PeerAddrList {
+       return &PeerAddrList{
+               list: NewMap[string, []*Address](),
+       }
+}
+
+// Add address for peer. The returned mode is 0=not added, 1=new peer,
+// 2=new address
+func (a *PeerAddrList) Add(id string, addr *Address) (mode int) {
+       // check for expired address.
+       mode = 0
+       if !addr.Expires.Expired() {
+               // run add operation
+               a.list.Process(func() error {
+                       list, ok := a.list.Get(id)
+                       if !ok {
+                               list = make([]*Address, 0)
+                               mode = 1
+                       } else {
+                               for _, a := range list {
+                                       if a.Equals(addr) {
+                                               return nil
+                                       }
+                               }
+                               mode = 2
+                       }
+                       list = append(list, addr)
+                       a.list.Put(id, list)
+                       return nil
+               })
+       }
+       return
+}
+
+// Get address for peer
+func (a *PeerAddrList) Get(id string, transport string) *Address {
+       list, ok := a.list.Get(id)
+       if ok {
+               for _, addr := range list {
+                       // check for expired address.
+                       if addr.Expires.Expired() {
+                               // skip expired
+                               continue
+                       }
+                       // check for matching protocol
+                       if len(transport) > 0 && transport != addr.Netw {
+                               // skip other transports
+                               continue
+                       }
+                       return addr
+               }
+       }
+       return nil
+}
+
+// Delete a list entry by key.
+func (a *PeerAddrList) Delete(id string) {
+       a.list.Delete(id)
+}
diff --git a/src/gnunet/util/array.go b/src/gnunet/util/array.go
index 254610b..c6d6371 100644
--- a/src/gnunet/util/array.go
+++ b/src/gnunet/util/array.go
@@ -24,44 +24,68 @@ import (
 
 // Error variables
 var (
-       ErrUtilArrayTooSmall = fmt.Errorf("Array to small")
+       ErrUtilArrayTooSmall = fmt.Errorf("array to small")
 )
 
 //----------------------------------------------------------------------
-// Byte array helpers
+// generic array helpers
 //----------------------------------------------------------------------
 
 // Clone creates a new array of same content as the argument.
-func Clone(d []byte) []byte {
-       r := make([]byte, len(d))
+func Clone[T []E, E any](d T) T {
+       r := make(T, len(d))
        copy(r, d)
        return r
 }
 
-// Reverse the content of a byte array
-func Reverse(b []byte) []byte {
+// Equals returns true if two arrays match.
+func Equals[T []E, E comparable](a, b T) bool {
+       if len(a) != len(b) {
+               return false
+       }
+       for i, e := range a {
+               if e != b[i] {
+                       return false
+               }
+       }
+       return true
+}
+
+// Reverse the content of an array
+func Reverse[T []E, E any](b T) T {
        bl := len(b)
-       r := make([]byte, bl)
+       r := make(T, bl)
        for i := 0; i < bl; i++ {
                r[bl-i-1] = b[i]
        }
        return r
 }
 
-// IsNull returns true if all bytes in an array are set to 0.
-func IsNull(b []byte) bool {
+// IsAll returns true if all elements in an array are set to null.
+func IsAll[T []E, E comparable](b T, null E) bool {
        for _, v := range b {
-               if v != 0 {
+               if v != null {
                        return false
                }
        }
        return true
 }
 
-// CopyBlock copies 'in' to 'out' so that 'out' is filled completely.
+// Fill an array with a value
+func Fill[T []E, E any](b T, val E) {
+       for i := range b {
+               b[i] = val
+       }
+}
+
+//----------------------------------------------------------------------
+// byte array helpers
+//----------------------------------------------------------------------
+
+// CopyAlignedBlock copies 'in' to 'out' so that 'out' is filled completely.
 // - If 'in' is larger than 'out', it is left-truncated before copy
 // - If 'in' is smaller than 'out', it is left-padded with 0 before copy
-func CopyBlock(out, in []byte) {
+func CopyAlignedBlock(out, in []byte) {
        count := len(in)
        size := len(out)
        from, to := 0, 0
@@ -76,27 +100,10 @@ func CopyBlock(out, in []byte) {
        copy(out[to:], in[from:])
 }
 
-// Fill an array with a value
-func Fill(b []byte, val byte) {
-       for i := 0; i < len(b); i++ {
-               b[i] = val
-       }
-}
-
 //----------------------------------------------------------------------
 // String list helpers
 //----------------------------------------------------------------------
 
-// ReverseStringList reverse an array of strings
-func ReverseStringList(s []string) []string {
-       sl := len(s)
-       r := make([]string, sl)
-       for i := 0; i < sl; i++ {
-               r[sl-i-1] = s[i]
-       }
-       return r
-}
-
 // StringList converts a binary representation of a string list. Each string
 // is '\0'-terminated. The whole byte array is parsed; if the final string is
 // not terminated, it is skipped.
diff --git a/src/gnunet/util/database.go b/src/gnunet/util/database.go
index 5805a8f..a1198fd 100644
--- a/src/gnunet/util/database.go
+++ b/src/gnunet/util/database.go
@@ -19,6 +19,7 @@
 package util
 
 import (
+       "context"
        "database/sql"
        "fmt"
        "os"
@@ -34,7 +35,96 @@ var (
        ErrSQLNoDatabase          = fmt.Errorf("Database not found")
 )
 
-// ConnectSQLDatabase connects to an SQL database (various types and flavors):
+//----------------------------------------------------------------------
+// Connection to a database instance. There can be multiple connections
+// on the same instance, managed by the database pool.
+//----------------------------------------------------------------------
+
+// DbConn is a database connection suitable for executing SQL commands.
+type DbConn struct {
+       conn *sql.Conn // connection to database instance
+       pool *dbPool   // reference to managng pool
+       key  string    // database identifier (connect string)
+}
+
+// Close database connection.
+func (db *DbConn) Close() (err error) {
+       if err = db.conn.Close(); err != nil {
+               return
+       }
+       err = db.pool.remove(db.key)
+       return
+}
+
+// QueryRow returns a single record for a query
+func (db *DbConn) QueryRow(query string, args ...any) *sql.Row {
+       return db.conn.QueryRowContext(db.pool.ctx, query, args...)
+}
+
+// Query returns all matching records for a query
+func (db *DbConn) Query(query string, args ...any) (*sql.Rows, error) {
+       return db.conn.QueryContext(db.pool.ctx, query, args...)
+}
+
+// Exec a SQL statement
+func (db *DbConn) Exec(query string, args ...any) (sql.Result, error) {
+       return db.conn.ExecContext(db.pool.ctx, query, args...)
+}
+
+// TODO: add more SQL methods
+
+//----------------------------------------------------------------------
+// DbPool holds all database instances used: Connecting with the same
+// connect string returns the same instance.
+//----------------------------------------------------------------------
+
+// global instance for the database pool (singleton)
+var (
+       DbPool *dbPool
+)
+
+// DbPoolEntry holds information about a database instance.
+type DbPoolEntry struct {
+       db      *sql.DB // reference to the database engine
+       refs    int     // number of open connections (reference count)
+       connect string  // SQL connect string
+}
+
+// package initialization
+func init() {
+       // construct database pool
+       DbPool = new(dbPool)
+       DbPool.insts = NewMap[string, *DbPoolEntry]()
+       DbPool.ctx, DbPool.cancel = context.WithCancel(context.Background())
+}
+
+// dbPool keeps a mapping between connect string and database instance
+type dbPool struct {
+       ctx    context.Context            // connection context
+       cancel context.CancelFunc         // cancel function
+       insts  *Map[string, *DbPoolEntry] // map of database instances
+}
+
+// remove a database instance from the pool based on its connect string.
+func (p *dbPool) remove(key string) error {
+       return p.insts.Process(func() (err error) {
+               // get pool entry
+               pe, ok := p.insts.Get(key)
+               if !ok {
+                       return nil
+               }
+               // decrement ref count
+               pe.refs--
+               if pe.refs == 0 {
+                       // no more refs: close database
+                       err = pe.db.Close()
+                       p.insts.Delete(key)
+               }
+               return
+       })
+}
+
+// Connect to a SQL database (various types and flavors):
 // The 'spec' option defines the arguments required to connect to a database;
 // the meaning and format of the arguments depends on the specific SQL 
database.
 // The arguments are seperated by the '+' character; the first (and mandatory)
@@ -46,27 +136,50 @@ var (
 // * 'mysql':   A MySQL-compatible database; the second argument specifies the
 //              information required to log into the database (e.g.
 //              "[user[:passwd]@][proto[(addr)]]/dbname[?param1=value1&...]").
-func ConnectSQLDatabase(spec string) (db *sql.DB, err error) {
-       // split spec string into segments
-       specs := strings.Split(spec, ":")
-       if len(specs) < 2 {
-               return nil, ErrSQLInvalidDatabaseSpec
-       }
-       switch specs[0] {
-       case "sqlite3":
-               // check if the database file exists
-               var fi os.FileInfo
-               if fi, err = os.Stat(specs[1]); err != nil {
-                       return nil, ErrSQLNoDatabase
-               }
-               if fi.IsDir() {
-                       return nil, ErrSQLNoDatabase
+func (p *dbPool) Connect(spec string) (db *DbConn, err error) {
+       err = p.insts.Process(func() error {
+               // check if we have a connection to this database.
+               inst, ok := p.insts.Get(spec)
+               if !ok {
+                       inst = new(DbPoolEntry)
+                       inst.refs = 0
+                       inst.connect = spec
+
+                       // No: create new database instance.
+                       // split spec string into segments
+                       specs := strings.Split(spec, ":")
+                       if len(specs) < 2 {
+                               return ErrSQLInvalidDatabaseSpec
+                       }
+                       // create database object
+                       switch specs[0] {
+                       case "sqlite3":
+                               // check if the database file exists
+                               var fi os.FileInfo
+                               if fi, err = os.Stat(specs[1]); err != nil {
+                                       return ErrSQLNoDatabase
+                               }
+                               if fi.IsDir() {
+                                       return ErrSQLNoDatabase
+                               }
+                               // open the database file
+                               inst.db, err = sql.Open("sqlite3", specs[1])
+                       case "mysql":
+                               // just connect to the database
+                               inst.db, err = sql.Open("mysql", specs[1])
+                       default:
+                               return ErrSQLInvalidDatabaseSpec
+                       }
+                       // save database in pool
+                       p.insts.Put(spec, inst)
+                       ok = true
                }
-               // open the database file
-               return sql.Open("sqlite3", specs[1])
-       case "mysql":
-               // just connect to the database
-               return sql.Open("mysql", specs[1])
-       }
-       return nil, ErrSQLInvalidDatabaseSpec
+               // increment reference count
+               inst.refs++
+               // return a new connection to the database.
+               db = new(DbConn)
+               db.conn, err = inst.db.Conn(p.ctx)
+               return err
+       })
+       return
 }
diff --git a/src/gnunet/util/fs.go b/src/gnunet/util/fs.go
index 009ef62..b2a464e 100644
--- a/src/gnunet/util/fs.go
+++ b/src/gnunet/util/fs.go
@@ -25,7 +25,7 @@ import (
        "github.com/bfix/gospel/logger"
 )
 
-// EnforceDirExists make sure that the path
+// EnforceDirExists make sure that the path is created
 func EnforceDirExists(path string) error {
        logger.Printf(logger.DBG, "[util] Checking directory '%s'...\n", path)
        fi, err := os.Lstat(path)
diff --git a/src/gnunet/util/key_value_store.go 
b/src/gnunet/util/key_value_store.go
deleted file mode 100644
index 0658218..0000000
--- a/src/gnunet/util/key_value_store.go
+++ /dev/null
@@ -1,188 +0,0 @@
-// This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
-//
-// gnunet-go is free software: you can redistribute it and/or modify it
-// under the terms of the GNU Affero General Public License as published
-// by the Free Software Foundation, either version 3 of the License,
-// or (at your option) any later version.
-//
-// gnunet-go is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-// Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <http://www.gnu.org/licenses/>.
-//
-// SPDX-License-Identifier: AGPL3.0-or-later
-
-package util
-
-import (
-       "context"
-       "database/sql"
-       "fmt"
-       "strconv"
-       "strings"
-
-       redis "github.com/go-redis/redis/v8"
-)
-
-// Error messages related to the key/value-store implementations
-var (
-       ErrKVSInvalidSpec  = fmt.Errorf("Invalid KVStore specification")
-       ErrKVSNotAvailable = fmt.Errorf("KVStore not available")
-)
-
-// KeyValueStore interface for implementations that store and retrieve
-// key/value pairs. Keys and values are strings.
-type KeyValueStore interface {
-       Put(key string, value string) error // put a key/value pair into store
-       Get(key string) (string, error)     // retrieve a value for a key from 
store
-       List() ([]string, error)            // get all keys from the store
-}
-
-// OpenKVStore opens a key/value store for further put/get operations.
-// The 'spec' option specifies the arguments required to connect to a specific
-// persistence mechanism. The arguments in the 'spec' string are separated by
-// the '+' character.
-// The first argument specifies the type of key/value store to be used; the
-// meaning and format of the following arguments depend on this type.
-//
-// Key/Value Store types defined:
-// * 'redis':   Use a Redis server for persistance; the specification is
-//              "redis+addr+[passwd]+db". 'db' must be an integer value.
-// * 'mysql':   MySQL-compatible database (see 'database.go' for details)
-// * 'sqlite3': SQLite3-compatible database (see 'database.go' for details)
-func OpenKVStore(spec string) (KeyValueStore, error) {
-       // check specification string
-       specs := strings.Split(spec, "+")
-       if len(specs) < 2 {
-               return nil, ErrKVSInvalidSpec
-       }
-       switch specs[0] {
-       case "redis":
-               //--------------------------------------------------------------
-               // NoSQL-based persistance
-               //--------------------------------------------------------------
-               if len(specs) < 4 {
-                       return nil, ErrKVSInvalidSpec
-               }
-               db, err := strconv.Atoi(specs[3])
-               if err != nil {
-                       return nil, ErrKVSInvalidSpec
-               }
-               kvs := new(KvsRedis)
-               kvs.db = db
-               kvs.client = redis.NewClient(&redis.Options{
-                       Addr:     specs[1],
-                       Password: specs[2],
-                       DB:       db,
-               })
-               if kvs.client == nil {
-                       err = ErrKVSNotAvailable
-               }
-               return kvs, err
-
-       case "sqlite3", "mysql":
-               //--------------------------------------------------------------
-               // SQL-based persistance
-               //--------------------------------------------------------------
-               kvs := new(KvsSQL)
-               var err error
-
-               // connect to SQL database
-               kvs.db, err = ConnectSQLDatabase(spec)
-               if err != nil {
-                       return nil, err
-               }
-               // get number of key/value pairs (as a check for existing table)
-               row := kvs.db.QueryRow("select count(*) from store")
-               var num int
-               if row.Scan(&num) != nil {
-                       return nil, ErrKVSNotAvailable
-               }
-               return kvs, nil
-       }
-       return nil, ErrKVSInvalidSpec
-}
-
-//======================================================================
-// NoSQL-based key-value-stores
-//======================================================================
-
-// KvsRedis represents a redis-based key/value store
-type KvsRedis struct {
-       client *redis.Client // client connection
-       db     int           // index to database
-}
-
-// Put a key/value pair into the store
-func (kvs *KvsRedis) Put(key string, value string) error {
-       return kvs.client.Set(context.TODO(), key, value, 0).Err()
-}
-
-// Get a value for a given key from store
-func (kvs *KvsRedis) Get(key string) (value string, err error) {
-       return kvs.client.Get(context.TODO(), key).Result()
-}
-
-// List all keys in store
-func (kvs *KvsRedis) List() (keys []string, err error) {
-       var (
-               crs  uint64
-               segm []string
-               ctx  = context.TODO()
-       )
-       for {
-               segm, crs, err = kvs.client.Scan(ctx, crs, "*", 10).Result()
-               if err != nil {
-                       return nil, err
-               }
-               if crs == 0 {
-                       break
-               }
-               keys = append(keys, segm...)
-       }
-       return
-}
-
-//======================================================================
-// SQL-based key-value-store
-//======================================================================
-
-// KvsSQL represents a SQL-based key/value store
-type KvsSQL struct {
-       db *sql.DB
-}
-
-// Put a key/value pair into the store
-func (kvs *KvsSQL) Put(key string, value string) error {
-       _, err := kvs.db.Exec("insert into store(key,value) values(?,?)", key, 
value)
-       return err
-}
-
-// Get a value for a given key from store
-func (kvs *KvsSQL) Get(key string) (value string, err error) {
-       row := kvs.db.QueryRow("select value from store where key=?", key)
-       err = row.Scan(&value)
-       return
-}
-
-// List all keys in store
-func (kvs *KvsSQL) List() (keys []string, err error) {
-       var (
-               rows *sql.Rows
-               key  string
-       )
-       rows, err = kvs.db.Query("select key from store")
-       if err == nil {
-               for rows.Next() {
-                       if err = rows.Scan(&key); err != nil {
-                               break
-                       }
-                       keys = append(keys, key)
-               }
-       }
-       return
-}
diff --git a/src/gnunet/util/misc.go b/src/gnunet/util/misc.go
index 66744bd..7240757 100644
--- a/src/gnunet/util/misc.go
+++ b/src/gnunet/util/misc.go
@@ -20,13 +20,18 @@ package util
 
 import (
        "strings"
+       "sync"
 )
 
-// CounterMap is a metric with single key
-type CounterMap map[interface{}]int
+//----------------------------------------------------------------------
+// Count occurence of multiple instance at the same time.
+//----------------------------------------------------------------------
+
+// Counter is a metric with single key
+type Counter[T comparable] map[T]int
 
 // Add one to themetric for a given key and return current value
-func (cm CounterMap) Add(i interface{}) int {
+func (cm Counter[T]) Add(i T) int {
        count, ok := cm[i]
        if !ok {
                count = 1
@@ -38,7 +43,7 @@ func (cm CounterMap) Add(i interface{}) int {
 }
 
 // Num returns the metric for a given key
-func (cm CounterMap) Num(i interface{}) int {
+func (cm Counter[T]) Num(i T) int {
        count, ok := cm[i]
        if !ok {
                count = 0
@@ -46,6 +51,68 @@ func (cm CounterMap) Num(i interface{}) int {
        return count
 }
 
+//----------------------------------------------------------------------
+// Thread-safe map implementation
+//----------------------------------------------------------------------
+
+// Map keys to values
+type Map[K comparable, V any] struct {
+       list      map[K]V
+       mtx       sync.RWMutex
+       inProcess bool
+}
+
+// NewMap allocates a new mapping.
+func NewMap[K comparable, V any]() *Map[K, V] {
+       return &Map[K, V]{
+               list:      make(map[K]V),
+               inProcess: false,
+       }
+}
+
+// Process a function in the locked map context. Calls
+// to other map functions in 'f' will use additional locks.
+func (m *Map[K, V]) Process(f func() error) error {
+       m.mtx.Lock()
+       defer m.mtx.Unlock()
+       m.inProcess = true
+       err := f()
+       m.inProcess = false
+       return err
+}
+
+// Put value into map under given key.
+func (m *Map[K, V]) Put(key K, value V) {
+       if !m.inProcess {
+               m.mtx.Lock()
+               defer m.mtx.Unlock()
+       }
+       m.list[key] = value
+}
+
+// Get value with iven key from map.
+func (m *Map[K, V]) Get(key K) (value V, ok bool) {
+       if !m.inProcess {
+               m.mtx.RLock()
+               defer m.mtx.RUnlock()
+       }
+       value, ok = m.list[key]
+       return
+}
+
+// Delete key/value pair from map.
+func (m *Map[K, V]) Delete(key K) {
+       if !m.inProcess {
+               m.mtx.Lock()
+               defer m.mtx.Unlock()
+       }
+       delete(m.list, key)
+}
+
+//----------------------------------------------------------------------
+// additional helpers
+//----------------------------------------------------------------------
+
 // StripPathRight returns a dot-separated path without
 // its last (right-most) element.
 func StripPathRight(s string) string {
diff --git a/src/gnunet/util/peer_id.go b/src/gnunet/util/peer_id.go
index f4c14bd..a8202a1 100644
--- a/src/gnunet/util/peer_id.go
+++ b/src/gnunet/util/peer_id.go
@@ -1,5 +1,5 @@
 // This file is part of gnunet-go, a GNUnet-implementation in Golang.
-// Copyright (C) 2019, 2020 Bernd Fix  >Y<
+// Copyright (C) 2019-2022 Bernd Fix  >Y<
 //
 // gnunet-go is free software: you can redistribute it and/or modify it
 // under the terms of the GNU Affero General Public License as published
@@ -18,6 +18,8 @@
 
 package util
 
+import "bytes"
+
 // PeerID is the 32-byte binary representation od a Ed25519 key
 type PeerID struct {
        Key []byte `size:"32"`
@@ -33,7 +35,7 @@ func NewPeerID(data []byte) *PeerID {
                        data = data[:32]
                } else if size < 32 {
                        buf := make([]byte, 32)
-                       CopyBlock(buf, data)
+                       CopyAlignedBlock(buf, data)
                        data = buf
                }
        }
@@ -42,6 +44,11 @@ func NewPeerID(data []byte) *PeerID {
        }
 }
 
+// Equals returns true if two peer IDs match.
+func (p *PeerID) Equals(q *PeerID) bool {
+       return bytes.Equal(p.Key, q.Key)
+}
+
 // String returns a human-readable representation of a peer id.
 func (p *PeerID) String() string {
        return EncodeBinaryToString(p.Key)
diff --git a/src/gnunet/util/time.go b/src/gnunet/util/time.go
index e70635c..70b91e1 100644
--- a/src/gnunet/util/time.go
+++ b/src/gnunet/util/time.go
@@ -43,6 +43,13 @@ func NewAbsoluteTime(t time.Time) AbsoluteTime {
        }
 }
 
+// NewAbsoluteTimeEpoch set the point in time to the given time value
+func NewAbsoluteTimeEpoch(secs uint64) AbsoluteTime {
+       return AbsoluteTime{
+               Val: uint64(secs * 1000000),
+       }
+}
+
 // AbsoluteTimeNow returns the current point in time.
 func AbsoluteTimeNow() AbsoluteTime {
        return NewAbsoluteTime(time.Now())
@@ -53,6 +60,11 @@ func AbsoluteTimeNever() AbsoluteTime {
        return AbsoluteTime{math.MaxUint64}
 }
 
+// Epoch returns the seconds since Unix epoch.
+func (t AbsoluteTime) Epoch() uint64 {
+       return t.Val / 1000000
+}
+
 // String returns a human-readable notation of an absolute time.
 func (t AbsoluteTime) String() string {
        if t.Val == math.MaxUint64 {
@@ -133,3 +145,8 @@ func (t RelativeTime) String() string {
        }
        return time.Duration(t.Val * 1000).String()
 }
+
+// Add two durations
+func (t RelativeTime) Add(t2 RelativeTime) {
+       t.Val += t2.Val
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]