Skip to content

Commit

Permalink
initial specs
Browse files Browse the repository at this point in the history
  • Loading branch information
Ganesha Upadhyaya authored and Ganesha Upadhyaya committed Oct 11, 2023
1 parent df01474 commit 8f7e5e1
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 1 deletion.
198 changes: 198 additions & 0 deletions p2p/p2p.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# P2P

The p2p package mainly contains two services:

1) Subscriber
2) Exchange

## Subscriber

Subscriber is a service that manages gossip of headers among the nodes in the p2p network by using [libp2p][libp2p] and its [pubsub][pubsub] modules. The pubsub topic used for gossip (`/<networkID>/header-sub/v0.0.1`) is configurable based on `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.

## 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], list of peers in the form of slice [peer.IDSlice][peer], and [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.

#### Peer Tracker

//TODO

#### Trusted Peers vs Tracked Peers

//TODO

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)
}
// 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 option `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:
* 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;
}
```
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](../specs/src/specs/store.md). 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](../specs/src/specs/store.md) 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

//TODO

# References

[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
53 changes: 52 additions & 1 deletion specs/src/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,54 @@
# Welcome

Welcome to the go-header Specifications.
Welcome to the go-header Specifications.

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.

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

The go-header library makes it easy to consume by other projects by defining a clear interface (as described below). An example consumer 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
104 changes: 104 additions & 0 deletions store/store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 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

[go-datastore]: https://github.com/ipfs/go-datastore
[lru.ARCCache]: https://github.com/hashicorp/golang-lru
14 changes: 14 additions & 0 deletions sync/sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Sync

Syncer implements efficient synchronization for headers.

There are two main processes running in Syncer:
* Main syncing loop(`syncLoop`)
* Performs syncing from the latest stored header up to the latest known Subjective Head
* Syncs by requesting missing headers from Exchange or
* By accessing cache of pending headers
* Receives every new Network Head from PubSub gossip subnetwork (`incomingNetworkHead`)
* Validates against the latest known Subjective Head, is so
* Sets as the new Subjective Head, which
* If there is a gap between the previous and the new Subjective Head
* Triggers s.syncLoop and saves the Subjective Head in the pending so s.syncLoop can access it

0 comments on commit 8f7e5e1

Please sign in to comment.