Files
zeekayandHanzo Dev 9fb1642e1f fix: follow warp v1.24 folding UnsignedMessage into Message
This module would not build against the warp it resolves. warp v1.24 removed the
UnsignedMessage / Message split, and the rename is a rotation, not a substitution
— every name shifted one place along:

  UnsignedMessage       -> Message       (the signed subject)
  NewUnsignedMessage    -> NewMessage
  ParseUnsignedMessage  -> ParseMessage
  Message               -> Envelope      (subject + lanes)
  NewMessage            -> NewEnvelope
  ParseMessage          -> ParseEnvelope
  VerifyMessage         -> VerifyEnvelope
  .UnsignedMessage      -> .Message
  .Signature            -> .Beam

Applying it in the obvious order silently conflates the two Message meanings, so
the rewrite goes through placeholders.

The fold is a security fix upstream, not cosmetic: Message now carries the Pulsar
lineage (SourceNebulaRoot, SourceKeyEraID, SourceGeneration, HashSuiteID) inside
the signed subject. Under the old split the BLS Beam signed only the unsigned
body and that lineage was Beam-unauthenticated.

VerifyEnvelope takes the same five arguments VerifyMessage did and is documented
as the canonical classical path, so verification semantics are unchanged here.
VerifyWithOptions is the opt-in door to the Corona and ML-DSA lanes; nothing in
this module opts in, so no PQ policy is decided by this commit.

Two shape changes fall out of Envelope holding concrete lanes:
  - Beam is a BitSetSignature value, not an interface, so the signer-count
    type-assertion in the gas path is unreachable and its unknown-signature-type
    branch is gone. A parsed envelope always has a Beam to count.
  - Envelope.Bytes reports c14n failures instead of panicking, so the RPC path
    surfaces that error rather than returning a truncated message.

NewEnvelope also takes the Corona and ML-DSA cert-set lanes; the test aggregator
is Beam-only and passes nil for both.

Builds clean, go vet clean, warp + precompile + aggregator suites pass.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 19:21:10 -07:00

202 lines
6.6 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package warp
import (
"context"
"errors"
"fmt"
"github.com/luxfi/cache"
"github.com/luxfi/cache/lru"
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/database"
"github.com/luxfi/ids"
log "github.com/luxfi/log"
"github.com/luxfi/warp"
"github.com/luxfi/warp/payload"
)
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) (block.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.Message) error
// GetMessageSignature validates the message and returns the signature of the requested message.
GetMessageSignature(ctx context.Context, message *warp.Message) ([]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.Message, error)
warp.Verifier
}
// 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
signatureCache cache.Cacher[ids.ID, []byte]
messageCache *lru.Cache[ids.ID, *warp.Message]
offchainAddressedCallMsgs map[ids.ID]*warp.Message
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,
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,
messageCache: lru.NewCache[ids.ID, *warp.Message](messageCacheSize),
stats: newVerifierStats(),
offchainAddressedCallMsgs: make(map[ids.ID]*warp.Message),
}
return b, b.initOffChainMessages(offchainMessages)
}
func (b *backend) initOffChainMessages(offchainMessages [][]byte) error {
for i, offchainMsg := range offchainMessages {
unsignedMsg, err := warp.ParseMessage(offchainMsg)
if err != nil {
return fmt.Errorf("%w at index %d: %w", errParsingOffChainMessage, i, err)
}
if unsignedMsg.NetworkID != b.networkID {
return fmt.Errorf("%w at index %d", warp.ErrWrongNetworkID, i)
}
if unsignedMsg.SourceChainID != b.sourceChainID {
return fmt.Errorf("%w at index %d", warp.ErrWrongSourceChainID, i)
}
_, err = payload.ParseAddressedCall(unsignedMsg.Payload)
if err != nil {
return fmt.Errorf("%w at index %d as AddressedCall: %w", errParsingOffChainMessage, i, err)
}
b.offchainAddressedCallMsgs[unsignedMsg.ID()] = unsignedMsg
}
return nil
}
func (b *backend) AddMessage(unsignedMessage *warp.Message) error {
messageID := unsignedMessage.ID()
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.Message) ([]byte, error) {
messageID := unsignedMessage.ID()
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.NewMessage(b.networkID, b.sourceChainID, blockHashPayload.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to create new unsigned warp message: %w", err)
}
if sig, ok := b.signatureCache.Get(unsignedMessage.ID()); 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.Message, error) {
if message, ok := b.messageCache.Get(messageID); ok {
return message, nil
}
if message, ok := b.offchainAddressedCallMsgs[messageID]; ok {
return message, nil
}
unsignedMessageBytes, err := b.db.Get(messageID[:])
if err != nil {
return nil, err
}
unsignedMessage, err := warp.ParseMessage(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.Message) ([]byte, error) {
sig, err := b.warpSigner.Sign(unsignedMessage)
if err != nil {
return nil, fmt.Errorf("failed to sign warp message: %w", err)
}
b.signatureCache.Put(unsignedMessage.ID(), sig)
return sig, nil
}