Skip to content

Commit

Permalink
chore: Add aync relaymining doc page
Browse files Browse the repository at this point in the history
  • Loading branch information
red-0ne committed Feb 22, 2025
1 parent cad18f1 commit 937983d
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 6 deletions.
54 changes: 54 additions & 0 deletions docusaurus/docs/protocol/primitives/async_service_mining.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Mining Asynchronous Services

::: warning

Note: This documentation describes the behavior as of PR #1073. Implementation
details may change as the protocol evolves.

:::

The bridge represents a WebSocket bridge between the gateway and the service
backend. It handles the forwarding of relay requests from the gateway to the
service backend and relay responses from the service backend to the gateway.

## Asynchronous Message Handling

Due to the asynchronous nature of WebSockets, there isn't always a 1:1 mapping
between requests and responses. The bridge must handle two common scenarios:

### 1. Many Responses for Few Requests (M-resp >> N-req)

In this scenario, a single request can trigger multiple responses over time.
For example:
- A client subscribes once to an event stream (eth_subscribe)
- The client receives many event notifications over time through that single
subscription

### 2. Many Requests for Few Responses (N-req >> M-resp)

In this scenario, multiple requests may be associated with fewer responses.
For example:
- A client uploads a large file in chunks, sending many requests
- The server only occasionally sends progress updates

## Design Implications

This asynchronous design has two important implications:

1. **Reward Eligibility**: Each message (inbound or outbound) is treated as a
reward-eligible relay. For example, with eth_subscribe, both the initial
subscription request and each received event would be eligible for rewards.

2. **Message Pairing**: To maintain protocol compatibility, the bridge must always
pair messages when submitting to the miner. It does this by combining the most
recent request with the most recent response.

## Future Considerations

Currently, the RelayMiner is paid for each incoming and outgoing message
transmitted. While this is the most common and trivial use case, future services
might have different payable units of work (e.g. packet size, specific packet or
data delimiter...).

To support these use cases, the bridge should be extensible to allow for custom
units of work to be metered and paid.
15 changes: 9 additions & 6 deletions pkg/relayer/proxy/websockets/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,10 @@ func NewBridge(
return nil, ErrWebsocketsBridge.Wrapf("failed to connect to the service backend: %v", err)
}

// Use an observable instead of direct stopChan reads to prevent race conditions
// when terminating connections. Since both serviceBackendConn and gatewayConn
// need to read the stop signal, reading directly from stopChan could result in
// one connection missing the signal if both try to read simultaneously. The
// observable pattern ensures all components receive the termination notification.
// Using an observable prevents race conditions during connection termination:
// - Both serviceBackendConn and gatewayConn need to read the stop signal
// - Direct stopChan reads could cause one connection to miss the signal during simultaneous reads
// - Observable pattern ensures all components receive termination notifications
stopBridgeObservable, stopChan := channel.NewObservable[error]()
msgChan := make(chan message)
ctx, cancelCtx := context.WithCancel(context.Background())
Expand Down Expand Up @@ -255,6 +254,7 @@ func (b *bridge) handleGatewayIncomingMessage(msg message) {
// Store the latest relay request to guarantee that there is always a request/response.
// This is to guarantee that any mined Relay will contain a request/response pair
// which is a requirement for the protocol's proof verification.
// E.g. The latest eth_subscribe RelayRequest will be mapped to multiple responses.
b.setLatestRelayRequest(&relayRequest)

relayer.RelaysTotal.With(
Expand Down Expand Up @@ -308,6 +308,7 @@ func (b *bridge) handleGatewayIncomingMessage(msg message) {
// Accumulate the relay reward.
// The asynchronous flow assumes that every inbound and outbound message is a
// payment-eligible relay.
// Recall that num inbound messages is unlikely to equal num outbound messages in a websocket.
if err := b.relayMeter.AccumulateRelayReward(b.ctx, relayRequest.Meta); err != nil {
b.serviceBackendConn.handleError(
ErrWebsocketsGatewayMessage.Wrapf("failed to accumulate relay reward: %v", err),
Expand Down Expand Up @@ -356,9 +357,10 @@ func (b *bridge) handleServiceBackendIncomingMessage(msg message) {

logger.Debug().Msg("relay response signed")

// Store the latest relay response to guarantee that there is always a request/response,
// Store the latest relay response to guarantee that there is always a request/response.
// This is to guarantee that any mined Relay will contain a request/response pair
// which is a requirement for the protocol's proof verification.
// E.g. The latest RelayResponse will be mapped to the initial eth_subscribe RelayRequest.
b.setLatestRelayResponse(relayResponse)

relayResponseBz, err := relayResponse.Marshal()
Expand Down Expand Up @@ -402,6 +404,7 @@ func (b *bridge) handleServiceBackendIncomingMessage(msg message) {
// Accumulate the relay reward.
// The asynchronous flow assumes that every inbound and outbound message is a
// payment-eligible relay.
// Recall that num inbound messages is unlikely to equal num outbound messages in a websocket.
if err := b.relayMeter.AccumulateRelayReward(b.ctx, b.latestRelayRequest.Meta); err != nil {
b.gatewayConn.handleError(
ErrWebsocketsServiceBackendMessage.Wrapf("failed to accumulate relay reward: %v", err),
Expand Down

0 comments on commit 937983d

Please sign in to comment.