Files
coreth/warp/verifier_backend_test.go
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

289 lines
9.2 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package warp
import (
"context"
"testing"
"time"
"github.com/luxfi/cache"
"github.com/luxfi/cache/lru"
"github.com/luxfi/coreth/metrics/metricstest"
"github.com/luxfi/coreth/warp/warptest"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/database/memdb"
"github.com/luxfi/ids"
"github.com/luxfi/p2p"
"github.com/luxfi/warp"
"github.com/luxfi/warp/payload"
"github.com/stretchr/testify/require"
)
const testNetworkID uint32 = 369
// testWarpSigner wraps a warp.Signer to implement warp.Signer (for tests)
type testWarpSigner struct {
signer warp.Signer
}
func (s *testWarpSigner) Sign(msg *warp.Message) ([]byte, error) {
sig, err := s.signer.Sign(msg)
if err != nil {
return nil, err
}
return sig[:], nil
}
func TestAddressedCallSignatures(t *testing.T) {
metricstest.WithMetrics(t)
database := memdb.New()
chainID := ids.GenerateTestID()
// Create BLS key and signer
sk, err := bls.NewSecretKey()
require.NoError(t, err)
warpSigner := warp.NewSigner(sk, testNetworkID, chainID)
testSigner := &testWarpSigner{signer: warpSigner}
offChainPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3})
require.NoError(t, err)
offchainMessage, err := warp.NewMessage(testNetworkID, chainID, offChainPayload.Bytes())
require.NoError(t, err)
offchainSignature, err := warpSigner.Sign(offchainMessage)
require.NoError(t, err)
tests := map[string]struct {
setup func(backend Backend) (request []byte, expectedResponse []byte)
verifyStats func(t *testing.T, stats *verifierStats)
errCode int32 // 0 means no error expected
}{
"known message": {
setup: func(backend Backend) (request []byte, expectedResponse []byte) {
knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test"))
require.NoError(t, err)
msg, err := warp.NewMessage(testNetworkID, chainID, knownPayload.Bytes())
require.NoError(t, err)
signature, err := warpSigner.Sign(msg)
require.NoError(t, err)
backend.AddMessage(msg)
return msg.Bytes(), signature[:]
},
verifyStats: func(t *testing.T, stats *verifierStats) {
require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count())
require.EqualValues(t, 0, stats.blockValidationFail.Snapshot().Count())
},
},
"offchain message": {
setup: func(_ Backend) (request []byte, expectedResponse []byte) {
return offchainMessage.Bytes(), offchainSignature[:]
},
verifyStats: func(t *testing.T, stats *verifierStats) {
require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count())
require.EqualValues(t, 0, stats.blockValidationFail.Snapshot().Count())
},
},
"unknown message": {
setup: func(_ Backend) (request []byte, expectedResponse []byte) {
unknownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("unknown message"))
require.NoError(t, err)
unknownMessage, err := warp.NewMessage(testNetworkID, chainID, unknownPayload.Bytes())
require.NoError(t, err)
return unknownMessage.Bytes(), nil
},
verifyStats: func(t *testing.T, stats *verifierStats) {
require.EqualValues(t, 1, stats.messageParseFail.Snapshot().Count())
require.EqualValues(t, 0, stats.blockValidationFail.Snapshot().Count())
},
errCode: ParseErrCode,
},
}
for name, test := range tests {
for _, withCache := range []bool{true, false} {
testName := name
if withCache {
testName += "_with_cache"
} else {
testName += "_no_cache"
}
t.Run(testName, func(t *testing.T) {
var sigCache cache.Cacher[ids.ID, []byte]
if withCache {
sigCache = lru.NewCache[ids.ID, []byte](100)
} else {
sigCache = &cache.Empty[ids.ID, []byte]{}
}
warpBackend, err := NewBackend(testNetworkID, chainID, warpSigner, warptest.EmptyBlockClient, database, sigCache, [][]byte{offchainMessage.Bytes()})
require.NoError(t, err)
handler := warp.NewCachedSignatureHandler(sigCache, warpBackend, testSigner)
requestBytes, expectedResponse := test.setup(warpBackend)
// Use warp binary format for request
reqBytes, err := warp.MarshalSignatureRequest(&warp.SignatureRequest{Message: requestBytes})
require.NoError(t, err)
responseBytes, handlerErr := handler.Request(context.Background(), ids.GenerateTestNodeID(), time.Time{}, reqBytes)
if test.errCode != 0 {
require.Error(t, handlerErr)
// Check if it's a p2p.Error with the expected code
if p2pErr, ok := handlerErr.(*p2p.Error); ok {
require.Equal(t, test.errCode, p2pErr.Code)
}
} else {
require.NoError(t, handlerErr)
}
test.verifyStats(t, warpBackend.(*backend).stats)
// If the expected response is empty, assert that the handler returns an empty response and return early.
if len(expectedResponse) == 0 {
require.Len(t, responseBytes, 0, "expected response to be empty")
return
}
// check cache is populated
if withCache {
require.NotZero(t, warpBackend.(*backend).signatureCache.Len())
} else {
require.Zero(t, warpBackend.(*backend).signatureCache.Len())
}
// Use warp binary format for response
response, err := warp.UnmarshalSignatureResponse(responseBytes)
require.NoError(t, err, "error unmarshalling SignatureResponse")
require.Equal(t, expectedResponse, response.Signature)
})
}
}
}
func TestBlockSignatures(t *testing.T) {
metricstest.WithMetrics(t)
database := memdb.New()
chainID := ids.GenerateTestID()
// Create BLS key and signer
sk, err := bls.NewSecretKey()
require.NoError(t, err)
warpSigner := warp.NewSigner(sk, testNetworkID, chainID)
testSigner := &testWarpSigner{signer: warpSigner}
knownBlkID := ids.GenerateTestID()
blockClient := warptest.MakeBlockClient(knownBlkID)
toMessageBytes := func(id ids.ID) []byte {
idPayload, err := payload.NewHash(id[:])
if err != nil {
panic(err)
}
msg, err := warp.NewMessage(testNetworkID, chainID, idPayload.Bytes())
if err != nil {
panic(err)
}
return msg.Bytes()
}
tests := map[string]struct {
setup func() (request []byte, expectedResponse []byte)
verifyStats func(t *testing.T, stats *verifierStats)
errCode int32 // 0 means no error expected
}{
"known block": {
setup: func() (request []byte, expectedResponse []byte) {
hashPayload, err := payload.NewHash(knownBlkID[:])
require.NoError(t, err)
unsignedMessage, err := warp.NewMessage(testNetworkID, chainID, hashPayload.Bytes())
require.NoError(t, err)
signature, err := warpSigner.Sign(unsignedMessage)
require.NoError(t, err)
return toMessageBytes(knownBlkID), signature[:]
},
verifyStats: func(t *testing.T, stats *verifierStats) {
require.EqualValues(t, 0, stats.blockValidationFail.Snapshot().Count())
require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count())
},
},
"unknown block": {
setup: func() (request []byte, expectedResponse []byte) {
unknownBlockID := ids.GenerateTestID()
return toMessageBytes(unknownBlockID), nil
},
verifyStats: func(t *testing.T, stats *verifierStats) {
require.EqualValues(t, 1, stats.blockValidationFail.Snapshot().Count())
require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count())
},
errCode: VerifyErrCode,
},
}
for name, test := range tests {
for _, withCache := range []bool{true, false} {
testName := name
if withCache {
testName += "_with_cache"
} else {
testName += "_no_cache"
}
t.Run(testName, func(t *testing.T) {
var sigCache cache.Cacher[ids.ID, []byte]
if withCache {
sigCache = lru.NewCache[ids.ID, []byte](100)
} else {
sigCache = &cache.Empty[ids.ID, []byte]{}
}
warpBackend, err := NewBackend(
testNetworkID,
chainID,
warpSigner,
blockClient,
database,
sigCache,
nil,
)
require.NoError(t, err)
handler := warp.NewCachedSignatureHandler(sigCache, warpBackend, testSigner)
requestBytes, expectedResponse := test.setup()
// Use warp binary format for request
reqBytes, err := warp.MarshalSignatureRequest(&warp.SignatureRequest{Message: requestBytes})
require.NoError(t, err)
responseBytes, handlerErr := handler.Request(context.Background(), ids.GenerateTestNodeID(), time.Time{}, reqBytes)
if test.errCode != 0 {
require.Error(t, handlerErr)
// Check if it's a p2p.Error with the expected code
if p2pErr, ok := handlerErr.(*p2p.Error); ok {
require.Equal(t, test.errCode, p2pErr.Code)
}
} else {
require.NoError(t, handlerErr)
}
test.verifyStats(t, warpBackend.(*backend).stats)
// If the expected response is empty, assert that the handler returns an empty response and return early.
if len(expectedResponse) == 0 {
require.Len(t, responseBytes, 0, "expected response to be empty")
return
}
// check cache is populated
if withCache {
require.NotZero(t, warpBackend.(*backend).signatureCache.Len())
} else {
require.Zero(t, warpBackend.(*backend).signatureCache.Len())
}
// Use warp binary format for response
response, err := warp.UnmarshalSignatureResponse(responseBytes)
require.NoError(t, err, "error unmarshalling SignatureResponse")
require.Equal(t, expectedResponse, response.Signature)
})
}
}
}