Repository: celestiaorg/celestia-node
Pull request #344 (2022-01-12 13:25:59)

core: Retry remote core client three times before failing out
Resolves https://github.com/celestiaorg/celestia-node/pull/289. 


Patch:
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
new file mode 100644
index 00000000..d3d9d402
--- /dev/null
+++ b/.github/workflows/docker-build.yml
@@ -0,0 +1,39 @@
+name: "docker-build"
+
+on:
+  push:
+    branches:
+      - "main"
+  workflow_dispatch:
+
+jobs:
+  docker-build:
+    runs-on: "ubuntu-latest"
+    permissions:
+      contents: write 
+      packages: write 
+    steps:
+      - name: "Checkout source code"
+        uses: "actions/checkout@v2"
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.17
+      - name: "Build"
+        run: "make build"
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v1
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v1
+      - name: Login to GHCR
+        uses: docker/login-action@v1
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Build and push
+        uses: docker/build-push-action@v2
+        with:
+          push: true
+          tags: ghcr.io/celestiaorg/celestia-node:latest
+          file: docker/Dockerfile
diff --git a/CHANGELOG-PENDING.md b/CHANGELOG-PENDING.md
index 95b122ad..d9937546 100644
--- a/CHANGELOG-PENDING.md
+++ b/CHANGELOG-PENDING.md
@@ -15,25 +15,34 @@ Month, DD, YYYY
 - [feat(cmd): give a birth to cel-shed and p2p key utilities #281](https://github.com/celestiaorg/celestia-node/pull/281) [@Wondertan](https://github.com/Wondertan)
 - [feat(cmd|node): MutualPeers Node option and CLI flag #280](https://github.com/celestiaorg/celestia-node/pull/280) [@Wondertan](https://github.com/Wondertan)
 - [node: enhance DI allowing overriding of dependencies](https://github.com/celestiaorg/celestia-node/pull/290) [@Wondertan](https://github.com/Wondertan)
+- [ci: create docker build GH action](https://github.com/celestiaorg/celestia-node/pull/338) [@jbowen93](https://github.com/jbowen93)
 
 ### IMPROVEMENTS
 
+- [service/header: remove start/stop from P2PExchange](https://github.com/celestiaorg/celestia-node/pull/367) [@Bidon15](https://github.com/Bidon15)
+- [service/share: Implement `FullAvailability`](https://github.com/celestiaorg/celestia-node/pull/333) [@renaynay](https://github.com/renaynay)
 - [services/header: Refactor `HeaderService` to be responsible for broadcasting new `ExtendedHeader`s to the gossipsub network](https://github.com/celestiaorg/celestia-node/pull/327) [@renaynay](https://github.com/renaynay)
 - [cmd: introduce Env - an Environment for CLI commands #313](https://github.com/celestiaorg/celestia-node/pull/313) [@Wondertan](https://github.com/Wondertan)
-- [chore: bump deps #297](https://github.com/celestiaorg/celestia-node/pull/297) [@Wondertan](https://github.com/Wondertan)
-- [workflows/lint: update golangci-lint to v1.43 #308](https://github.com/celestiaorg/celestia-node/pull/308) [@Wondertan](https://github.com/Wondertan)
-- [feat(node): extract overrides from Config into Settings #292](https://github.com/celestiaorg/celestia-node/pull/292) [@Wondertan](https://github.com/Wondertan)
 - [node: Adding WithHost options to settings section #301](https://github.com/celestiaorg/celestia-node/pull/301) [@Bidon15](https://github.com/Bidon15)
 - [node: Adding WithCoreClient option #305](https://github.com/celestiaorg/celestia-node/pull/305) [@Bidon15](https://github.com/Bidon15)
 - [service/header: Refactor `HeaderService` to only manage its sub-services' lifecycles #317](https://github.com/celestiaorg/celestia-node/pull/317) [@renaynay](https://github.com/renaynay)
-- [docker] Created `docker/` dir with `Dockerfile` and `entrypoint.sh` script. 
+- [docker: Created `docker/` dir with `Dockerfile` and `entrypoint.sh` script](https://github.com/celestiaorg/celestia-node/pull/295) [@jbowen93](https://github.com/jbowen93)
 - [chore(share): handle rows concurrently in GetSharesByNamespace #241](https://github.com/celestiaorg/celestia-node/pull/241) [@vgonkivs](https://github.com/vgonkivs)
+- [ci: adding data race detector action](https://github.com/celestiaorg/celestia-node/pull/289) [@Bidon15](https://github.com/Bidon15)
 
 ### BUG FIXES
 
-- [Properly fetch Validators from Core and two more fixes #328](https://github.com/celestiaorg/celestia-node/pull/328) [@Wondertan](https://github.com/Wondertan)
-- [header] Added missing `err` value in ErrorW logging calls. @jbowen93
-- [service/block, node/p2p] [Fix race conditions in TestExtendedHeaderBroadcast and TestFull_P2P_Streams.](https://github.com/celestiaorg/celestia-node/pull/288) [@jenyasd209](https://github.com/jenyasd209)
+- [fix(header/service): #339 race](https://github.com/celestiaorg/celestia-node/pull/343) [@Wondertan](https://github.com/Wondertan)
+- [core: Properly fetch Validators from Core and two more fixes #328](https://github.com/celestiaorg/celestia-node/pull/328) [@Wondertan](https://github.com/Wondertan)
+- [header: Added missing `err` value in ErrorW logging calls](https://github.com/celestiaorg/celestia-node/pull/282) [@jbowen93](https://github.com/jbowen93)
+- [service/block, node/p2p: Fix race conditions in TestExtendedHeaderBroadcast and TestFull_P2P_Streams.](https://github.com/celestiaorg/celestia-node/pull/288) [@jenyasd209](https://github.com/jenyasd209)
 - [ci: increase tokens ratio for dupl to fix false positive scenarios](https://github.com/celestiaorg/celestia-node/pull/314) [@Bidon15](https://github.com/Bidon15)
-- [node: update vanilla datastore with Mutex one](https://github.com/celestiaorg/celestia-node/pull/325) [@Bidon15](https://github.com/Bidon15)
+- [node: Wrap datastore with mutex to prevent data race](https://github.com/celestiaorg/celestia-node/pull/325) [@Bidon15](https://github.com/Bidon15)
+- [ci: update Docker entrypoint.sh to use new `store.path` flag name](https://github.com/celestiaorg/celestia-node/pull/337) [@jbowen93](https://github.com/jbowen93)
+
+### MISCELLANEOUS
+
+- [chore: bump deps #297](https://github.com/celestiaorg/celestia-node/pull/297) [@Wondertan](https://github.com/Wondertan)
+- [workflows/lint: update golangci-lint to v1.43 #308](https://github.com/celestiaorg/celestia-node/pull/308) [@Wondertan](https://github.com/Wondertan)
+- [feat(node): extract overrides from Config into Settings #292](https://github.com/celestiaorg/celestia-node/pull/292) [@Wondertan](https://github.com/Wondertan)
 - [node: fix naming of the test from full to bridge](https://github.com/celestiaorg/celestia-node/pull/341) [@Bidon15](https://github.com/Bidon15)
diff --git a/Makefile b/Makefile
index 18f4eca1..da2c1c4b 100644
--- a/Makefile
+++ b/Makefile
@@ -51,6 +51,12 @@ test:
 	@go test -v ./...
 .PHONY: test
 
+## test-race: Running all *_test.go with data race detector
+test-race:
+	@echo "--> Running tests with data race detector"
+	@go test -v -race ./...
+.PHONY: test-race
+
 ## benchmark: Running all benchmarks
 benchmark:
 	@echo "--> Running benchmarks"
diff --git a/core/client.go b/core/client.go
index b6ec129c..bda520ce 100644
--- a/core/client.go
+++ b/core/client.go
@@ -3,6 +3,8 @@ package core
 import (
 	"fmt"
 
+	retryhttp "github.com/hashicorp/go-retryablehttp"
+
 	corenode "github.com/tendermint/tendermint/node"
 	"github.com/tendermint/tendermint/rpc/client"
 	"github.com/tendermint/tendermint/rpc/client/http"
@@ -14,9 +16,13 @@ type Client = client.Client
 
 // NewRemote creates a new Client that communicates with a remote Core endpoint over HTTP.
 func NewRemote(protocol, remoteAddr string) (Client, error) {
-	return http.New(
+	httpClient := retryhttp.NewClient()
+	httpClient.RetryMax = 2
+
+	return http.NewWithClient(
 		fmt.Sprintf("%s://%s", protocol, remoteAddr),
 		"/websocket",
+		httpClient.StandardClient(),
 	)
 }
 
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 2d8d0164..fa4fde10 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -7,7 +7,8 @@ WORKDIR ${HOME}
 RUN env GOOS=linux GOARCH=amd64 make build
 
 FROM ubuntu
-ENV NODE_TYPE full
+# Default node type can be overwritten in deployment manifest
+ENV NODE_TYPE bridge
 
 COPY docker/entrypoint.sh /
 
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
index 9a318dd2..f147f693 100755
--- a/docker/entrypoint.sh
+++ b/docker/entrypoint.sh
@@ -3,7 +3,7 @@
 set -e 
 
 if [ "$1" = 'celestia' ]; then
-    ./celestia "${NODE_TYPE}" --repo.path /celestia-"${NODE_TYPE}" init
+    ./celestia "${NODE_TYPE}" --store.path /celestia-"${NODE_TYPE}" init
 
     exec ./"$@" "--"
 fi
diff --git a/go.mod b/go.mod
index a00ac469..d37f1d02 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
 	github.com/celestiaorg/nmt v0.8.0
 	github.com/celestiaorg/rsmt2d v0.3.0
 	github.com/gogo/protobuf v1.3.2
+	github.com/hashicorp/go-retryablehttp v0.7.1-0.20211018174820-ff6d014e72d9
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/ipfs/go-bitswap v0.4.0
 	github.com/ipfs/go-block-format v0.0.3
diff --git a/go.sum b/go.sum
index bda760bc..834d17ce 100644
--- a/go.sum
+++ b/go.sum
@@ -454,7 +454,9 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -464,7 +466,10 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
 github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s=
 github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-retryablehttp v0.7.1-0.20211018174820-ff6d014e72d9 h1:RSp277I12pTdb5wiZWy9qtE0IN4oC4BH5a/LIcNxw0w=
+github.com/hashicorp/go-retryablehttp v0.7.1-0.20211018174820-ff6d014e72d9/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
diff --git a/node/components.go b/node/components.go
index 83d7ffbd..9dbb409c 100644
--- a/node/components.go
+++ b/node/components.go
@@ -19,7 +19,7 @@ func lightComponents(cfg *Config, store Store) fxutil.Option {
 	)
 }
 
-// fullComponents keeps all the components as DI options required to build a Full Node.
+// bridgeComponents keeps all the components as DI options required to build a Bridge Node.
 func bridgeComponents(cfg *Config, store Store) fxutil.Option {
 	return fxutil.Options(
 		fxutil.Supply(Bridge),
@@ -37,12 +37,15 @@ func baseComponents(cfg *Config, store Store) fxutil.Option {
 		fxutil.Provide(store.Datastore),
 		fxutil.Provide(store.Keystore),
 		fxutil.Provide(services.ShareService),
+		// TODO @renaynay: once full node type is defined, add FullAvailability
+		//  to full node and LightAvailability to light node. Bridge node does
+		//  not need Availability.
+		fxutil.Provide(services.LightAvailability),
 		fxutil.Provide(services.HeaderService),
 		fxutil.Provide(services.HeaderStore),
 		fxutil.Provide(services.HeaderSyncer(cfg.Services)),
 		fxutil.Provide(services.P2PSubscriber),
 		fxutil.Provide(services.HeaderP2PExchangeServer),
-		fxutil.Provide(services.LightAvailability), // TODO(@Wondertan): Move to light once FullAvailability is implemented
 		p2p.Components(cfg.P2P),
 	)
 }
diff --git a/node/services/config.go b/node/services/config.go
index 3acd3092..52ae53a8 100644
--- a/node/services/config.go
+++ b/node/services/config.go
@@ -3,11 +3,14 @@ package services
 import (
 	"encoding/hex"
 
+	logging "github.com/ipfs/go-log/v2"
 	"github.com/libp2p/go-libp2p-core/peer"
 	"github.com/multiformats/go-multiaddr"
 	tmbytes "github.com/tendermint/tendermint/libs/bytes"
 )
 
+var log = logging.Logger("node/services")
+
 type Config struct {
 	// TrustedHash is the Block/Header hash that Nodes use as starting point for header synchronization.
 	// Only affects the node once on initial sync.
@@ -28,6 +31,7 @@ func DefaultConfig() Config {
 
 func (cfg *Config) trustedPeer() (*peer.AddrInfo, error) {
 	if cfg.TrustedPeer == "" {
+		log.Warn("No Trusted Peer provided. Headers won't be synced accordingly")
 		return &peer.AddrInfo{}, nil
 	}
 
diff --git a/node/services/service.go b/node/services/service.go
index 524ed385..346cdfa8 100644
--- a/node/services/service.go
+++ b/node/services/service.go
@@ -7,6 +7,7 @@ import (
 	ipld "github.com/ipfs/go-ipld-format"
 	"github.com/ipfs/go-merkledag"
 	"github.com/libp2p/go-libp2p-core/host"
+	"github.com/libp2p/go-libp2p-core/peerstore"
 	pubsub "github.com/libp2p/go-libp2p-pubsub"
 	"go.uber.org/fx"
 
@@ -55,23 +56,15 @@ func HeaderService(
 }
 
 // HeaderExchangeP2P constructs new P2PExchange for headers.
-func HeaderExchangeP2P(cfg Config) func(
-	lc fx.Lifecycle,
-	host host.Host,
-	store header.Store,
-) (header.Exchange, error) {
-	return func(lc fx.Lifecycle, host host.Host, store header.Store) (header.Exchange, error) {
+func HeaderExchangeP2P(cfg Config) func(host host.Host) (header.Exchange, error) {
+	return func(host host.Host) (header.Exchange, error) {
 		peer, err := cfg.trustedPeer()
 		if err != nil {
 			return nil, err
 		}
 
-		ex := header.NewP2PExchange(host, peer, store)
-		lc.Append(fx.Hook{
-			OnStart: ex.Start,
-			OnStop:  ex.Stop,
-		})
-		return ex, nil
+		host.Peerstore().AddAddrs(peer.ID, peer.Addrs, peerstore.PermanentAddrTTL)
+		return header.NewP2PExchange(host, peer.ID), nil
 	}
 }
 
@@ -128,3 +121,7 @@ func DASer(lc fx.Lifecycle, avail share.Availability, sub header.Subscriber) *da
 func LightAvailability(ctx context.Context, lc fx.Lifecycle, dag ipld.DAGService) share.Availability {
 	return share.NewLightAvailability(merkledag.NewSession(fxutil.WithLifecycle(ctx, lc), dag))
 }
+
+func FullAvailability(ctx context.Context, lc fx.Lifecycle, dag ipld.DAGService) share.Availability {
+	return share.NewFullAvailability(merkledag.NewSession(fxutil.WithLifecycle(ctx, lc), dag))
+}
diff --git a/service/header/core_listener.go b/service/header/core_listener.go
index 93c88249..25178c1d 100644
--- a/service/header/core_listener.go
+++ b/service/header/core_listener.go
@@ -67,9 +67,10 @@ func (cl *CoreListener) Stop(ctx context.Context) error {
 // gossipsub network.
 func (cl *CoreListener) listen() {
 	defer log.Info("core-listener: listening stopped")
+	sub := cl.blockSub
 	for {
 		select {
-		case b, ok := <-cl.blockSub:
+		case b, ok := <-sub:
 			if !ok {
 				return
 			}
diff --git a/service/header/p2p_exchange.go b/service/header/p2p_exchange.go
index bac2d11f..2837af11 100644
--- a/service/header/p2p_exchange.go
+++ b/service/header/p2p_exchange.go
@@ -4,10 +4,8 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"sync"
 
 	"github.com/libp2p/go-libp2p-core/host"
-	"github.com/libp2p/go-libp2p-core/network"
 	"github.com/libp2p/go-libp2p-core/peer"
 	"github.com/libp2p/go-libp2p-core/protocol"
 	tmbytes "github.com/tendermint/tendermint/libs/bytes"
@@ -22,55 +20,18 @@ var exchangeProtocolID = protocol.ID("/header-ex/v0.0.1")
 // P2PExchange enables sending outbound ExtendedHeaderRequests to the network as well as
 // handling inbound ExtendedHeaderRequests from the network.
 type P2PExchange struct {
-	host  host.Host
-	store Store
+	host host.Host
 
 	// TODO @renaynay: post-Devnet, we need to remove reliance of Exchange on one bootstrap peer
 	// Ref https://github.com/celestiaorg/celestia-node/issues/172#issuecomment-964306823.
-	trustedPeer *peer.AddrInfo
-	lk          sync.Mutex
-	connected   chan struct{} // if connected is closed, exchange is connected to peer
-
-	ctx    context.Context
-	cancel context.CancelFunc
+	trustedPeer peer.ID
 }
 
-func NewP2PExchange(host host.Host, peer *peer.AddrInfo, store Store) *P2PExchange {
-	ex := &P2PExchange{
+func NewP2PExchange(host host.Host, peer peer.ID) *P2PExchange {
+	return &P2PExchange{
 		host:        host,
-		store:       store,
 		trustedPeer: peer,
-		connected:   make(chan struct{}),
-	}
-	ex.host.Network().Notify(&network.NotifyBundle{ConnectedF: ex.Connected})
-	return ex
-}
-
-func (ex *P2PExchange) Start(ctx context.Context) error {
-	log.Info("p2p: starting p2p exchange")
-	ex.ctx, ex.cancel = context.WithCancel(context.Background())
-
-	if ex.trustedPeer.ID != "" {
-		if ex.host.Network().Connectedness(ex.trustedPeer.ID) == network.Connected {
-			close(ex.connected)
-			return nil
-		}
-
-		err := ex.host.Connect(ctx, *ex.trustedPeer)
-		if err != nil {
-			log.Errorw("p2p: connecting to trusted peer", "err", err)
-			log.Warn("p2p: HEADERS WONT BE SYNCHRONIZED - PLEASE RESTART WITH TRUSTED PEER BEING ONLINE")
-		}
 	}
-
-	return nil
-}
-
-func (ex *P2PExchange) Stop(context.Context) error {
-	log.Info("p2p: stopping p2p exchange")
-	ex.cancel()
-	ex.ctx, ex.cancel = nil, nil
-	return nil
 }
 
 func (ex *P2PExchange) RequestHead(ctx context.Context) (*ExtendedHeader, error) {
@@ -134,13 +95,11 @@ func (ex *P2PExchange) RequestByHash(ctx context.Context, hash tmbytes.HexBytes)
 }
 
 func (ex *P2PExchange) performRequest(ctx context.Context, req *pb.ExtendedHeaderRequest) ([]*ExtendedHeader, error) {
-	select {
-	case <-ctx.Done():
-		return nil, ctx.Err()
-	case <-ex.connected:
+	if ex.trustedPeer == "" {
+		return nil, fmt.Errorf("no trusted peer")
 	}
 
-	stream, err := ex.host.NewStream(ctx, ex.trustedPeer.ID, exchangeProtocolID)
+	stream, err := ex.host.NewStream(ctx, ex.trustedPeer, exchangeProtocolID)
 	if err != nil {
 		return nil, err
 	}
@@ -180,19 +139,3 @@ func (ex *P2PExchange) performRequest(ctx context.Context, req *pb.ExtendedHeade
 	}
 	return headers, stream.Close()
 }
-
-func (ex *P2PExchange) Connected(_ network.Network, conn network.Conn) {
-	select {
-	// don't connect if already connected
-	case <-ex.connected:
-		return
-	default:
-	}
-
-	ex.lk.Lock()
-	defer ex.lk.Unlock()
-
-	if conn.RemotePeer() == ex.trustedPeer.ID {
-		close(ex.connected)
-	}
-}
diff --git a/service/share/full_availability.go b/service/share/full_availability.go
new file mode 100644
index 00000000..3e138cf5
--- /dev/null
+++ b/service/share/full_availability.go
@@ -0,0 +1,31 @@
+package share
+
+import (
+	"context"
+
+	format "github.com/ipfs/go-ipld-format"
+
+	"github.com/celestiaorg/celestia-node/ipld"
+	"github.com/celestiaorg/rsmt2d"
+)
+
+// fullAvailability implements Availability using the full data square
+// recovery technique. It is considered "full" because it is required
+// to download enough shares to fully reconstruct the data square.
+type fullAvailability struct {
+	getter format.NodeGetter
+}
+
+// NewFullAvailability creates a new full Availability.
+func NewFullAvailability(get format.NodeGetter) Availability {
+	return &fullAvailability{
+		getter: get,
+	}
+}
+
+// SharesAvailable reconstructs the data committed to the given Root by requesting
+// enough Shares from the network.
+func (fa *fullAvailability) SharesAvailable(ctx context.Context, root *Root) error {
+	_, err := ipld.RetrieveData(ctx, root, fa.getter, rsmt2d.NewRSGF8Codec())
+	return err
+}
diff --git a/service/share/light_availability.go b/service/share/light_availability.go
index 2ed1eb73..a6acc223 100644
--- a/service/share/light_availability.go
+++ b/service/share/light_availability.go
@@ -13,14 +13,14 @@ import (
 var DefaultSampleAmount = 16
 
 // lightAvailability implements Availability using Data Availability Sampling technique.
-// It is light because it does not require to download all the data to verify it availability.
-// It is assumed that there are lots of lightAvailability instances on the network doing sampling over the same Root
-// to collectively verify its availability.
+// It is light because it does not require the downloading of all the data to verify
+// its availability. It is assumed that there are lots of lightAvailability instances
+// on the network doing sampling over the same Root to collectively verify its availability.
 type lightAvailability struct {
 	getter format.NodeGetter
 }
 
-// NewLightAvailability creates a new Light DataAvailability.
+// NewLightAvailability creates a new light Availability.
 func NewLightAvailability(get format.NodeGetter) Availability {
 	return &lightAvailability{
 		getter: get,
diff --git a/service/share/share.go b/service/share/share.go
index 71180b84..62bb4a1c 100644
--- a/service/share/share.go
+++ b/service/share/share.go
@@ -16,6 +16,7 @@ import (
 	"github.com/celestiaorg/celestia-node/ipld/plugin"
 	"github.com/celestiaorg/nmt"
 	"github.com/celestiaorg/nmt/namespace"
+	"github.com/celestiaorg/rsmt2d"
 )
 
 var log = logging.Logger("share")
@@ -129,8 +130,24 @@ func (s *service) GetShare(ctx context.Context, dah *Root, row, col int) (Share,
 	return nd.RawData()[1:], nil
 }
 
-func (s *service) GetShares(context.Context, *Root) ([][]Share, error) {
-	panic("implement me")
+func (s *service) GetShares(ctx context.Context, root *Root) ([][]Share, error) {
+	eds, err := ipld.RetrieveData(ctx, root, s.dag, rsmt2d.NewRSGF8Codec())
+	if err != nil {
+		return nil, err
+	}
+
+	origWidth := int(eds.Width() / 2)
+	shares := make([][]Share, origWidth)
+
+	for i := 0; i < origWidth; i++ {
+		row := eds.Row(uint(i))
+		shares[i] = make([]Share, origWidth)
+		for j := 0; j < origWidth; j++ {
+			shares[i][j] = row[j]
+		}
+	}
+
+	return shares, nil
 }
 
 func (s *service) GetSharesByNamespace(ctx context.Context, root *Root, nID namespace.ID) ([]Share, error) {


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b5671c7e..7083664e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,17 +7,19 @@ on:
   pull_request:
 
 jobs:
-  build:
+  test:
+    name: Run Tests
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go: ['1.16', '1.17']
+        go: ["1.16", "1.17"]
     steps:
       - uses: actions/checkout@v2
       - name: set up go
         uses: actions/setup-go@v2
         with:
           go-version: ${{ matrix.go }}
-
-      - name: test
-        run: go test -v ./...
+      - name: Tests
+        run: make test
+      - name: Tests with Race Detector
+        run: make test-race
diff --git a/core/client_test.go b/core/client_test.go
index c6651fc1..9f0b10af 100644
--- a/core/client_test.go
+++ b/core/client_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -102,3 +103,21 @@ func TestRemoteClient_StartBlockSubscription_And_GetBlock(t *testing.T) {
 	require.NoError(t, client.Stop())
 	require.NoError(t, remote.Stop())
 }
+
+// TestRemoteClient_RetryDial ensures 3 additional attempts
+// are made to dial the remote node.
+func TestRemoteClient_RetryDial(t *testing.T) {
+	remote := StartMockNode()
+	protocol, ip := getRemoteEndpoint(remote)
+
+	// deliberately stop remote
+	err := remote.Stop()
+	require.NoError(t, err)
+
+	client, err := NewRemote(protocol, ip)
+	require.NoError(t, err)
+
+	_, err = client.Block(context.Background(), nil)
+	require.Error(t, err)
+	assert.ErrorContains(t, err, "giving up after 3 attempt(s)")
+}
diff --git a/das/daser_test.go b/das/daser_test.go
index bf15b00b..e35a442b 100644
--- a/das/daser_test.go
+++ b/das/daser_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 func TestDASer(t *testing.T) {
-	shareServ, dah := share.RandServiceWithSquare(t, 16)
+	shareServ, dah := share.RandLightServiceWithSquare(t, 16)
 
 	randHeader := header.RandExtendedHeader(t)
 	randHeader.DataHash = dah.Hash()
diff --git a/service/header/p2p_exchange_test.go b/service/header/p2p_exchange_test.go
index 41c60eb5..99c8f6e6 100644
--- a/service/header/p2p_exchange_test.go
+++ b/service/header/p2p_exchange_test.go
@@ -115,17 +115,11 @@ func createP2PExAndServer(t *testing.T, host, peer libhost.Host) (Exchange, *moc
 	err := serverSideEx.Start(context.Background())
 	require.NoError(t, err)
 
-	// create new exchange
-	clientSideEx := NewP2PExchange(host, libhost.InfoFromHost(peer), nil) // we don't need the store on the requesting side
-	err = clientSideEx.Start(context.Background())
-	require.NoError(t, err)
-
 	t.Cleanup(func() {
 		serverSideEx.Stop(context.Background()) //nolint:errcheck
-		clientSideEx.Stop(context.Background()) //nolint:errcheck
 	})
 
-	return clientSideEx, store
+	return NewP2PExchange(host, peer.ID()), store
 }
 
 type mockStore struct {
diff --git a/service/share/full_availability_test.go b/service/share/full_availability_test.go
new file mode 100644
index 00000000..289f48db
--- /dev/null
+++ b/service/share/full_availability_test.go
@@ -0,0 +1,44 @@
+package share
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/celestiaorg/celestia-node/service/header"
+)
+
+func TestSharesAvailable_Full(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	// RandFullServiceWithSquare creates a NewFullAvailability inside, so we can test it
+	service, dah := RandFullServiceWithSquare(t, 16)
+	err := service.SharesAvailable(ctx, dah)
+	assert.NoError(t, err)
+}
+
+func TestSharesAvailableFailed_Full(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	// RandFullServiceWithSquare creates a NewFullAvailability inside, so we can test it
+	s, _ := RandFullServiceWithSquare(t, 16)
+	empty := header.EmptyDAH()
+	err := s.SharesAvailable(ctx, &empty)
+	assert.Error(t, err)
+}
+
+func TestShareAvailableOverMocknet_Full(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	net := NewDAGNet(ctx, t)
+	_, root := net.RandFullService(16)
+	serv := net.CleanService()
+	net.ConnectAll()
+
+	err := serv.SharesAvailable(ctx, root)
+	assert.NoError(t, err)
+}
diff --git a/service/share/light_availability_test.go b/service/share/light_availability_test.go
index 5542ace0..bfbb0e75 100644
--- a/service/share/light_availability_test.go
+++ b/service/share/light_availability_test.go
@@ -13,8 +13,8 @@ func TestSharesAvailable(t *testing.T) {
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 
-	// RandServiceWithSquare creates a NewLightAvailability inside, so we can test it
-	service, dah := RandServiceWithSquare(t, 16)
+	// RandLightServiceWithSquare creates a NewLightAvailability inside, so we can test it
+	service, dah := RandLightServiceWithSquare(t, 16)
 	err := service.SharesAvailable(ctx, dah)
 	assert.NoError(t, err)
 }
@@ -23,8 +23,8 @@ func TestSharesAvailableFailed(t *testing.T) {
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 
-	// RandServiceWithSquare creates a NewLightAvailability inside, so we can test it
-	s, _ := RandServiceWithSquare(t, 16)
+	// RandLightServiceWithSquare creates a NewLightAvailability inside, so we can test it
+	s, _ := RandLightServiceWithSquare(t, 16)
 	empty := header.EmptyDAH()
 	err := s.SharesAvailable(ctx, &empty)
 	assert.Error(t, err)
@@ -35,7 +35,7 @@ func TestShareAvailableOverMocknet(t *testing.T) {
 	defer cancel()
 
 	net := NewDAGNet(ctx, t)
-	_, root := net.RandService(16)
+	_, root := net.RandLightService(16)
 	serv := net.CleanService()
 	net.ConnectAll()
 
diff --git a/service/share/share_test.go b/service/share/share_test.go
index 078949f0..a618221d 100644
--- a/service/share/share_test.go
+++ b/service/share/share_test.go
@@ -2,11 +2,14 @@ package share
 
 import (
 	"context"
+	"math"
 	"strconv"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
+	"github.com/tendermint/tendermint/pkg/da"
 )
 
 func TestGetShare(t *testing.T) {
@@ -14,7 +17,7 @@ func TestGetShare(t *testing.T) {
 	defer cancel()
 
 	n := 16
-	serv, dah := RandServiceWithSquare(t, n)
+	serv, dah := RandLightServiceWithSquare(t, n)
 	err := serv.Start(ctx)
 	require.NoError(t, err)
 
@@ -42,7 +45,7 @@ func TestService_GetSharesByNamespace(t *testing.T) {
 
 	for i, tt := range tests {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			serv, root := RandServiceWithSquare(t, tt.amountShares)
+			serv, root := RandLightServiceWithSquare(t, tt.amountShares)
 			randNID := root.RowsRoots[(len(root.RowsRoots)-1)/2][:8]
 			if tt.expectedShareCount > 1 {
 				// make it so that two rows have the same namespace ID
@@ -59,8 +62,39 @@ func TestService_GetSharesByNamespace(t *testing.T) {
 	}
 }
 
+func TestGetShares(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	n := 16
+	serv, dah := RandLightServiceWithSquare(t, n)
+	err := serv.Start(ctx)
+	require.NoError(t, err)
+
+	shares, err := serv.GetShares(ctx, dah)
+	require.NoError(t, err)
+
+	flattened := make([][]byte, 0)
+	for _, row := range shares {
+		for _, share := range row {
+			flattened = append(flattened, share)
+		}
+	}
+	// generate DAH from shares returned by `GetShares` to compare
+	// calculated DAH to expected DAH
+	squareSize := uint64(math.Sqrt(float64(len(flattened))))
+	eds, err := da.ExtendShares(squareSize, flattened)
+	require.NoError(t, err)
+	gotDAH := da.NewDataAvailabilityHeader(eds)
+
+	require.True(t, dah.Equals(&gotDAH))
+
+	err = serv.Stop(ctx)
+	require.NoError(t, err)
+}
+
 func TestService_GetSharesByNamespaceNotFoundInRange(t *testing.T) {
-	serv, root := RandServiceWithSquare(t, 1)
+	serv, root := RandLightServiceWithSquare(t, 1)
 	root.RowsRoots = nil
 
 	shares, err := serv.GetSharesByNamespace(context.Background(), root, []byte(""))
@@ -80,7 +114,7 @@ func BenchmarkService_GetSharesByNamespace(b *testing.B) {
 	for _, tt := range tests {
 		b.Run(strconv.Itoa(tt.amountShares), func(b *testing.B) {
 			t := &testing.T{}
-			serv, root := RandServiceWithSquare(t, tt.amountShares)
+			serv, root := RandLightServiceWithSquare(t, tt.amountShares)
 			randNID := root.RowsRoots[(len(root.RowsRoots)-1)/2][:8]
 			root.RowsRoots[(len(root.RowsRoots) / 2)] = root.RowsRoots[(len(root.RowsRoots)-1)/2]
 			b.ResetTimer()
diff --git a/service/share/testing.go b/service/share/testing.go
index 9dfb63ab..a60bbce8 100644
--- a/service/share/testing.go
+++ b/service/share/testing.go
@@ -28,13 +28,20 @@ import (
 	"github.com/celestiaorg/celestia-node/service/header"
 )
 
-// RandServiceWithSquare provides a share.Service filled with 'n' NMT trees of 'n' random shares, essentially storing a
-// whole square.
-func RandServiceWithSquare(t *testing.T, n int) (Service, *Root) {
+// RandLightServiceWithSquare provides a share.Service filled with 'n' NMT
+// trees of 'n' random shares, essentially storing a whole square.
+func RandLightServiceWithSquare(t *testing.T, n int) (Service, *Root) {
 	dag := mdutils.Mock()
 	return NewService(dag, NewLightAvailability(dag)), RandFillDAG(t, n, dag)
 }
 
+// RandFullServiceWithSquare provides a share.Service filled with 'n' NMT
+// trees of 'n' random shares, essentially storing a whole square.
+func RandFullServiceWithSquare(t *testing.T, n int) (Service, *Root) {
+	dag := mdutils.Mock()
+	return NewService(dag, NewFullAvailability(dag)), RandFillDAG(t, n, dag)
+}
+
 func RandFillDAG(t *testing.T, n int, dag format.DAGService) *Root {
 	shares := RandShares(t, n*n)
 	sharesSlices := make([][]byte, n*n)
@@ -79,11 +86,16 @@ func NewDAGNet(ctx context.Context, t *testing.T) *DAGNet {
 	}
 }
 
-func (dn *DAGNet) RandService(n int) (Service, *Root) {
+func (dn *DAGNet) RandLightService(n int) (Service, *Root) {
 	dag, root := dn.RandDAG(n)
 	return NewService(dag, NewLightAvailability(dag)), root
 }
 
+func (dn *DAGNet) RandFullService(n int) (Service, *Root) {
+	dag, root := dn.RandDAG(n)
+	return NewService(dag, NewFullAvailability(dag)), root
+}
+
 func (dn *DAGNet) RandDAG(n int) (format.DAGService, *Root) {
 	dag := dn.CleanDAG()
 	return dag, RandFillDAG(dn.t, n, dag)