Skip to content

Commit

Permalink
Baby steps on client code
Browse files Browse the repository at this point in the history
  • Loading branch information
AnomalRoil committed Jul 22, 2024
1 parent a7ef367 commit a4b0ead
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 26 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
## New Project
## drand-cli is a set of useful binaries for the drand ecosystem

This repo contains most notably:
- a client CLI tool to fetch and verify drand beacons from the various available sources
- a gossipsub relay to relay drand beacons on gossipsub
- Go APIs to connect to drand networks through http or gossipsub relays.



---
Expand Down
46 changes: 46 additions & 0 deletions client/http/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package http_test

import (
"context"
"encoding/hex"
"fmt"

client2 "github.com/drand/drand-cli/client"
"github.com/drand/drand-cli/client/http"
"github.com/drand/drand/v2/crypto"
)

func ExampleHttpNew() {

Check failure on line 13 in client/http/example_test.go

View workflow job for this annotation

GitHub Actions / lint

tests: ExampleHttpNew refers to unknown identifier: HttpNew (govet)
chainhash, err := hex.DecodeString("52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971")

Check failure on line 14 in client/http/example_test.go

View workflow job for this annotation

GitHub Actions / lint

ineffectual assignment to err (ineffassign)
client, err := http.New(context.Background(), nil, "http://api.drand.sh", chainhash, nil)
if err != nil {
panic(err)
}

result, err := client.Get(context.Background(), 1234)
if err != nil {
panic(err)
}

info, err := client.Info(context.Background())
if err != nil {
panic(err)
}

scheme, err := crypto.SchemeFromName(info.GetSchemeName())
if err != nil {
panic(err)
}

// make sure to verify the beacons when using the raw http client without a verifying client
err = scheme.VerifyBeacon(&client2.RandomData{
Rnd: result.GetRound(),
Sig: result.GetSignature(),
}, info.PublicKey)
if err != nil {
panic(err)
}

fmt.Printf("got beacon: round=%d; randomness=%x\n", result.GetRound(), result.GetRandomness())
//output: got beacon: round=1234; randomness=9ead58abb451d8f521338c43ba5595610642a0c07d0e9babeaae6a98787629de
}
7 changes: 5 additions & 2 deletions client/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ const httpWaitInterval = 2 * time.Second
const maxTimeoutHTTPRequest = 5 * time.Second

