Skip to content

Commit

Permalink
Implement certificate exchange
Browse files Browse the repository at this point in the history
This implements a basic certificate exchange protocol (which still needs
tests and is probably slightly broken at the moment).

Importantly, it's missing the ability to fetch power table deltas for
validating future instances (beyond the latest certificate). My plan is
to implement this as a somewhat separate protocol (likely re-using a lot
of the same machinery). However:

1. That protocol is only needed for observer nodes. Active participants
in the network will follow the EC chain and will learn these power
tables through the EC chain.
2. That protocol won't need as much guessing because we'll _know_ which
power tables should be available given the latest certificate we've
received.

The large remaining TODOs are tests and design documentation. The entire
protocol has been in constant flux so... I'm sure there are some
inconsistencies...
  • Loading branch information
Stebalien committed Jun 27, 2024
1 parent 69e5251 commit 2fe69aa
Show file tree
Hide file tree
Showing 10 changed files with 1,292 additions and 1 deletion.
142 changes: 142 additions & 0 deletions certexchange/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package certexchange

import (
"bufio"
"context"
"fmt"
"io"
"runtime/debug"
"time"

"github.com/filecoin-project/go-f3"
"github.com/filecoin-project/go-f3/certs"
"github.com/filecoin-project/go-f3/gpbft"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
)

// We've estimated the max power table size to be less than 1MiB:
//
// 1. For 10k participants.
// 2. <100 bytes per entry (key + id + power)
const maxPowerTableSize = 1024 * 1024

// Client is a libp2p certificate exchange client for requesting finality certificates from specific
// peers.
type Client struct {
Host host.Host
NetworkName gpbft.NetworkName
RequestTimeout time.Duration

Log f3.Logger
}

func resetOnCancel(ctx context.Context, s network.Stream) func() error {
errCh := make(chan error, 1)
cancel := context.AfterFunc(ctx, func() {
errCh <- s.Reset()
close(errCh)
})
return func() error {
if cancel() {
return s.Reset()
} else {
return <-errCh

Check warning on line 45 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L35-L45

Added lines #L35 - L45 were not covered by tests
}
}
}

func (c *Client) withDeadline(ctx context.Context) (context.Context, context.CancelFunc) {
if c.RequestTimeout > 0 {
return context.WithTimeout(ctx, c.RequestTimeout)

Check warning on line 52 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L50-L52

Added lines #L50 - L52 were not covered by tests
}
return ctx, func() {}

Check warning on line 54 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L54

Added line #L54 was not covered by tests
}

// Request finality certificates from the specified peer. Returned finality certificates start at
// the requested instance number and are sequential, but are otherwise unvalidated.
func (c *Client) Request(ctx context.Context, p peer.ID, req *Request) (_rh *ResponseHeader, _ch <-chan *certs.FinalityCertificate, _err error) {
defer func() {
if perr := recover(); perr != nil {
_err = fmt.Errorf("panicked requesting certificates from peer %s: %v\n%s", p, perr, string(debug.Stack()))
c.Log.Error(_err)

Check warning on line 63 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L59-L63

Added lines #L59 - L63 were not covered by tests
}
}()

ctx, cancel := c.withDeadline(ctx)
defer cancel()

Check warning on line 68 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L67-L68

Added lines #L67 - L68 were not covered by tests

proto := FetchProtocolName(c.NetworkName)
stream, err := c.Host.NewStream(ctx, p, proto)
if err != nil {
return nil, nil, err

Check warning on line 73 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L70-L73

Added lines #L70 - L73 were not covered by tests
}
defer resetOnCancel(ctx, stream)()

Check failure on line 75 in certexchange/client.go

View workflow job for this annotation

GitHub Actions / Lint

Error return value is not checked (errcheck)

Check warning on line 75 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L75

Added line #L75 was not covered by tests

if deadline, ok := ctx.Deadline(); ok {
if err := stream.SetDeadline(deadline); err != nil {
return nil, nil, err

Check warning on line 79 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L77-L79

Added lines #L77 - L79 were not covered by tests
}
}

br := &io.LimitedReader{R: bufio.NewReader(stream), N: 100}
bw := bufio.NewWriter(stream)

Check warning on line 84 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L83-L84

Added lines #L83 - L84 were not covered by tests

if err := req.MarshalCBOR(bw); err != nil {
c.Log.Debugf("failed to marshal certificate exchange request to peer %s: %w", p, err)
return nil, nil, err

Check warning on line 88 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L86-L88

Added lines #L86 - L88 were not covered by tests
}
if err := bw.Flush(); err != nil {
return nil, nil, err

Check warning on line 91 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L90-L91

Added lines #L90 - L91 were not covered by tests
}
if err := stream.CloseWrite(); err != nil {
return nil, nil, err

Check warning on line 94 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L93-L94

Added lines #L93 - L94 were not covered by tests
}

var resp ResponseHeader
if req.IncludePowerTable {
br.N = maxPowerTableSize

Check warning on line 99 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L97-L99

Added lines #L97 - L99 were not covered by tests
}
err = resp.UnmarshalCBOR(br)
if err != nil {
c.Log.Debugf("failed to unmarshal certificate exchange response header from peer %s: %w", p, err)
return nil, nil, err

Check warning on line 104 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L101-L104

Added lines #L101 - L104 were not covered by tests
}

ch := make(chan *certs.FinalityCertificate, 1)

Check warning on line 107 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L107

Added line #L107 was not covered by tests
// copy this in case the caller decides to re-use the request object...
request := *req
go func() {
defer func() {
if perr := recover(); perr != nil {
c.Log.Errorf("panicked while receiving certificates from peer %s: %v\n%s", p, perr, string(debug.Stack()))

Check warning on line 113 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L109-L113

Added lines #L109 - L113 were not covered by tests
}
}()
defer close(ch)
for i := uint64(0); request.Limit == 0 || i < request.Limit; i++ {
cert := new(certs.FinalityCertificate)

Check warning on line 118 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L116-L118

Added lines #L116 - L118 were not covered by tests

// We'll read at most 1MiB per certificate. They generally shouldn't be that
// large, but large power deltas could get close.
br.N = maxPowerTableSize
err := cert.UnmarshalCBOR(br)
if err != nil {
c.Log.Debugf("failed to unmarshal certificate from peer %s: %w", p, err)
return

Check warning on line 126 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L122-L126

Added lines #L122 - L126 were not covered by tests
}
// One quick sanity check. The rest will be validated by the caller.
if cert.GPBFTInstance != request.FirstInstance+i {
c.Log.Warnf("received out-of-order certificate from peer %s", p)
return

Check warning on line 131 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L129-L131

Added lines #L129 - L131 were not covered by tests
}

select {
case <-ctx.Done():
return
case ch <- cert:

Check warning on line 137 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L134-L137

Added lines #L134 - L137 were not covered by tests
}
}
}()
return &resp, ch, nil

Check warning on line 141 in certexchange/client.go

View check run for this annotation

Codecov / codecov/patch

certexchange/client.go#L141

Added line #L141 was not covered by tests
}
239 changes: 239 additions & 0 deletions certexchange/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 2fe69aa

Please sign in to comment.