Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: specs #116

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
224 changes: 224 additions & 0 deletions p2p/p2p.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# P2P

The P2P package mainly contains two services:

1) Subscriber
2) Exchange
gupadhyaya marked this conversation as resolved.
Show resolved Hide resolved

gupadhyaya marked this conversation as resolved.
Show resolved Hide resolved
## Subscriber
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must clearly mention somewhere that the gossiped message is only a serialized header with user-defined serialization and that protocol does not add any metadata on top.


Subscriber is a service that manages the gossip of headers among the nodes in the P2P network by using [libp2p][libp2p] and its [pubsub][pubsub] modules. The pubsub topic is used for gossip (`/<networkID>/header-sub/v0.0.1`) and is configurable based on the `networkID` parameter used to initialize the subscriber service.

The Subscriber implements the following interface:

```
// Subscriber encompasses the behavior necessary to
// subscribe/unsubscribe from new Header events from the
// network.
type Subscriber[H Header[H]] interface {
// Subscribe creates long-living Subscription for validated Headers.
// Multiple Subscriptions can be created.
Subscribe() (Subscription[H], error)
// SetVerifier registers verification func for all Subscriptions.
// Registered func screens incoming headers
// before they are forwarded to Subscriptions.
// Only one func can be set.
SetVerifier(func(context.Context, H) error) error
}
```

The `Subscribe()` method allows listening to any new headers that are published to the P2P network. The `SetVerifier()` method allows for setting a custom verifier that will be executed upon receiving any new headers from the P2P network. This is a very useful customization for the consumers of go-header library to pass any custom logic as part of the pubsub.
gupadhyaya marked this conversation as resolved.
Show resolved Hide resolved

## Exchange

An exchange is a combination of:

* Exchange: a client for requesting headers from the P2P network (outbound)
* ExchangeServer: a P2P server for handling inbound header requests

### Exchange Client

Exchange defines a client for requesting headers from the P2P network. An exchange client is initialized using self [host.Host][host], a list of peers in the form of slice [peer.IDSlice][peer], and a [connection gater][gater] for blocking and allowing nodes. Optional parameters like `ChainID` and `NetworkID` can also be passed. The exchange client also maintains a list of trusted peers via a peer tracker. The peer tracker discovers peers until `len(connected)+len(disconnected)` will not reach the limit(`maxPeerTrackerSize` for now it is 100) and `len(connected)>len(disconnected)`.

#### Peer Tracker
gupadhyaya marked this conversation as resolved.
Show resolved Hide resolved

The three main functionalities of the peer tracker are:

* bootstrap
* track
* garbage collection (gc)

When the exchange client is started, it bootstraps the peer tracker using the set of trusted peers used to initialize the exchange client.

The new peers are tracked by subscribing to `event.EvtPeerConnectednessChanged{}`.

The peer tracker also runs garbage collector (gc) that removes the disconnected peers (determined as disconnected for more than `maxAwaitingTime` or connected peers whose scores are less than or equal to `defaultScore`) from the tracked peers list once every `gcPeriod`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should either link to those constants or mention their values here is some legend.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment applies to all other constants throughout the spec


The peer tracker also provides a block peer functionality which is used to block peers that send invalid network headers. Invalid header is a header that fails when `Verify` method of the header interface is invoked.

The exchange client implements the following interface:

```
// Getter contains the behavior necessary for a component to retrieve
// headers that have been processed during header sync.
type Getter[H Header[H]] interface {
Head[H]

// Get returns the Header corresponding to the given hash.
Get(context.Context, Hash) (H, error)

// GetByHeight returns the Header corresponding to the given block height.
GetByHeight(context.Context, uint64) (H, error)

// GetRangeByHeight returns the given range of Headers.
GetRangeByHeight(ctx context.Context, from, amount uint64) ([]H, error)

// GetVerifiedRange requests the header range from the provided Header and
// verifies that the returned headers are adjacent to each other.
GetVerifiedRange(ctx context.Context, from H, amount uint64) ([]H, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods were recently unified, and this needs to be updated.

}

// Head contains the behavior necessary for a component to retrieve
// the chain head. Note that "chain head" is subjective to the component
// reporting it.
type Head[H Header[H]] interface {
// Head returns the latest known header.
Head(context.Context, ...HeadOption[H]) (H, error)
}
```

