Files
evm/warp/backend.go
zeekay d5c9b2be62 feat: update to vm v1.0.23 with ZAP-based Serve function
Updates dependencies:
- github.com/luxfi/vm v1.0.22 → v1.0.23 (adds ZAP transport Serve)
- github.com/luxfi/log v1.3.0 → v1.4.1 (fixes module path declaration)
- github.com/luxfi/consensus v1.22.53 → v1.22.56
- github.com/luxfi/crypto v1.17.39 → v1.17.40
- github.com/luxfi/database v1.17.38 → v1.17.39
- github.com/luxfi/geth v1.16.69 → v1.16.73
- github.com/luxfi/p2p v1.18.7 → v1.18.8
- github.com/luxfi/warp v1.18.4 → v1.18.5

The vm package now provides a ZAP-based Serve function by default,
eliminating the need for the grpc build tag for VM plugins.

Also includes various improvements to validators, warp service,
and precompile interfaces.
2026-01-24 12:01:19 -08:00

225 lines
7.6 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package warp
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/luxfi/cache"
"github.com/luxfi/cache/lru"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/vm/chain"
"github.com/luxfi/warp"
"github.com/luxfi/warp/payload"
"github.com/luxfi/crypto"
"github.com/luxfi/evm/plugin/evm/validators/interfaces"
log "github.com/luxfi/log"
)
var (
_ Backend = (*backend)(nil)
errParsingOffChainMessage = errors.New("failed to parse off-chain message")
messageCacheSize = 500
)
type BlockClient interface {
GetAcceptedBlock(ctx context.Context, blockID ids.ID) (chain.Block, error)
}
// Backend tracks signature-eligible warp messages and provides an interface to fetch them.
// The backend is also used to query for warp message signatures by the signature request handler.
type Backend interface {
// AddMessage signs [unsignedMessage] and adds it to the warp backend database
AddMessage(unsignedMessage *warp.UnsignedMessage) error
// GetMessageSignature validates the message and returns the signature of the requested message.
GetMessageSignature(ctx context.Context, message *warp.UnsignedMessage) ([]byte, error)
// GetBlockSignature returns the signature of a hash payload containing blockID if it's the ID of an accepted block.
GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error)
// GetMessage retrieves the [unsignedMessage] from the warp backend database if available
GetMessage(messageHash ids.ID) (*warp.UnsignedMessage, error)
// Verify verifies the signature of the message
Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) error
}
// backend implements Backend, keeps track of warp messages, and generates message signatures.
type backend struct {
networkID uint32
sourceChainID ids.ID
db database.Database
warpSigner warp.Signer
blockClient BlockClient
validatorReader interfaces.ValidatorReader
signatureCache cache.Cacher[ids.ID, []byte]
messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage]
offchainAddressedCallMsgs map[string]*warp.UnsignedMessage
stats *verifierStats
}
// NewBackend creates a new Backend, and initializes the signature cache and message tracking database.
func NewBackend(
networkID uint32,
sourceChainID ids.ID,
warpSigner warp.Signer,
blockClient BlockClient,
validatorReader interfaces.ValidatorReader,
db database.Database,
signatureCache cache.Cacher[ids.ID, []byte],
offchainMessages [][]byte,
) (Backend, error) {
b := &backend{
networkID: networkID,
sourceChainID: sourceChainID,
db: db,
warpSigner: warpSigner,
blockClient: blockClient,
signatureCache: signatureCache,
validatorReader: validatorReader,
messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize),
stats: newVerifierStats(),
offchainAddressedCallMsgs: make(map[string]*warp.UnsignedMessage),
}
return b, b.initOffChainMessages(offchainMessages)
}
func (b *backend) initOffChainMessages(offchainMessages [][]byte) error {
for i, offchainMsg := range offchainMessages {
unsignedMsg, err := warp.ParseUnsignedMessage(offchainMsg)
if err != nil {
return fmt.Errorf("%w at index %d: %w", errParsingOffChainMessage, i, err)
}
if unsignedMsg.NetworkID != b.networkID {
return fmt.Errorf("wrong network ID at index %d", i)
}
// Compare source chain IDs
if !bytes.Equal(unsignedMsg.SourceChainID[:], b.sourceChainID[:]) {
return fmt.Errorf("wrong source chain ID at index %d", i)
}
_, err = payload.ParsePayload(unsignedMsg.Payload)
if err != nil {
return fmt.Errorf("%w at index %d as AddressedCall: %w", errParsingOffChainMessage, i, err)
}
messageID := unsignedMsg.ID()
msgIDHash := ids.ID(crypto.Keccak256Hash(messageID[:]))
b.offchainAddressedCallMsgs[msgIDHash.String()] = unsignedMsg
}
return nil
}
func (b *backend) AddMessage(unsignedMessage *warp.UnsignedMessage) error {
messageIDBytes := unsignedMessage.ID()
messageID := ids.ID(crypto.Keccak256Hash(messageIDBytes[:]))
log.Debug("Adding warp message to backend", "messageID", messageID)
// In the case when a node restarts, and possibly changes its bls key, the cache gets emptied but the database does not.
// So to avoid having incorrect signatures saved in the database after a bls key change, we save the full message in the database.
// Whereas for the cache, after the node restart, the cache would be emptied so we can directly save the signatures.
if err := b.db.Put(messageID[:], unsignedMessage.Bytes()); err != nil {
return fmt.Errorf("failed to put warp signature in db: %w", err)
}
if _, err := b.signMessage(unsignedMessage); err != nil {
return fmt.Errorf("failed to sign warp message: %w", err)
}
return nil
}
func (b *backend) GetMessageSignature(ctx context.Context, unsignedMessage *warp.UnsignedMessage) ([]byte, error) {
messageIDBytes := unsignedMessage.ID()
messageID := ids.ID(crypto.Keccak256Hash(messageIDBytes[:]))
log.Debug("Getting warp message from backend", "messageID", messageID)
if sig, ok := b.signatureCache.Get(messageID); ok {
return sig, nil
}
if err := b.Verify(ctx, unsignedMessage, nil); err != nil {
return nil, fmt.Errorf("failed to validate warp message: %w", err)
}
return b.signMessage(unsignedMessage)
}
func (b *backend) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) {
log.Debug("Getting block from backend", "blockID", blockID)
blockHashPayload, err := payload.NewHash(blockID[:])
if err != nil {
return nil, fmt.Errorf("failed to create new block hash payload: %w", err)
}
unsignedMessage, err := warp.NewUnsignedMessage(b.networkID, b.sourceChainID, blockHashPayload.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to create new unsigned warp message: %w", err)
}
messageIDBytes := unsignedMessage.ID()
messageID := ids.ID(crypto.Keccak256Hash(messageIDBytes[:]))
if sig, ok := b.signatureCache.Get(messageID); ok {
return sig, nil
}
if err := b.verifyBlockMessage(ctx, blockHashPayload); err != nil {
return nil, fmt.Errorf("failed to validate block message: %w", err)
}
sig, err := b.signMessage(unsignedMessage)
if err != nil {
return nil, fmt.Errorf("failed to sign block message: %w", err)
}
return sig, nil
}
func (b *backend) GetMessage(messageID ids.ID) (*warp.UnsignedMessage, error) {
if message, ok := b.messageCache.Get(messageID); ok {
return message, nil
}
messageIDStr := messageID.String()
if message, ok := b.offchainAddressedCallMsgs[messageIDStr]; ok {
return message, nil
}
unsignedMessageBytes, err := b.db.Get(messageID[:])
if err != nil {
return nil, err
}
unsignedMessage, err := warp.ParseUnsignedMessage(unsignedMessageBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse unsigned message %s: %w", messageID.String(), err)
}
b.messageCache.Put(messageID, unsignedMessage)
return unsignedMessage, nil
}
func (b *backend) signMessage(unsignedMessage *warp.UnsignedMessage) ([]byte, error) {
if b.warpSigner == nil {
return nil, errors.New("warp signer not configured")
}
sig, err := b.warpSigner.Sign(unsignedMessage)
if err != nil {
return nil, fmt.Errorf("failed to sign message: %w", err)
}
messageIDBytes := unsignedMessage.ID()
messageID := ids.ID(crypto.Keccak256Hash(messageIDBytes[:]))
b.signatureCache.Put(messageID, sig)
return sig, nil
}