Files
node/x/sync/client.go
T

393 lines
11 KiB
Go

//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sync
import (
"context"
"errors"
"fmt"
"math"
"sync/atomic"
"time"
"google.golang.org/protobuf/proto"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/x/merkledb"
"github.com/luxfi/container/maybe"
pb "github.com/luxfi/node/proto/pb/sync"
)
const (
initialRetryWait = 10 * time.Millisecond
maxRetryWait = time.Second
retryWaitFactor = 1.5 // Larger --> timeout grows more quickly
epsilon = 1e-6 // small amount to add to time to avoid division by 0
)
var (
_ Client = (*client)(nil)
errInvalidRangeProof = errors.New("failed to verify range proof")
errInvalidChangeProof = errors.New("failed to verify change proof")
errTooManyKeys = errors.New("response contains more than requested keys")
errTooManyBytes = errors.New("response contains more than requested bytes")
errUnexpectedChangeProofResponse = errors.New("unexpected response type")
)
// ChangeOrRangeProof contains either a ChangeProof or RangeProof.
// Exactly one of ChangeProof or RangeProof should be non-nil.
type ChangeOrRangeProof struct {
ChangeProof *merkledb.ChangeProof
RangeProof *merkledb.RangeProof
}
// Client synchronously fetches data from the network
// to fulfill state sync requests.
// Repeatedly retries failed requests until the context is canceled.
type Client interface {
// GetRangeProof synchronously sends the given request
// and returns the parsed response.
// This method verifies the range proof before returning it.
GetRangeProof(
ctx context.Context,
request *pb.SyncGetRangeProofRequest,
) (*merkledb.RangeProof, error)
// GetChangeProof synchronously sends the given request
// and returns the parsed response.
// This method verifies the change proof / range proof
// before returning it.
// If the server responds with a change proof,
// it's verified using [verificationDB].
GetChangeProof(
ctx context.Context,
request *pb.SyncGetChangeProofRequest,
verificationDB DB,
) (*ChangeOrRangeProof, error)
}
type client struct {
networkClient NetworkClient
stateSyncNodes []ids.NodeID
stateSyncNodeIdx uint32
log log.Logger
metrics SyncMetrics
tokenSize int
hasher merkledb.Hasher
}
type ClientConfig struct {
NetworkClient NetworkClient
StateSyncNodeIDs []ids.NodeID
Log log.Logger
Metrics SyncMetrics
BranchFactor merkledb.BranchFactor
// If not specified, [merkledb.DefaultHasher] will be used.
Hasher merkledb.Hasher
}
func NewClient(config *ClientConfig) (Client, error) {
if err := config.BranchFactor.Valid(); err != nil {
return nil, err
}
hasher := config.Hasher
if hasher == nil {
hasher = merkledb.DefaultHasher
}
return &client{
networkClient: config.NetworkClient,
stateSyncNodes: config.StateSyncNodeIDs,
log: config.Log,
metrics: config.Metrics,
tokenSize: merkledb.BranchFactorToTokenSize[config.BranchFactor],
hasher: hasher,
}, nil
}
// GetChangeProof synchronously retrieves the change proof given by [req].
// Upon failure, retries until the context is expired.
// The returned change proof is verified.
func (c *client) GetChangeProof(
ctx context.Context,
req *pb.SyncGetChangeProofRequest,
db DB,
) (*ChangeOrRangeProof, error) {
parseFn := func(ctx context.Context, responseBytes []byte) (*ChangeOrRangeProof, error) {
if len(responseBytes) > int(req.BytesLimit) {
return nil, fmt.Errorf("%w: (%d) > %d)", errTooManyBytes, len(responseBytes), req.BytesLimit)
}
var changeProofResp pb.SyncGetChangeProofResponse
if err := proto.Unmarshal(responseBytes, &changeProofResp); err != nil {
return nil, err
}
startKey := maybeBytesToMaybe(req.StartKey)
endKey := maybeBytesToMaybe(req.EndKey)
switch changeProofResp := changeProofResp.Response.(type) {
case *pb.SyncGetChangeProofResponse_ChangeProof:
// The server had enough history to send us a change proof
var changeProof merkledb.ChangeProof
if err := changeProof.UnmarshalProto(changeProofResp.ChangeProof); err != nil {
return nil, err
}
// Ensure the response does not contain more than the requested number of leaves
// and the start and end roots match the requested roots.
if len(changeProof.KeyChanges) > int(req.KeyLimit) {
return nil, fmt.Errorf(
"%w: (%d) > %d)",
errTooManyKeys, len(changeProof.KeyChanges), req.KeyLimit,
)
}
endRoot, err := ids.ToID(req.EndRootHash)
if err != nil {
return nil, err
}
if err := db.VerifyChangeProof(
ctx,
&changeProof,
startKey,
endKey,
endRoot,
); err != nil {
return nil, fmt.Errorf("%w due to %w", errInvalidChangeProof, err)
}
return &ChangeOrRangeProof{
ChangeProof: &changeProof,
}, nil
case *pb.SyncGetChangeProofResponse_RangeProof:
var rangeProof merkledb.RangeProof
if err := rangeProof.UnmarshalProto(changeProofResp.RangeProof); err != nil {
return nil, err
}
// The server did not have enough history to send us a change proof
// so they sent a range proof instead.
err := verifyRangeProof(
ctx,
&rangeProof,
int(req.KeyLimit),
startKey,
endKey,
req.EndRootHash,
c.tokenSize,
c.hasher,
)
if err != nil {
return nil, err
}
return &ChangeOrRangeProof{
RangeProof: &rangeProof,
}, nil
default:
return nil, fmt.Errorf(
"%w: %T",
errUnexpectedChangeProofResponse, changeProofResp,
)
}
}
reqBytes, err := proto.Marshal(req)
if err != nil {
return nil, err
}
return getAndParse(ctx, c, reqBytes, parseFn)
}
// Verify [rangeProof] is a valid range proof for keys in [start, end] for
// root [rootBytes]. Returns [errTooManyKeys] if the response contains more
// than [keyLimit] keys.
func verifyRangeProof(
ctx context.Context,
rangeProof *merkledb.RangeProof,
keyLimit int,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
rootBytes []byte,
tokenSize int,
hasher merkledb.Hasher,
) error {
root, err := ids.ToID(rootBytes)
if err != nil {
return err
}
// Ensure the response does not contain more than the maximum requested number of leaves.
if len(rangeProof.KeyChanges) > keyLimit {
return fmt.Errorf(
"%w: (%d) > %d)",
errTooManyKeys, len(rangeProof.KeyChanges), keyLimit,
)
}
if err := rangeProof.Verify(
ctx,
start,
end,
root,
tokenSize,
hasher,
); err != nil {
return fmt.Errorf("%w due to %w", errInvalidRangeProof, err)
}
return nil
}
// GetRangeProof synchronously retrieves the range proof given by [req].
// Upon failure, retries until the context is expired.
// The returned range proof is verified.
func (c *client) GetRangeProof(
ctx context.Context,
req *pb.SyncGetRangeProofRequest,
) (*merkledb.RangeProof, error) {
parseFn := func(ctx context.Context, responseBytes []byte) (*merkledb.RangeProof, error) {
if len(responseBytes) > int(req.BytesLimit) {
return nil, fmt.Errorf(
"%w: (%d) > %d)",
errTooManyBytes, len(responseBytes), req.BytesLimit,
)
}
var rangeProofProto pb.RangeProof
if err := proto.Unmarshal(responseBytes, &rangeProofProto); err != nil {
return nil, err
}
var rangeProof merkledb.RangeProof
if err := rangeProof.UnmarshalProto(&rangeProofProto); err != nil {
return nil, err
}
if err := verifyRangeProof(
ctx,
&rangeProof,
int(req.KeyLimit),
maybeBytesToMaybe(req.StartKey),
maybeBytesToMaybe(req.EndKey),
req.RootHash,
c.tokenSize,
c.hasher,
); err != nil {
return nil, err
}
return &rangeProof, nil
}
reqBytes, err := proto.Marshal(req)
if err != nil {
return nil, err
}
return getAndParse(ctx, c, reqBytes, parseFn)
}
// getAndParse uses [client] to send [request] to an arbitrary peer.
// Returns the response to the request.
// [parseFn] parses the raw response.
// If the request is unsuccessful or the response can't be parsed,
// retries the request to a different peer until [ctx] expires.
// Returns [errAppSendFailed] if we fail to send an Request/Response.
// This should be treated as a fatal error.
func getAndParse[T any](
ctx context.Context,
client *client,
request []byte,
parseFn func(context.Context, []byte) (*T, error),
) (*T, error) {
var (
lastErr error
response *T
)
// Loop until the context is cancelled or we get a valid response.
for attempt := 1; ; attempt++ {
nodeID, responseBytes, err := client.get(ctx, request)
if err == nil {
if response, err = parseFn(ctx, responseBytes); err == nil {
return response, nil
}
}
if errors.Is(err, errAppSendFailed) {
// Failing to send an Request is a fatal error.
return nil, err
}
client.log.Debug("request failed, retrying",
log.Stringer("nodeID", nodeID),
log.Int("attempt", attempt),
log.Reflect("error", err),
)
// if [err] is being propagated from [ctx], avoid overwriting [lastErr].
if err != ctx.Err() {
lastErr = err
}
retryWait := initialRetryWait * time.Duration(math.Pow(retryWaitFactor, float64(attempt)))
if retryWait > maxRetryWait || retryWait < 0 { // Handle overflows with negative check.
retryWait = maxRetryWait
}
select {
case <-ctx.Done():
if lastErr != nil {
// prefer reporting [lastErr] if it's not nil.
return nil, fmt.Errorf(
"request failed after %d attempts with last error %w and ctx error %w",
attempt, lastErr, ctx.Err(),
)
}
return nil, ctx.Err()
case <-time.After(retryWait):
}
}
}
// get sends [request] to an arbitrary peer and blocks
// until the node receives a response, failure notification
// or [ctx] is canceled.
// Returns the peer's NodeID and response.
// Returns [errAppSendFailed] if we failed to send an Request/Response.
// This should be treated as fatal.
// It's safe to call this method multiple times concurrently.
func (c *client) get(ctx context.Context, request []byte) (ids.NodeID, []byte, error) {
var (
response []byte
nodeID ids.NodeID
err error
)
c.metrics.RequestMade()
if len(c.stateSyncNodes) == 0 {
nodeID, response, err = c.networkClient.RequestAny(ctx, request)
} else {
// Get the next nodeID to query using the [nodeIdx] offset.
// If we're out of nodes, loop back to 0.
// We do this try to query a different node each time if possible.
nodeIdx := atomic.AddUint32(&c.stateSyncNodeIdx, 1)
nodeID = c.stateSyncNodes[nodeIdx%uint32(len(c.stateSyncNodes))]
response, err = c.networkClient.Request(ctx, nodeID, request)
}
if err != nil {
c.metrics.RequestFailed()
return nodeID, response, err
}
c.metrics.RequestSucceeded()
return nodeID, response, nil
}