// New creates a new client pointing to an HTTP endpoint
func New(ctx context.Context, l log.Logger, url string, chainHash []byte, transport nhttp.RoundTripper) (client.Client, error) {
func New(ctx context.Context, l log.Logger, url string, chainHash []byte, transport nhttp.RoundTripper) (*httpClient, error) {

Check warning on line 37 in client/http/http.go

View workflow job for this annotation

GitHub Actions / lint

unexported-return: exported func New returns unexported type *http.httpClient, which can be annoying to use (revive)
if l == nil {
l = log.DefaultLogger()
}
if transport == nil {
transport = nhttp.DefaultTransport
}
Expand Down Expand Up @@ -64,7 +67,7 @@ func New(ctx context.Context, l log.Logger, url string, chainHash []byte, transp
}

// NewWithInfo constructs an http client when the group parameters are already known.
func NewWithInfo(l log.Logger, url string, info *chain2.Info, transport nhttp.RoundTripper) (client.Client, error) {
func NewWithInfo(l log.Logger, url string, info *chain2.Info, transport nhttp.RoundTripper) (*httpClient, error) {

Check warning on line 70 in client/http/http.go

View workflow job for this annotation

GitHub Actions / lint

unexported-return: exported func NewWithInfo returns unexported type *http.httpClient, which can be annoying to use (revive)
if transport == nil {
transport = nhttp.DefaultTransport
}
Expand Down
56 changes: 41 additions & 15 deletions client/lp2p/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
clock "github.com/jonboulle/clockwork"
"github.com/libp2p/go-libp2p"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
dnsaddr "github.com/multiformats/go-multiaddr-dns"
"google.golang.org/protobuf/proto"

client2 "github.com/drand/drand-cli/client"
Expand Down Expand Up @@ -61,14 +63,26 @@ func PubSubTopic(h string) string {
return fmt.Sprintf("/drand/pubsub/v0.0.0/%s", h)
}

// NewWithPubsub creates a gossip randomness client.
// NewWithPubsub creates a gossip randomness client. If the logger l is nil, it will default to
// a default Logger,
//
//nolint:funlen,lll // This is the correct function length
//nolint:funlen,lll // This is a long line
func NewWithPubsub(l log.Logger, ps *pubsub.PubSub, info *chain.Info, cache client2.Cache, clk clock.Clock, bufferSize int) (*Client, error) {

Check failure on line 70 in client/lp2p/client.go

View workflow job for this annotation

GitHub Actions / lint

cyclomatic complexity 17 of func `NewWithPubsub` is high (> 15) (gocyclo)
if info == nil {
return nil, fmt.Errorf("no chain supplied for joining")
}

if l == nil {
l = log.DefaultLogger()
}

scheme, err := crypto.SchemeFromName(info.Scheme)
if err != nil {
l.Errorw("invalid scheme in info", "info", info, "scheme", info.Scheme, "err", err)

return nil, fmt.Errorf("invalid scheme in info: %w", err)
}

ctx, cancel := context.WithCancel(context.Background())
c := &Client{
cancel: cancel,
Expand Down Expand Up @@ -128,7 +142,11 @@ func NewWithPubsub(l log.Logger, ps *pubsub.PubSub, info *chain.Info, cache clie
continue
}

// TODO: verification, need to pass drand network public key in
err = scheme.VerifyBeacon(&rand, info.PublicKey)
if err != nil {
c.log.Errorw("invalid signature for beacon", "round", rand.GetRound(), "err", err)
continue
}

if c.latest >= rand.Round {
c.log.Debugw("received round older than the latest previously received one", "latest", c.latest, "round", rand.Round)
Expand Down Expand Up @@ -235,22 +253,30 @@ func (c *Client) Close() error {
}

// NewPubsub constructs a basic libp2p pubsub module for use with the drand client.
func NewPubsub(ctx context.Context, listenAddr, relayAddr string) (*pubsub.PubSub, error) {
// The local libp2p host is returned as well to allow to properly close it once done.
func NewPubsub(ctx context.Context, listenAddr string, relayAddrs []string) (*pubsub.PubSub, host.Host, error) {
h, err := libp2p.New(libp2p.ListenAddrStrings(listenAddr))
if err != nil {
return nil, err
return nil, nil, err
}

relayMa, err := multiaddr.NewMultiaddr(relayAddr)
if err != nil {
return nil, err
}

relayAi, err := peer.AddrInfoFromP2pAddr(relayMa)
if err != nil {
return nil, err
peers := make([]peer.AddrInfo, 0, len(relayAddrs))
for _, relayAddr := range relayAddrs {
// resolve the relay multiaddr to peers' AddrInfo
mas, err := dnsaddr.Resolve(ctx, multiaddr.StringCast(relayAddr))
if err != nil {
return nil, nil, fmt.Errorf("dnsaddr.Resolve error: %w", err)
}
for _, ma := range mas {
relayAi, err := peer.AddrInfoFromP2pAddr(ma)
if err != nil {
h.Close()
return nil, nil, fmt.Errorf("peer.AddrInfoFromP2pAddr error: %w", err)
}
peers = append(peers, *relayAi)
}
}

dps := []peer.AddrInfo{*relayAi}
return pubsub.NewGossipSub(ctx, h, pubsub.WithDirectPeers(dps))
ps, err := pubsub.NewGossipSub(ctx, h, pubsub.WithDirectPeers(peers))
return ps, h, err
}
78 changes: 78 additions & 0 deletions client/lp2p/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package lp2p_test

import (
"bytes"
"context"
"fmt"
"time"

Check failure on line 8 in client/lp2p/example_test.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed with -local github.com/drand (goimports)
gclient "github.com/drand/drand-cli/client/lp2p"
"github.com/drand/drand/v2/common"
"github.com/drand/drand/v2/common/chain"
"github.com/drand/drand/v2/common/log"
clock "github.com/jonboulle/clockwork"
)

const (
// relayP2PAddr is the p2p multiaddr of the drand gossipsub relay node to connect to.
relayP2PAddr = "/dnsaddr/api.drand.sh"
relayP2PAddr2 = "/dnsaddr/api2.drand.sh"
relayP2PAddr3 = "/dnsaddr/api3.drand.sh"

jsonDefaultInfo = `{

Check failure on line 22 in client/lp2p/example_test.go

View workflow job for this annotation

GitHub Actions / lint

const `jsonDefaultInfo` is unused (unused)
"genesis_time": 1595431050,
"groupHash": "176f93498eac9ca337150b46d21dd58673ea4e3581185f869672e59fa4cb390a",
"hash": "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce",
"metadata": {
"beaconID": "default"
},
"period": 30,
"public_key": "868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31",
"schemeID": "pedersen-bls-chained"
}`
// jsonQuicknetInfo, can be hardcoded since these don't change over time
jsonQuicknetInfo = `{
"genesis_time": 1692803367,
"groupHash": "f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e",
"hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
"metadata": {
"beaconID": "quicknet"
},
"period": 3,
"public_key": "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a",
"schemeID": "bls-unchained-g1-rfc9380"
}
`
)

func ExampleNewPubsub() {
ctx := context.Background()

// /0 to use a random free port
ps, h, err := gclient.NewPubsub(ctx, "/ip4/0.0.0.0/tcp/0", []string{relayP2PAddr, relayP2PAddr2, relayP2PAddr3})
if err != nil {
panic(err)
}
defer h.Close()

info, err := chain.InfoFromJSON(bytes.NewReader([]byte(jsonQuicknetInfo)))
if err != nil {
panic(err)
}

// NewWithPubSub will automatically register the topic for the chainhash you're interested in
c, err := gclient.NewWithPubsub(log.DefaultLogger(), ps, info, nil, clock.NewRealClock(), gclient.DefaultBufferSize)
if err != nil {
panic(err)
}

// This can be slow to "start"
for res := range c.Watch(context.Background()) {
expected := common.CurrentRound(time.Now().Unix(), info.Period, info.GenesisTime)
fmt.Println("correct round:", expected == res.GetRound(), "with", len(res.GetRandomness()), "random bytes")
// we just waited on the first one as an example
break
}

//output: correct round: true with 32 random bytes
}
11 changes: 5 additions & 6 deletions client/lp2p/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import (
)

func randomnessValidator(info *chain2.Info, cache client.Cache, c *Client, clk clock.Clock) pubsub.ValidatorEx {
var scheme *crypto.Scheme
if info != nil {
scheme, _ = crypto.GetSchemeByID(info.Scheme)
}
return func(ctx context.Context, p peer.ID, m *pubsub.Message) pubsub.ValidationResult {

Check warning on line 26 in client/lp2p/validator.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
rand := &drand.PublicRandResponse{}
err := proto.Unmarshal(m.Data, rand)
Expand Down Expand Up @@ -65,7 +69,7 @@ func randomnessValidator(info *chain2.Info, cache client.Cache, c *Client, clk c
return pubsub.ValidationReject
}
if current.GetRound() == rand.GetRound() &&
bytes.Equal(current.GetRandomness(), rand.GetRandomness()) &&
bytes.Equal(current.GetRandomness(), crypto.RandomnessFromSignature(rand.GetSignature())) &&
bytes.Equal(current.GetSignature(), rand.GetSignature()) &&
bytes.Equal(currentFull.PreviousSignature, rand.GetPreviousSignature()) {
c.log.Warnw("", "gossip validator", "ignore")
Expand All @@ -75,11 +79,6 @@ func randomnessValidator(info *chain2.Info, cache client.Cache, c *Client, clk c
return pubsub.ValidationReject
}
}
scheme, err := crypto.SchemeFromName(info.Scheme)
if err != nil {
c.log.Warnw("", "gossip validator", "reject", "err", err)
return pubsub.ValidationReject
}

err = scheme.VerifyBeacon(rand, info.PublicKey)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions client/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type resultWithPreviousSignature interface {
func asRandomData(r client.Result) *RandomData {
rd, ok := r.(*RandomData)
if ok {
rd.Random = crypto.RandomnessFromSignature(rd.GetSignature())
return rd
}
rd = &RandomData{
Expand Down
5 changes: 3 additions & 2 deletions internal/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,11 @@ func getChainInfo(cctx *cli.Context) error {
if err != nil {
return err
}

info, err := c.Info(cctx.Context)
if err != nil {
return err
}
info.ToJSON(cctx.App.Writer, nil)
return nil

return info.ToJSON(cctx.App.Writer, nil)
}
2 changes: 2 additions & 0 deletions internal/lib/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ var ClientFlags = []cli.Flag{

// Create builds a client, and can be invoked from a cli action supplied
// with ClientFlags
//
//nolint:gocyclo
func Create(c *cli.Context, withInstrumentation bool, opts ...pubClient.Option) (client.Client, error) {
ctx := c.Context
clients := make([]client.Client, 0)
Expand Down
2 changes: 2 additions & 0 deletions internal/lib/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ func TestClientLib(t *testing.T) {
addr, info, cancel, _ := httpmock.NewMockHTTPPublicServer(t, false, sch, clk)
defer cancel()

time.Sleep(time.Second)

grpcLis, _ := mock.NewMockGRPCPublicServer(t, lg, ":0", false, sch, clk)
go grpcLis.Start()
defer grpcLis.Stop(context.Background())
Expand Down

0 comments on commit a4b0ead

Please sign in to comment.