`Head()` method requests the latest header from trusted peers. The `Head()` requests utilizes 90% of the set deadline (in the form of context deadline) for requests and remaining for determining the best head from gathered responses. The `Head()` call also allows passing an optional `TrustedHead` which allows the caller to specify a trusted header against which the untrusted headers received from a list of tracked peers (limited to `maxUntrustedHeadRequests` of 4) can be verified against, in the absence of trusted peers. Upon receiving headers from peers (either trusted or tracked), the best head is determined as the head:
gupadhyaya marked this conversation as resolved.
Show resolved Hide resolved

* with max height among the received
* which is received from at least `minHeadResponses` of 2 peers
* when neither or both conditions meet, the head with highest height is used

Apart from requesting the latest header, any arbitrary header(s) can be requested (with 3 retries) using height (`GetByHeight`), hash (`Get`), range (`GetRangeByHeight` and `GetVerifiedRange`) from trusted peers as defined in the request proto message:

```
message HeaderRequest {
oneof data {
uint64 origin = 1;
bytes hash = 2;
}
uint64 amount = 3;
}
```
Comment on lines +78 to +86
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking]
Let's separate the protocol from implementation. I suggest adding ### Protocol paragraph, which contains all the messages and explains the basic flow. The method description of components can then link to it.


The `GetVerifiedRange` differs from `GetRangeByHeight` as it ensures that the returned headers are correct against another header (passed as parameter to the call).

### Exchange Server

ExchangeServer represents the server-side component (P2P server) for responding to inbound header requests. The exchange server needs to be initialized using self [host.Host][host] and a [store][store]. Optional `ServerParameters` as shown below, can be set during the server initialization.

```
// ServerParameters is the set of parameters that must be configured for the exchange.
type ServerParameters struct {
// WriteDeadline sets the timeout for sending messages to the stream
WriteDeadline time.Duration
// ReadDeadline sets the timeout for reading messages from the stream
ReadDeadline time.Duration
// RangeRequestTimeout defines a timeout after which the session will try to re-request headers
// from another peer.
RangeRequestTimeout time.Duration
// networkID is a network that will be used to create a protocol.ID
// Is empty by default
networkID string
}
```

The default values for `ServerParameters` are as described below.

```
// DefaultServerParameters returns the default params to configure the store.
func DefaultServerParameters() ServerParameters {
return ServerParameters{
WriteDeadline: time.Second * 8,
ReadDeadline: time.Minute,
RangeRequestTimeout: time.Second * 10,
}
}
```

During the server start, a request handler for the `protocolID` (`/networkID/header-ex/v0.0.3`) which defined using the `networkID` configurable parameter is setup to serve the inbound header requests.

The request handler returns a response which contains bytes of the requested header(s) and a status code as shown below.

```
message HeaderResponse {
bytes body = 1;
StatusCode statusCode = 2;
}
```

The `OK` status code for success, `NOT_FOUND` for requested headers not found, and `INVALID` for error (default).

```
enum StatusCode {
INVALID = 0;
OK = 1;
NOT_FOUND = 2;
}
```

The request handler utilizes its local [store][store] for serving the header requests and only up to `MaxRangeRequestSize` of 512 headers can be requested while requesting headers by range. If the requested range is not available, the range is reset to whatever is available.

### Session

Session aims to divide a header range requests into several smaller requests among different peers. This service is used by the exchange client for making the `GetRangeByHeight` and `GetVerifiedRange` calls.

