mirror of
https://github.com/luxfi/warp.git
synced 2026-07-27 03:37:46 +00:00
feat: add Verifier interface and signature handling types
Move warp-specific types from lp118 to the warp package: - Add Verifier interface for warp message verification - Add SignatureRequest/SignatureResponse with binary marshaling - Add SignatureHandler, CachedSignatureHandler, SignatureHandlerAdapter - Add SignatureAggregator for validator signature aggregation This consolidates warp functionality and maintains clear separation between p2p networking (github.com/luxfi/p2p) and warp-specific types (github.com/luxfi/warp).
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package warp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/luxfi/log"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/p2p"
|
||||
)
|
||||
|
||||
var errFailedVerification = errors.New("failed verification")
|
||||
|
||||
type indexedValidator struct {
|
||||
*Validator
|
||||
Index int
|
||||
}
|
||||
|
||||
type aggregatorResult struct {
|
||||
NodeID ids.NodeID
|
||||
Validator indexedValidator
|
||||
Signature *bls.Signature
|
||||
Err error
|
||||
}
|
||||
|
||||
// NewSignatureAggregator returns an instance of SignatureAggregator
|
||||
func NewSignatureAggregator(log log.Logger, client *p2p.Client) *SignatureAggregator {
|
||||
return &SignatureAggregator{
|
||||
log: log,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureAggregator aggregates validator signatures for warp messages
|
||||
type SignatureAggregator struct {
|
||||
log log.Logger
|
||||
client *p2p.Client
|
||||
}
|
||||
|
||||
// AggregateSignatures blocks until quorumNum/quorumDen signatures from
|
||||
// validators are requested to be aggregated into a warp message or the context
|
||||
// is canceled. Returns the signed message and the amount of stake that signed
|
||||
// the message. Caller is responsible for providing a well-formed canonical
|
||||
// validator set corresponding to the signer bitset in the message.
|
||||
func (s *SignatureAggregator) AggregateSignatures(
|
||||
ctx context.Context,
|
||||
message *Message,
|
||||
justification []byte,
|
||||
validators []*Validator,
|
||||
quorumNum uint64,
|
||||
quorumDen uint64,
|
||||
) (
|
||||
_ *Message,
|
||||
aggregatedStake *big.Int,
|
||||
totalStake *big.Int,
|
||||
_ error,
|
||||
) {
|
||||
request := &SignatureRequest{
|
||||
Message: message.UnsignedMessage.Bytes(),
|
||||
Justification: justification,
|
||||
}
|
||||
|
||||
requestBytes, err := MarshalSignatureRequest(request)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to marshal signature request: %w", err)
|
||||
}
|
||||
|
||||
nodeIDsToValidator := make(map[ids.NodeID]indexedValidator)
|
||||
// TODO expose concrete type to avoid type casting
|
||||
bitSetSignature, ok := message.Signature.(*BitSetSignature)
|
||||
if !ok {
|
||||
return nil, nil, nil, errors.New("invalid warp signature type")
|
||||
}
|
||||
|
||||
signerBitSet := set.BitsFromBytes(bitSetSignature.Signers)
|
||||
|
||||
nonSigners := make([]ids.NodeID, 0, len(validators))
|
||||
aggregatedStakeWeight := new(big.Int)
|
||||
totalStakeWeight := new(big.Int)
|
||||
for i, validator := range validators {
|
||||
totalStakeWeight.Add(totalStakeWeight, new(big.Int).SetUint64(validator.Weight))
|
||||
|
||||
// Only try to aggregate signatures from validators that are not already in
|
||||
// the signer bit set
|
||||
if signerBitSet.Contains(i) {
|
||||
aggregatedStakeWeight.Add(aggregatedStakeWeight, new(big.Int).SetUint64(validator.Weight))
|
||||
continue
|
||||
}
|
||||
|
||||
nodeID := validator.NodeID
|
||||
|
||||
v := indexedValidator{
|
||||
Index: i,
|
||||
Validator: validator,
|
||||
}
|
||||
|
||||
nodeIDsToValidator[nodeID] = v
|
||||
nonSigners = append(nonSigners, nodeID)
|
||||
}
|
||||
|
||||
// Account for requested signatures + the signature that was provided
|
||||
signatures := make([]*bls.Signature, 0, len(nonSigners)+1)
|
||||
if bitSetSignature.Signature != [bls.SignatureLen]byte{} {
|
||||
blsSignature, err := bls.SignatureFromBytes(bitSetSignature.Signature[:])
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to parse bls signature: %w", err)
|
||||
}
|
||||
signatures = append(signatures, blsSignature)
|
||||
}
|
||||
|
||||
results := make(chan aggregatorResult)
|
||||
handler := signatureResponseHandler{
|
||||
message: message,
|
||||
nodeIDsToValidators: nodeIDsToValidator,
|
||||
results: results,
|
||||
}
|
||||
|
||||
if err := s.client.Request(ctx, set.Of(nonSigners...), requestBytes, handler.HandleResponse); err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to send aggregation request: %w", err)
|
||||
}
|
||||
|
||||
minThreshold := new(big.Int).Mul(totalStakeWeight, new(big.Int).SetUint64(quorumNum))
|
||||
minThreshold.Div(minThreshold, new(big.Int).SetUint64(quorumDen))
|
||||
|
||||
// Block until:
|
||||
// 1. The context is cancelled
|
||||
// 2. We get responses from all validators
|
||||
// 3. The specified security threshold is reached
|
||||
for i := 0; i < len(nonSigners); i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Try to return whatever progress we have if the context is cancelled
|
||||
msg, err := newAggregatedMessage(message, signerBitSet, signatures)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return msg, aggregatedStakeWeight, totalStakeWeight, nil
|
||||
case result := <-results:
|
||||
if result.Err != nil {
|
||||
s.log.Debug(
|
||||
"dropping response",
|
||||
log.Stringer("nodeID", result.NodeID),
|
||||
log.Err(result.Err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Validators may share public keys so drop any duplicate signatures
|
||||
if signerBitSet.Contains(result.Validator.Index) {
|
||||
s.log.Debug(
|
||||
"dropping duplicate signature",
|
||||
log.Stringer("nodeID", result.NodeID),
|
||||
log.Err(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
signatures = append(signatures, result.Signature)
|
||||
signerBitSet.Add(result.Validator.Index)
|
||||
aggregatedStakeWeight.Add(aggregatedStakeWeight, new(big.Int).SetUint64(result.Validator.Weight))
|
||||
|
||||
if aggregatedStakeWeight.Cmp(minThreshold) != -1 {
|
||||
msg, err := newAggregatedMessage(message, signerBitSet, signatures)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return msg, aggregatedStakeWeight, totalStakeWeight, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg, err := newAggregatedMessage(message, signerBitSet, signatures)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return msg, aggregatedStakeWeight, totalStakeWeight, nil
|
||||
}
|
||||
|
||||
func newAggregatedMessage(
|
||||
message *Message,
|
||||
signerBitSet set.Bits,
|
||||
signatures []*bls.Signature,
|
||||
) (*Message, error) {
|
||||
if len(signatures) == 0 {
|
||||
return message, nil
|
||||
}
|
||||
|
||||
aggregateSignature, err := bls.AggregateSignatures(signatures)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bitSetSignature := &BitSetSignature{
|
||||
Signers: signerBitSet.Bytes(),
|
||||
Signature: [bls.SignatureLen]byte{},
|
||||
}
|
||||
copy(bitSetSignature.Signature[:], bls.SignatureToBytes(aggregateSignature))
|
||||
|
||||
return NewMessage(message.UnsignedMessage, bitSetSignature)
|
||||
}
|
||||
|
||||
type signatureResponseHandler struct {
|
||||
message *Message
|
||||
nodeIDsToValidators map[ids.NodeID]indexedValidator
|
||||
results chan aggregatorResult
|
||||
}
|
||||
|
||||
func (r *signatureResponseHandler) HandleResponse(
|
||||
_ context.Context,
|
||||
nodeID ids.NodeID,
|
||||
responseBytes []byte,
|
||||
err error,
|
||||
) {
|
||||
validator := r.nodeIDsToValidators[nodeID]
|
||||
if err != nil {
|
||||
r.results <- aggregatorResult{NodeID: nodeID, Validator: validator, Err: err}
|
||||
return
|
||||
}
|
||||
|
||||
response, err := UnmarshalSignatureResponse(responseBytes)
|
||||
if err != nil {
|
||||
r.results <- aggregatorResult{NodeID: nodeID, Validator: validator, Err: err}
|
||||
return
|
||||
}
|
||||
|
||||
signature, err := bls.SignatureFromBytes(response.Signature)
|
||||
if err != nil {
|
||||
r.results <- aggregatorResult{NodeID: nodeID, Validator: validator, Err: err}
|
||||
return
|
||||
}
|
||||
|
||||
if !bls.Verify(validator.PublicKey, signature, r.message.UnsignedMessage.Bytes()) {
|
||||
r.results <- aggregatorResult{NodeID: nodeID, Validator: validator, Err: errFailedVerification}
|
||||
return
|
||||
}
|
||||
|
||||
r.results <- aggregatorResult{NodeID: nodeID, Validator: validator, Signature: signature}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package warp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/p2p"
|
||||
)
|
||||
|
||||
// SignatureHandlerID is the protocol ID for warp signature handling
|
||||
const SignatureHandlerID = 0x12345678
|
||||
|
||||
// SignatureRequest represents a request for a warp signature
|
||||
type SignatureRequest struct {
|
||||
Message []byte
|
||||
Justification []byte
|
||||
}
|
||||
|
||||
// SignatureResponse represents a warp signature response
|
||||
type SignatureResponse struct {
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// MarshalSignatureRequest marshals a signature request to bytes
|
||||
func MarshalSignatureRequest(req *SignatureRequest) ([]byte, error) {
|
||||
// Format: msgLen(4) + msg + justLen(4) + just
|
||||
msgLen := len(req.Message)
|
||||
justLen := len(req.Justification)
|
||||
buf := make([]byte, 4+msgLen+4+justLen)
|
||||
binary.BigEndian.PutUint32(buf[0:4], uint32(msgLen))
|
||||
copy(buf[4:4+msgLen], req.Message)
|
||||
binary.BigEndian.PutUint32(buf[4+msgLen:8+msgLen], uint32(justLen))
|
||||
copy(buf[8+msgLen:], req.Justification)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// UnmarshalSignatureRequest unmarshals bytes to a signature request
|
||||
func UnmarshalSignatureRequest(data []byte) (*SignatureRequest, error) {
|
||||
if len(data) < 8 {
|
||||
return nil, fmt.Errorf("data too short: %d", len(data))
|
||||
}
|
||||
msgLen := binary.BigEndian.Uint32(data[0:4])
|
||||
if len(data) < int(4+msgLen+4) {
|
||||
return nil, fmt.Errorf("data too short for message: %d", len(data))
|
||||
}
|
||||
justLen := binary.BigEndian.Uint32(data[4+msgLen : 8+msgLen])
|
||||
if len(data) < int(8+msgLen+justLen) {
|
||||
return nil, fmt.Errorf("data too short for justification: %d", len(data))
|
||||
}
|
||||
return &SignatureRequest{
|
||||
Message: data[4 : 4+msgLen],
|
||||
Justification: data[8+msgLen : 8+msgLen+justLen],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MarshalSignatureResponse marshals a signature response to bytes
|
||||
func MarshalSignatureResponse(signature []byte) ([]byte, error) {
|
||||
// Format: sigLen(4) + sig
|
||||
buf := make([]byte, 4+len(signature))
|
||||
binary.BigEndian.PutUint32(buf[0:4], uint32(len(signature)))
|
||||
copy(buf[4:], signature)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// UnmarshalSignatureResponse unmarshals bytes to a signature response
|
||||
func UnmarshalSignatureResponse(data []byte) (*SignatureResponse, error) {
|
||||
if len(data) < 4 {
|
||||
return nil, fmt.Errorf("data too short: %d", len(data))
|
||||
}
|
||||
sigLen := binary.BigEndian.Uint32(data[0:4])
|
||||
if len(data) < int(4+sigLen) {
|
||||
return nil, fmt.Errorf("data too short for signature: %d", len(data))
|
||||
}
|
||||
return &SignatureResponse{
|
||||
Signature: data[4 : 4+sigLen],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignatureHandler handles warp signature requests
|
||||
type SignatureHandler interface {
|
||||
// Request handles an incoming signature request
|
||||
Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, request []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// NoOpSignatureHandler is a no-op implementation of SignatureHandler
|
||||
type NoOpSignatureHandler struct{}
|
||||
|
||||
// Request returns an empty response
|
||||
func (NoOpSignatureHandler) Request(context.Context, ids.NodeID, time.Time, []byte) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SignatureCacher provides caching for signature responses
|
||||
type SignatureCacher[K comparable, V any] interface {
|
||||
Get(key K) (V, bool)
|
||||
Put(key K, value V)
|
||||
}
|
||||
|
||||
// CachedSignatureHandler implements a cached handler for warp signatures
|
||||
type CachedSignatureHandler struct {
|
||||
cache SignatureCacher[ids.ID, []byte]
|
||||
backend interface{}
|
||||
signer Signer
|
||||
}
|
||||
|
||||
// NewCachedSignatureHandler creates a new cached signature handler
|
||||
func NewCachedSignatureHandler(cache SignatureCacher[ids.ID, []byte], backend interface{}, signer Signer) SignatureHandler {
|
||||
return &CachedSignatureHandler{
|
||||
cache: cache,
|
||||
backend: backend,
|
||||
signer: signer,
|
||||
}
|
||||
}
|
||||
|
||||
// Request handles an incoming signature request with caching
|
||||
func (h *CachedSignatureHandler) Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, request []byte) ([]byte, error) {
|
||||
req, err := UnmarshalSignatureRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsignedMessage, err := ParseUnsignedMessage(req.Message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check cache
|
||||
messageID := unsignedMessage.ID()
|
||||
if signatureBytes, ok := h.cache.Get(messageID); ok {
|
||||
return MarshalSignatureResponse(signatureBytes)
|
||||
}
|
||||
|
||||
// Verify if backend is a Verifier
|
||||
if verifier, ok := h.backend.(Verifier); ok {
|
||||
if appErr := verifier.Verify(ctx, unsignedMessage, req.Justification); appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
}
|
||||
|
||||
// Sign the message
|
||||
if h.signer == nil {
|
||||
return nil, fmt.Errorf("signer is nil")
|
||||
}
|
||||
signatureBytes, err := h.signer.Sign(unsignedMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.cache.Put(messageID, signatureBytes)
|
||||
|
||||
return MarshalSignatureResponse(signatureBytes)
|
||||
}
|
||||
|
||||
// Ensure SignatureHandlerAdapter implements p2p.Handler
|
||||
var _ p2p.Handler = (*SignatureHandlerAdapter)(nil)
|
||||
|
||||
// SignatureHandlerAdapter adapts a SignatureHandler to the p2p.Handler interface.
|
||||
// This allows warp signature handlers to be registered with the p2p router.
|
||||
type SignatureHandlerAdapter struct {
|
||||
handler SignatureHandler
|
||||
}
|
||||
|
||||
// NewSignatureHandlerAdapter creates a new adapter that wraps a SignatureHandler
|
||||
// and implements the p2p.Handler interface.
|
||||
func NewSignatureHandlerAdapter(handler SignatureHandler) *SignatureHandlerAdapter {
|
||||
return &SignatureHandlerAdapter{handler: handler}
|
||||
}
|
||||
|
||||
// Gossip implements p2p.Handler. Signature handlers do not use gossip.
|
||||
func (a *SignatureHandlerAdapter) Gossip(ctx context.Context, nodeID ids.NodeID, gossipBytes []byte) {
|
||||
// Signature handlers do not use Gossip
|
||||
}
|
||||
|
||||
// Request implements p2p.Handler by delegating to the wrapped SignatureHandler.
|
||||
func (a *SignatureHandlerAdapter) Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *p2p.Error) {
|
||||
response, err := a.handler.Request(ctx, nodeID, deadline, requestBytes)
|
||||
if err != nil {
|
||||
return nil, &p2p.Error{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package warp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSignatureRequestMarshal(t *testing.T) {
|
||||
req := &SignatureRequest{
|
||||
Message: []byte("test message"),
|
||||
Justification: []byte("justification"),
|
||||
}
|
||||
|
||||
data, err := MarshalSignatureRequest(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
decoded, err := UnmarshalSignatureRequest(data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if string(decoded.Message) != string(req.Message) {
|
||||
t.Errorf("message mismatch: expected %s, got %s", req.Message, decoded.Message)
|
||||
}
|
||||
if string(decoded.Justification) != string(req.Justification) {
|
||||
t.Errorf("justification mismatch: expected %s, got %s", req.Justification, decoded.Justification)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureResponseMarshal(t *testing.T) {
|
||||
signature := []byte("test signature bytes")
|
||||
|
||||
data, err := MarshalSignatureResponse(signature)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
decoded, err := UnmarshalSignatureResponse(data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if string(decoded.Signature) != string(signature) {
|
||||
t.Errorf("signature mismatch: expected %s, got %s", signature, decoded.Signature)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureUnmarshalErrors(t *testing.T) {
|
||||
// Test empty data
|
||||
_, err := UnmarshalSignatureRequest(nil)
|
||||
if err == nil {
|
||||
t.Error("expected error for nil data")
|
||||
}
|
||||
|
||||
_, err = UnmarshalSignatureRequest([]byte{0, 0, 0})
|
||||
if err == nil {
|
||||
t.Error("expected error for short data")
|
||||
}
|
||||
|
||||
_, err = UnmarshalSignatureResponse(nil)
|
||||
if err == nil {
|
||||
t.Error("expected error for nil data")
|
||||
}
|
||||
|
||||
_, err = UnmarshalSignatureResponse([]byte{0, 0, 0})
|
||||
if err == nil {
|
||||
t.Error("expected error for short data")
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package warp
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Verifier verifies warp messages before signing
|
||||
type Verifier interface {
|
||||
// Verify verifies an unsigned warp message with justification.
|
||||
// Returns nil on success, or an error if verification fails.
|
||||
Verify(ctx context.Context, unsignedMessage *UnsignedMessage, justification []byte) error
|
||||
}
|
||||
Reference in New Issue
Block a user