```
// ClientParameters is the set of parameters that must be configured for the exchange.
type ClientParameters struct {
// MaxHeadersPerRangeRequest defines the max amount of headers that can be requested per 1 request.
MaxHeadersPerRangeRequest uint64
// RangeRequestTimeout defines a timeout after which the session will try to re-request headers
// from another peer.
RangeRequestTimeout time.Duration
// networkID is a network that will be used to create a protocol.ID
networkID string
// chainID is an identifier of the chain.
chainID string

pidstore PeerIDStore
}
```

The default values for `ClientParameters` are as described below.

```
// DefaultClientParameters returns the default params to configure the store.
func DefaultClientParameters() ClientParameters {
return ClientParameters{
MaxHeadersPerRangeRequest: 64,
RangeRequestTimeout: time.Second * 8,
}
}
```

## Metrics

Currently only following metrics are collected:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the list is already extended for Subscriber, and more will be added soon.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take care of updating the spec


* P2P header exchange response size
* Duration of the get headers request in seconds
* Total synced headers

# References

[1] [libp2p][libp2p]

[2] [pubsub][pubsub]

[3] [host.Host][host]

[4] [peer.IDSlice][peer]

[5] [connection gater][gater]

[libp2p]: https://github.com/libp2p/go-libp2p
[pubsub]: https://github.com/libp2p/go-libp2p-pubsub
[host]: https://github.com/libp2p/go-libp2p/core/host
[peer]: https://github.com/libp2p/go-libp2p/core/peer
[gater]: https://github.com/libp2p/go-libp2p/p2p/net/conngater
[store]: https://github.com/celestiaorg/go-header/blob/main/store/store.md
55 changes: 55 additions & 0 deletions specs/src/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,58 @@
# Welcome

Welcome to the go-header Specifications.
gupadhyaya marked this conversation as resolved.
Show resolved Hide resolved

go-header is a library for syncing blockchain data such as block headers over the P2P network. It contains services for requesting and receiving headers from the P2P network, serving header requests from other nodes in the P2P network, storing headers, and syncing historical headers in case of fallbacks.
gupadhyaya marked this conversation as resolved.
Show resolved Hide resolved

|Component|Description|
|---|---|
|[p2p.Subscriber][p2p]|listens for new headers from the P2P network|
|[p2p.ExchangeServer][p2p]|serve header requests from other nodes in the P2P network|
|[p2p.Exchange][p2p]|client that requests headers from other nodes in the P2P network|
|[store.Store][store]|storing headers and making them available for access by other services such as exchange and syncer|
|[sync.Syncer][sync]|syncing of historical and new headers from the P2P network|

The go-header library makes it easy to be used by other projects by defining a clear interface (as described below). An example usage is defined in [headertest/dummy_header.go][dummy header]

```
type Header[H any] interface {
// New creates new instance of a header.
New() H

// IsZero reports whether Header is a zero value of it's concrete type.
IsZero() bool

// ChainID returns identifier of the chain.
ChainID() string

// Hash returns hash of a header.
Hash() Hash

// Height returns the height of a header.
Height() uint64

// LastHeader returns the hash of last header before this header (aka. previous header hash).
LastHeader() Hash

// Time returns time when header was created.
Time() time.Time

// Verify validates given untrusted Header against trusted Header.
Verify(H) error

// Validate performs stateless validation to check for missed/incorrect fields.
Validate() error

encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
}
```

# References

[1] [Dummy Header][dummy header]

[dummy header]: https://github.com/celestiaorg/go-header/blob/main/headertest/dummy_header.go
[p2p]: https://github.com/celestiaorg/go-header/blob/main/p2p/p2p.md
[store]: https://github.com/celestiaorg/go-header/blob/main/store/store.md
[sync]: https://github.com/celestiaorg/go-header/blob/main/sync/sync.md
110 changes: 110 additions & 0 deletions store/store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Store

Store implements the Store interface (shown below) for headers over [datastore][go-datastore].

```
// Store encompasses the behavior necessary to store and retrieve Headers
// from a node's local storage.
type Store[H Header[H]] interface {
// Getter encompasses all getter methods for headers.
Getter[H]

// Init initializes Store with the given head, meaning it is initialized with the genesis header.
Init(context.Context, H) error

// Height reports current height of the chain head.
Height() uint64

// Has checks whether Header is already stored.
Has(context.Context, Hash) (bool, error)

// HasAt checks whether Header at the given height is already stored.
HasAt(context.Context, uint64) bool

// Append stores and verifies the given Header(s).
// It requires them to be adjacent and in ascending order,
// as it applies them contiguously on top of the current head height.
// It returns the amount of successfully applied headers,
// so caller can understand what given header was invalid, if any.
Append(context.Context, ...H) error
}

// Getter contains the behavior necessary for a component to retrieve
// headers that have been processed during header sync.
type Getter[H Header[H]] interface {
Head[H]

// Get returns the Header corresponding to the given hash.
Get(context.Context, Hash) (H, error)

// GetByHeight returns the Header corresponding to the given block height.
GetByHeight(context.Context, uint64) (H, error)

// GetRangeByHeight returns the given range of Headers.
GetRangeByHeight(ctx context.Context, from, amount uint64) ([]H, error)

// GetVerifiedRange requests the header range from the provided Header and
// verifies that the returned headers are adjacent to each other.
GetVerifiedRange(ctx context.Context, from H, amount uint64) ([]H, error)
}

// Head contains the behavior necessary for a component to retrieve
// the chain head. Note that "chain head" is subjective to the component
// reporting it.
type Head[H Header[H]] interface {
// Head returns the latest known header.
Head(context.Context, ...HeadOption[H]) (H, error)
}
```

A new store is created by passing a [datastore][go-datastore] instance and an optional head. If the head is not passed while creating a new store, `Init` method can be used to later initialize the store with head. The store must have a head before start. The head is considered trusted header and generally it is the genesis header. A custom store prefix can be passed during the store initialization. Further, a set of parameters can be passed during the store initialization to configure the store as described below.

```
// Parameters is the set of parameters that must be configured for the store.
type Parameters struct {
// StoreCacheSize defines the maximum amount of entries in the Header Store cache.
StoreCacheSize int

// IndexCacheSize defines the maximum amount of entries in the Height to Hash index cache.
IndexCacheSize int

// WriteBatchSize defines the size of the batched header write.
// Headers are written in batches not to thrash the underlying Datastore with writes.
WriteBatchSize int

// storePrefix defines the prefix used to wrap the store
// OPTIONAL
storePrefix datastore.Key
}
```

The default values for store `Parameters` are as described below.

```
// DefaultParameters returns the default params to configure the store.
func DefaultParameters() Parameters {
return Parameters{
StoreCacheSize: 4096,
IndexCacheSize: 16384,
WriteBatchSize: 2048,
}
}
```

The store runs a flush loop during the start which performs writing task to the underlying datastore in a separate routine. This way writes are controlled and manageable from one place allowing:

* `Append`s not blocked on long disk IO writes and underlying DB compactions
* Helps with batching header writes

`Append` appends a list of headers to the store head. It requires that all headers to be appended are adjacent to each other (sequential). Also, append invokes adjacency header verification by calling the `Verify` header interface method to ensure that only verified headers are appended. As described above, append does not directly writes to the underlying datastore, which is taken care by the flush loop.

`Has` method checks if a header with a given hash exists in the store. The check is performed on a cache ([lru.ARCCache][lru.ARCCache]) first, followed by the pending queue which contains headers that are not flushed (written to disk), and finally the datastore. The `Get` method works similar to `Has`, where the retrieval first checks cache, followed by the pending queue, and finally the datastore (disk access).

# References

[1] [datastore][go-datastore]

[2] [lru.ARCCache][lru.ARCCache]

[go-datastore]: https://github.com/ipfs/go-datastore
[lru.ARCCache]: https://github.com/hashicorp/golang-lru
Loading