From fc55f6b30102acd781ca552f8d013a74386c991e Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 4 Aug 2025 19:06:24 -0400 Subject: [PATCH 01/53] Initial merge --- vms/evm/warp/backend.go | 207 ++++++++++ vms/evm/warp/backend_test.go | 182 +++++++++ vms/evm/warp/client.go | 79 ++++ vms/evm/warp/service.go | 144 +++++++ vms/evm/warp/validators/state.go | 55 +++ vms/evm/warp/validators/state_test.go | 48 +++ vms/evm/warp/verifier_backend.go | 131 +++++++ vms/evm/warp/verifier_backend_test.go | 366 ++++++++++++++++++ vms/evm/warp/verifier_stats.go | 41 ++ vms/evm/warp/warptest/block_client.go | 43 ++ .../warp/warptest/noop_validator_reader.go | 35 ++ .../warp/message/validator_uptime.go | 51 +++ 12 files changed, 1382 insertions(+) create mode 100644 vms/evm/warp/backend.go create mode 100644 vms/evm/warp/backend_test.go create mode 100644 vms/evm/warp/client.go create mode 100644 vms/evm/warp/service.go create mode 100644 vms/evm/warp/validators/state.go create mode 100644 vms/evm/warp/validators/state_test.go create mode 100644 vms/evm/warp/verifier_backend.go create mode 100644 vms/evm/warp/verifier_backend_test.go create mode 100644 vms/evm/warp/verifier_stats.go create mode 100644 vms/evm/warp/warptest/block_client.go create mode 100644 vms/evm/warp/warptest/noop_validator_reader.go create mode 100644 vms/platformvm/warp/message/validator_uptime.go diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go new file mode 100644 index 000000000000..c28767f258a7 --- /dev/null +++ b/vms/evm/warp/backend.go @@ -0,0 +1,207 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/cache/lru" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + + "github.com/ava-labs/avalanchego/network/p2p/acp118" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/libevm/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) (snowman.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 *avalancheWarp.UnsignedMessage) error + + // GetMessageSignature validates the message and returns the signature of the requested message. + GetMessageSignature(ctx context.Context, message *avalancheWarp.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) (*avalancheWarp.UnsignedMessage, error) + + acp118.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 avalancheWarp.Signer + blockClient BlockClient + validatorReader warptest.ValidatorReader + signatureCache cache.Cacher[ids.ID, []byte] + messageCache *lru.Cache[ids.ID, *avalancheWarp.UnsignedMessage] + offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.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 avalancheWarp.Signer, + blockClient BlockClient, + validatorReader warptest.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, *avalancheWarp.UnsignedMessage](messageCacheSize), + stats: newVerifierStats(), + offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), + } + return b, b.initOffChainMessages(offchainMessages) +} + +func (b *backend) initOffChainMessages(offchainMessages [][]byte) error { + for i, offchainMsg := range offchainMessages { + unsignedMsg, err := avalancheWarp.ParseUnsignedMessage(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", avalancheWarp.ErrWrongNetworkID, i) + } + + if unsignedMsg.SourceChainID != b.sourceChainID { + return fmt.Errorf("%w at index %d", avalancheWarp.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 *avalancheWarp.UnsignedMessage) 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 *avalancheWarp.UnsignedMessage) ([]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 := avalancheWarp.NewUnsignedMessage(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) (*avalancheWarp.UnsignedMessage, 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 := avalancheWarp.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 *avalancheWarp.UnsignedMessage) ([]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 +} diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go new file mode 100644 index 000000000000..2c7f599d15df --- /dev/null +++ b/vms/evm/warp/backend_test.go @@ -0,0 +1,182 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/cache/lru" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" + "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/stretchr/testify/require" +) + +var ( + networkID uint32 = 54321 + sourceChainID = ids.GenerateTestID() + testSourceAddress = utils.RandomBytes(20) + testPayload = []byte("test") + testUnsignedMessage *warp.UnsignedMessage +) + +func init() { + testAddressedCallPayload, err := payload.NewAddressedCall(testSourceAddress, testPayload) + if err != nil { + panic(err) + } + testUnsignedMessage, err = warp.NewUnsignedMessage(networkID, sourceChainID, testAddressedCallPayload.Bytes()) + if err != nil { + panic(err) + } +} + +func TestAddAndGetValidMessage(t *testing.T) { + db := memdb.New() + + sk, err := localsigner.New() + require.NoError(t, err) + warpSigner := warp.NewSigner(sk, networkID, sourceChainID) + messageSignatureCache := lru.NewCache[ids.ID, []byte](500) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) + require.NoError(t, err) + + // Add testUnsignedMessage to the warp backend + require.NoError(t, backend.AddMessage(testUnsignedMessage)) + + // Verify that a signature is returned successfully, and compare to expected signature. + signature, err := backend.GetMessageSignature(context.TODO(), testUnsignedMessage) + require.NoError(t, err) + + expectedSig, err := warpSigner.Sign(testUnsignedMessage) + require.NoError(t, err) + require.Equal(t, expectedSig, signature[:]) +} + +func TestAddAndGetUnknownMessage(t *testing.T) { + db := memdb.New() + + sk, err := localsigner.New() + require.NoError(t, err) + warpSigner := warp.NewSigner(sk, networkID, sourceChainID) + messageSignatureCache := lru.NewCache[ids.ID, []byte](500) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) + require.NoError(t, err) + + // Try getting a signature for a message that was not added. + _, err = backend.GetMessageSignature(context.TODO(), testUnsignedMessage) + require.Error(t, err) +} + +func TestGetBlockSignature(t *testing.T) { + require := require.New(t) + + blkID := ids.GenerateTestID() + blockClient := warptest.MakeBlockClient(blkID) + db := memdb.New() + + sk, err := localsigner.New() + require.NoError(err) + warpSigner := warp.NewSigner(sk, networkID, sourceChainID) + messageSignatureCache := lru.NewCache[ids.ID, []byte](500) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) + require.NoError(err) + + blockHashPayload, err := payload.NewHash(blkID) + require.NoError(err) + unsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) + require.NoError(err) + expectedSig, err := warpSigner.Sign(unsignedMessage) + require.NoError(err) + + signature, err := backend.GetBlockSignature(context.TODO(), blkID) + require.NoError(err) + require.Equal(expectedSig, signature[:]) + + _, err = backend.GetBlockSignature(context.TODO(), ids.GenerateTestID()) + require.Error(err) +} + +func TestZeroSizedCache(t *testing.T) { + db := memdb.New() + + sk, err := localsigner.New() + require.NoError(t, err) + warpSigner := warp.NewSigner(sk, networkID, sourceChainID) + + // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. + messageSignatureCache := lru.NewCache[ids.ID, []byte](0) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) + require.NoError(t, err) + + // Add testUnsignedMessage to the warp backend + require.NoError(t, backend.AddMessage(testUnsignedMessage)) + + // Verify that a signature is returned successfully, and compare to expected signature. + signature, err := backend.GetMessageSignature(context.TODO(), testUnsignedMessage) + require.NoError(t, err) + + expectedSig, err := warpSigner.Sign(testUnsignedMessage) + require.NoError(t, err) + require.Equal(t, expectedSig, signature[:]) +} + +func TestOffChainMessages(t *testing.T) { + type test struct { + offchainMessages [][]byte + check func(require *require.Assertions, b Backend) + err error + } + sk, err := localsigner.New() + require.NoError(t, err) + warpSigner := warp.NewSigner(sk, networkID, sourceChainID) + + for name, test := range map[string]test{ + "no offchain messages": {}, + "single off-chain message": { + offchainMessages: [][]byte{ + testUnsignedMessage.Bytes(), + }, + check: func(require *require.Assertions, b Backend) { + msg, err := b.GetMessage(testUnsignedMessage.ID()) + require.NoError(err) + require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) + + signature, err := b.GetMessageSignature(context.TODO(), testUnsignedMessage) + require.NoError(err) + expectedSignatureBytes, err := warpSigner.Sign(msg) + require.NoError(err) + require.Equal(expectedSignatureBytes, signature[:]) + }, + }, + "unknown message": { + check: func(require *require.Assertions, b Backend) { + _, err := b.GetMessage(testUnsignedMessage.ID()) + require.ErrorIs(err, database.ErrNotFound) + }, + }, + "invalid message": { + offchainMessages: [][]byte{{1, 2, 3}}, + err: errParsingOffChainMessage, + }, + } { + t.Run(name, func(t *testing.T) { + require := require.New(t) + db := memdb.New() + + messageSignatureCache := lru.NewCache[ids.ID, []byte](0) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, test.offchainMessages) + require.ErrorIs(err, test.err) + if test.check != nil { + test.check(require, backend) + } + }) + } +} diff --git a/vms/evm/warp/client.go b/vms/evm/warp/client.go new file mode 100644 index 000000000000..f59db180b993 --- /dev/null +++ b/vms/evm/warp/client.go @@ -0,0 +1,79 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/libevm/common/hexutil" + "github.com/ava-labs/libevm/rpc" +) + +var _ Client = (*client)(nil) + +type Client interface { + GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) + GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) + GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) + GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) + GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) +} + +// client implementation for interacting with EVM [chain] +type client struct { + client *rpc.Client +} + +// NewClient returns a Client for interacting with EVM [chain] +func NewClient(uri, chain string) (Client, error) { + innerClient, err := rpc.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", uri, chain)) + if err != nil { + return nil, fmt.Errorf("failed to dial client. err: %w", err) + } + return &client{ + client: innerClient, + }, nil +} + +func (c *client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) { + var res hexutil.Bytes + if err := c.client.CallContext(ctx, &res, "warp_getMessage", messageID); err != nil { + return nil, fmt.Errorf("call to warp_getMessage failed. err: %w", err) + } + return res, nil +} + +func (c *client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { + var res hexutil.Bytes + if err := c.client.CallContext(ctx, &res, "warp_getMessageSignature", messageID); err != nil { + return nil, fmt.Errorf("call to warp_getMessageSignature failed. err: %w", err) + } + return res, nil +} + +func (c *client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { + var res hexutil.Bytes + if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetIDStr); err != nil { + return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed. err: %w", err) + } + return res, nil +} + +func (c *client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { + var res hexutil.Bytes + if err := c.client.CallContext(ctx, &res, "warp_getBlockSignature", blockID); err != nil { + return nil, fmt.Errorf("call to warp_getBlockSignature failed. err: %w", err) + } + return res, nil +} + +func (c *client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { + var res hexutil.Bytes + if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetIDStr); err != nil { + return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed. err: %w", err) + } + return res, nil +} diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go new file mode 100644 index 000000000000..9dee0be6a878 --- /dev/null +++ b/vms/evm/warp/service.go @@ -0,0 +1,144 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p/acp118" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/evm/warp/validators" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/libevm/common/hexutil" + "github.com/ava-labs/libevm/log" +) + +var errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") + +// API introduces snowman specific functionality to the evm +type API struct { + chainContext *snow.Context + backend Backend + signatureAggregator *acp118.SignatureAggregator + requirePrimaryNetworkSigners func() bool +} + +func NewAPI(chainCtx *snow.Context, backend Backend, signatureAggregator *acp118.SignatureAggregator, requirePrimaryNetworkSigners func() bool) *API { + return &API{ + backend: backend, + chainContext: chainCtx, + signatureAggregator: signatureAggregator, + requirePrimaryNetworkSigners: requirePrimaryNetworkSigners, + } +} + +// GetMessage returns the Warp message associated with a messageID. +func (a *API) GetMessage(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { + message, err := a.backend.GetMessage(messageID) + if err != nil { + return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) + } + return hexutil.Bytes(message.Bytes()), nil +} + +// GetMessageSignature returns the BLS signature associated with a messageID. +func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { + unsignedMessage, err := a.backend.GetMessage(messageID) + if err != nil { + return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) + } + signature, err := a.backend.GetMessageSignature(ctx, unsignedMessage) + if err != nil { + return nil, fmt.Errorf("failed to get signature for message %s with error %w", messageID, err) + } + return signature[:], nil +} + +// GetBlockSignature returns the BLS signature associated with a blockID. +func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { + signature, err := a.backend.GetBlockSignature(ctx, blockID) + if err != nil { + return nil, fmt.Errorf("failed to get signature for block %s with error %w", blockID, err) + } + return signature[:], nil +} + +// GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] +func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { + unsignedMessage, err := a.backend.GetMessage(messageID) + if err != nil { + return nil, err + } + return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) +} + +// GetBlockAggregateSignature fetches the aggregate signature for the requested [blockID] +func (a *API) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { + blockHashPayload, err := payload.NewHash(blockID) + if err != nil { + return nil, err + } + unsignedMessage, err := warp.NewUnsignedMessage(a.chainContext.NetworkID, a.chainContext.ChainID, blockHashPayload.Bytes()) + if err != nil { + return nil, err + } + + return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) +} + +func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetIDStr string) (hexutil.Bytes, error) { + subnetID := a.chainContext.SubnetID + if len(subnetIDStr) > 0 { + sid, err := ids.FromString(subnetIDStr) + if err != nil { + return nil, fmt.Errorf("failed to parse subnetID: %q", subnetIDStr) + } + subnetID = sid + } + validatorState := a.chainContext.ValidatorState + pChainHeight, err := validatorState.GetCurrentHeight(ctx) + if err != nil { + return nil, err + } + + state := validators.NewState(validatorState, a.chainContext.SubnetID, a.chainContext.ChainID, a.requirePrimaryNetworkSigners()) + validatorSet, err := warp.GetCanonicalValidatorSetFromSubnetID(ctx, state, pChainHeight, subnetID) + if err != nil { + return nil, fmt.Errorf("failed to get validator set: %w", err) + } + if len(validatorSet.Validators) == 0 { + return nil, fmt.Errorf("%w (SubnetID: %s, Height: %d)", errNoValidators, subnetID, pChainHeight) + } + + log.Debug("Fetching signature", + "sourceSubnetID", subnetID, + "height", pChainHeight, + "numValidators", len(validatorSet.Validators), + "totalWeight", validatorSet.TotalWeight, + ) + warpMessage := &warp.Message{ + UnsignedMessage: *unsignedMessage, + Signature: &warp.BitSetSignature{}, + } + signedMessage, _, _, err := a.signatureAggregator.AggregateSignatures( + ctx, + warpMessage, + nil, + validatorSet.Validators, + quorumNum, + executor.WarpQuorumDenominator, + ) + if err != nil { + return nil, err + } + // TODO: return the signature and total weight as well to the caller for more complete details + // Need to decide on the best UI for this and write up documentation with the potential + // gotchas that could impact signed messages becoming invalid. + return hexutil.Bytes(signedMessage.Bytes()), nil +} diff --git a/vms/evm/warp/validators/state.go b/vms/evm/warp/validators/state.go new file mode 100644 index 000000000000..58530750698f --- /dev/null +++ b/vms/evm/warp/validators/state.go @@ -0,0 +1,55 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "context" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/constants" +) + +var _ validators.State = (*State)(nil) + +// State provides a special case used to handle Avalanche Warp Message verification for messages sent +// from the Primary Network. Subnets have strictly fewer validators than the Primary Network, so we require +// signatures from a threshold of the RECEIVING subnet validator set rather than the full Primary Network +// since the receiving subnet already relies on a majority of its validators being correct. +type State struct { + validators.State + mySubnetID ids.ID + sourceChainID ids.ID + requirePrimaryNetworkSigners bool +} + +// NewState returns a wrapper of [validators.State] which special cases the handling of the Primary Network. +// +// The wrapped state will return the [mySubnetID's] validator set instead of the Primary Network when +// the Primary Network SubnetID is passed in. +func NewState(state validators.State, mySubnetID ids.ID, sourceChainID ids.ID, requirePrimaryNetworkSigners bool) *State { + return &State{ + State: state, + mySubnetID: mySubnetID, + sourceChainID: sourceChainID, + requirePrimaryNetworkSigners: requirePrimaryNetworkSigners, + } +} + +func (s *State) GetValidatorSet( + ctx context.Context, + height uint64, + subnetID ids.ID, +) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + // If the subnetID is anything other than the Primary Network, or Primary + // Network signers are required (except P-Chain), this is a direct passthrough. + usePrimary := s.requirePrimaryNetworkSigners && s.sourceChainID != constants.PlatformChainID + if usePrimary || subnetID != constants.PrimaryNetworkID { + return s.State.GetValidatorSet(ctx, height, subnetID) + } + + // If the requested subnet is the primary network, then we return the validator + // set for the Subnet that is receiving the message instead. + return s.State.GetValidatorSet(ctx, height, s.mySubnetID) +} diff --git a/vms/evm/warp/validators/state_test.go b/vms/evm/warp/validators/state_test.go new file mode 100644 index 000000000000..49ffaf2e3a07 --- /dev/null +++ b/vms/evm/warp/validators/state_test.go @@ -0,0 +1,48 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/snow/validators/validatorsmock" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestGetValidatorSetPrimaryNetwork(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + mySubnetID := ids.GenerateTestID() + otherSubnetID := ids.GenerateTestID() + + mockState := validatorsmock.NewState(ctrl) + snowCtx := snowtest.Context(t, snowtest.CChainID) + snowCtx.SubnetID = mySubnetID + snowCtx.ValidatorState = mockState + state := NewState(snowCtx.ValidatorState, snowCtx.SubnetID, snowCtx.ChainID, false) + // Expect that requesting my validator set returns my validator set + mockState.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), mySubnetID).Return(make(map[ids.NodeID]*validators.GetValidatorOutput), nil) + output, err := state.GetValidatorSet(context.Background(), 10, mySubnetID) + require.NoError(err) + require.Len(output, 0) + + // Expect that requesting the Primary Network validator set overrides and returns my validator set + mockState.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), mySubnetID).Return(make(map[ids.NodeID]*validators.GetValidatorOutput), nil) + output, err = state.GetValidatorSet(context.Background(), 10, constants.PrimaryNetworkID) + require.NoError(err) + require.Len(output, 0) + + // Expect that requesting other validator set returns that validator set + mockState.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), otherSubnetID).Return(make(map[ids.NodeID]*validators.GetValidatorOutput), nil) + output, err = state.GetValidatorSet(context.Background(), 10, otherSubnetID) + require.NoError(err) + require.Len(output, 0) +} diff --git a/vms/evm/warp/verifier_backend.go b/vms/evm/warp/verifier_backend.go new file mode 100644 index 000000000000..9192744c0f26 --- /dev/null +++ b/vms/evm/warp/verifier_backend.go @@ -0,0 +1,131 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "fmt" + + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/snow/engine/common" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +const ( + ParseErrCode = iota + 1 + VerifyErrCode +) + +// Verify verifies the signature of the message +// It also implements the acp118.Verifier interface +func (b *backend) Verify(ctx context.Context, unsignedMessage *avalancheWarp.UnsignedMessage, _ []byte) *common.AppError { + messageID := unsignedMessage.ID() + // Known on-chain messages should be signed + if _, err := b.GetMessage(messageID); err == nil { + return nil + } else if err != database.ErrNotFound { + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("failed to get message %s: %s", messageID, err.Error()), + } + } + + parsed, err := payload.Parse(unsignedMessage.Payload) + if err != nil { + b.stats.IncMessageParseFail() + return &common.AppError{ + Code: ParseErrCode, + Message: "failed to parse payload: " + err.Error(), + } + } + + switch p := parsed.(type) { + case *payload.AddressedCall: + return b.verifyOffchainAddressedCall(p) + case *payload.Hash: + return b.verifyBlockMessage(ctx, p) + default: + b.stats.IncMessageParseFail() + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("unknown payload type: %T", p), + } + } +} + +// verifyBlockMessage returns nil if blockHashPayload contains the ID +// of an accepted block indicating it should be signed by the VM. +func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { + blockID := blockHashPayload.Hash + _, err := b.blockClient.GetAcceptedBlock(ctx, blockID) + if err != nil { + b.stats.IncBlockValidationFail() + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), + } + } + + return nil +} + +// verifyOffchainAddressedCall verifies the addressed call message +func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { + // Further, parse the payload to see if it is a known type. + parsed, err := message.Parse(addressedCall.Payload) + if err != nil { + b.stats.IncMessageParseFail() + return &common.AppError{ + Code: ParseErrCode, + Message: "failed to parse addressed call message: " + err.Error(), + } + } + + if len(addressedCall.SourceAddress) != 0 { + return &common.AppError{ + Code: VerifyErrCode, + Message: "source address should be empty for offchain addressed messages", + } + } + + switch p := parsed.(type) { + case *message.ValidatorUptime: + if err := b.verifyUptimeMessage(p); err != nil { + b.stats.IncUptimeValidationFail() + return err + } + default: + b.stats.IncMessageParseFail() + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("unknown message type: %T", p), + } + } + + return nil +} + +func (b *backend) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { + vdr, currentUptime, _, err := b.validatorReader.GetValidatorAndUptime(uptimeMsg.ValidationID) + if err != nil { + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to get uptime for validationID %s: %s", uptimeMsg.ValidationID, err.Error()), + } + } + + currentUptimeSeconds := uint64(currentUptime.Seconds()) + // verify the current uptime against the total uptime in the message + if currentUptimeSeconds < uptimeMsg.TotalUptime { + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for nodeID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, vdr.NodeID), + } + } + + return nil +} diff --git a/vms/evm/warp/verifier_backend_test.go b/vms/evm/warp/verifier_backend_test.go new file mode 100644 index 000000000000..3cc5c3e1301a --- /dev/null +++ b/vms/evm/warp/verifier_backend_test.go @@ -0,0 +1,366 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/cache/lru" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p/acp118" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" + "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + + // TODO: FIGURE OUT HOW TO GET RID OF THIS IMPORT + "github.com/ava-labs/subnet-evm/plugin/evm/validators" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestAddressedCallSignatures(t *testing.T) { + metricstest.WithMetrics(t) + + database := memdb.New() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + offChainPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) + require.NoError(t, err) + offchainMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offChainPayload.Bytes()) + require.NoError(t, err) + offchainSignature, err := snowCtx.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) + err error + }{ + "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.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) + require.NoError(t, err) + signature, err := snowCtx.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.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.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()) + }, + err: &common.AppError{Code: ParseErrCode}, + }, + } + + for name, test := range tests { + for _, withCache := range []bool{true, false} { + if withCache { + name += "_with_cache" + } else { + name += "_no_cache" + } + t.Run(name, 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( + snowCtx.NetworkID, + snowCtx.ChainID, + snowCtx.WarpSigner, + warptest.EmptyBlockClient, + nil, + database, + sigCache, + [][]byte{offchainMessage.Bytes()}, + ) + require.NoError(t, err) + handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) + + requestBytes, expectedResponse := test.setup(warpBackend) + protoMsg := &sdk.SignatureRequest{Message: requestBytes} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + if test.err != nil { + require.Error(t, appErr) + require.ErrorIs(t, appErr, test.err) + } else { + require.Nil(t, appErr) + } + + 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()) + } + response := &sdk.SignatureResponse{} + require.NoError(t, proto.Unmarshal(responseBytes, response)) + require.NoError(t, err, "error unmarshalling SignatureResponse") + + require.Equal(t, expectedResponse, response.Signature) + }) + } + } +} + +func TestBlockSignatures(t *testing.T) { + metricstest.WithMetrics(t) + + database := memdb.New() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + 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.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.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) + err error + }{ + "known block": { + setup: func() (request []byte, expectedResponse []byte) { + hashPayload, err := payload.NewHash(knownBlkID) + require.NoError(t, err) + unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) + require.NoError(t, err) + signature, err := snowCtx.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()) + }, + err: &common.AppError{Code: VerifyErrCode}, + }, + } + + for name, test := range tests { + for _, withCache := range []bool{true, false} { + if withCache { + name += "_with_cache" + } else { + name += "_no_cache" + } + t.Run(name, 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( + snowCtx.NetworkID, + snowCtx.ChainID, + snowCtx.WarpSigner, + blockClient, + warptest.NoOpValidatorReader{}, + database, + sigCache, + nil, + ) + require.NoError(t, err) + handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) + + requestBytes, expectedResponse := test.setup() + protoMsg := &sdk.SignatureRequest{Message: requestBytes} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + if test.err != nil { + require.NotNil(t, appErr) + require.ErrorIs(t, test.err, appErr) + } else { + require.Nil(t, appErr) + } + + 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()) + } + var response sdk.SignatureResponse + err = proto.Unmarshal(responseBytes, &response) + require.NoError(t, err, "error unmarshalling SignatureResponse") + require.Equal(t, expectedResponse, response.Signature) + }) + } + } +} + +func TestUptimeSignatures(t *testing.T) { + database := memdb.New() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID, totalUptime uint64) ([]byte, *avalancheWarp.UnsignedMessage) { + uptimePayload, err := message.NewValidatorUptime(vID, 80) + require.NoError(t, err) + addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) + require.NoError(t, err) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) + require.NoError(t, err) + + protoMsg := &sdk.SignatureRequest{Message: unsignedMessage.Bytes()} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + return protoBytes, unsignedMessage + } + + for _, withCache := range []bool{true, false} { + var sigCache cache.Cacher[ids.ID, []byte] + if withCache { + sigCache = lru.NewCache[ids.ID, []byte](100) + } else { + sigCache = &cache.Empty[ids.ID, []byte]{} + } + chainCtx := snowtest.Context(t, snowtest.CChainID) + clk := &mockable.Clock{} + validatorsManager, err := validators.NewManager(chainCtx, memdb.New(), clk) + require.NoError(t, err) + lock := &sync.RWMutex{} + newLockedValidatorManager := validators.NewLockedValidatorReader(validatorsManager, lock) + validatorsManager.StartTracking([]ids.NodeID{}) + warpBackend, err := NewBackend( + snowCtx.NetworkID, + snowCtx.ChainID, + snowCtx.WarpSigner, + warptest.EmptyBlockClient, + newLockedValidatorManager, + database, + sigCache, + nil, + ) + require.NoError(t, err) + handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) + + // sourceAddress nonZero + protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID(), 80) + _, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Contains(t, appErr.Error(), "source address should be empty") + + // not existing validationID + vID := ids.GenerateTestID() + protoBytes, _ = getUptimeMessageBytes([]byte{}, vID, 80) + _, appErr = handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Contains(t, appErr.Error(), "failed to get validator") + + // uptime is less than requested (not connected) + validationID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + require.NoError(t, validatorsManager.AddValidator(warptest.Validator{ + ValidationID: validationID, + NodeID: nodeID, + Weight: 1, + StartTimestamp: clk.Unix(), + IsActive: true, + IsL1Validator: true, + })) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID, 80) + _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Contains(t, appErr.Error(), "current uptime 0 is less than queried uptime 80") + + // uptime is less than requested (not enough) + require.NoError(t, validatorsManager.Connect(nodeID)) + clk.Set(clk.Time().Add(40 * time.Second)) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID, 80) + _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Contains(t, appErr.Error(), "current uptime 40 is less than queried uptime 80") + + // valid uptime + clk.Set(clk.Time().Add(40 * time.Second)) + protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID, 80) + responseBytes, appErr := handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + require.Nil(t, appErr) + expectedSignature, err := snowCtx.WarpSigner.Sign(msg) + require.NoError(t, err) + response := &sdk.SignatureResponse{} + require.NoError(t, proto.Unmarshal(responseBytes, response)) + require.Equal(t, expectedSignature[:], response.Signature) + } +} diff --git a/vms/evm/warp/verifier_stats.go b/vms/evm/warp/verifier_stats.go new file mode 100644 index 000000000000..6c870ce848fa --- /dev/null +++ b/vms/evm/warp/verifier_stats.go @@ -0,0 +1,41 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import "github.com/ava-labs/libevm/metrics" + +type verifierStats struct { + messageParseFail metrics.Counter + // AddressedCall metrics + addressedCallValidationFail metrics.Counter + // BlockRequest metrics + blockValidationFail metrics.Counter + // Uptime metrics + uptimeValidationFail metrics.Counter +} + +func newVerifierStats() *verifierStats { + return &verifierStats{ + messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), + addressedCallValidationFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_validation_fail", nil), + blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), + uptimeValidationFail: metrics.NewRegisteredCounter("warp_backend_uptime_validation_fail", nil), + } +} + +func (h *verifierStats) IncAddressedCallValidationFail() { + h.addressedCallValidationFail.Inc(1) +} + +func (h *verifierStats) IncBlockValidationFail() { + h.blockValidationFail.Inc(1) +} + +func (h *verifierStats) IncMessageParseFail() { + h.messageParseFail.Inc(1) +} + +func (h *verifierStats) IncUptimeValidationFail() { + h.uptimeValidationFail.Inc(1) +} diff --git a/vms/evm/warp/warptest/block_client.go b/vms/evm/warp/warptest/block_client.go new file mode 100644 index 000000000000..7f98d4f79786 --- /dev/null +++ b/vms/evm/warp/warptest/block_client.go @@ -0,0 +1,43 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// warptest exposes common functionality for testing the warp package. +package warptest + +import ( + "context" + "slices" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/consensus/snowman/snowmantest" + "github.com/ava-labs/avalanchego/snow/snowtest" +) + +// EmptyBlockClient returns an error if a block is requested +var EmptyBlockClient BlockClient = MakeBlockClient() + +type BlockClient func(ctx context.Context, blockID ids.ID) (snowman.Block, error) + +func (f BlockClient) GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) { + return f(ctx, blockID) +} + +// MakeBlockClient returns a new BlockClient that returns the provided blocks. +// If a block is requested that isn't part of the provided blocks, an error is +// returned. +func MakeBlockClient(blkIDs ...ids.ID) BlockClient { + return func(_ context.Context, blkID ids.ID) (snowman.Block, error) { + if !slices.Contains(blkIDs, blkID) { + return nil, database.ErrNotFound + } + + return &snowmantest.Block{ + Decidable: snowtest.Decidable{ + IDV: blkID, + Status: snowtest.Accepted, + }, + }, nil + } +} diff --git a/vms/evm/warp/warptest/noop_validator_reader.go b/vms/evm/warp/warptest/noop_validator_reader.go new file mode 100644 index 000000000000..69d598996fb8 --- /dev/null +++ b/vms/evm/warp/warptest/noop_validator_reader.go @@ -0,0 +1,35 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// warptest exposes common functionality for testing the warp package. +package warptest + +import ( + "time" + + "github.com/ava-labs/avalanchego/ids" +) + +var _ ValidatorReader = (*NoOpValidatorReader)(nil) + +type Validator struct { + ValidationID ids.ID `json:"validationID"` + NodeID ids.NodeID `json:"nodeID"` + Weight uint64 `json:"weight"` + StartTimestamp uint64 `json:"startTimestamp"` + IsActive bool `json:"isActive"` + IsL1Validator bool `json:"isL1Validator"` +} + +type ValidatorReader interface { + // GetValidatorAndUptime returns the calculated uptime of the validator specified by validationID + // and the last updated time. + // GetValidatorAndUptime holds the VM lock while performing the operation and can be called concurrently. + GetValidatorAndUptime(validationID ids.ID) (Validator, time.Duration, time.Time, error) +} + +type NoOpValidatorReader struct{} + +func (NoOpValidatorReader) GetValidatorAndUptime(ids.ID) (Validator, time.Duration, time.Time, error) { + return Validator{}, 0, time.Time{}, nil +} diff --git a/vms/platformvm/warp/message/validator_uptime.go b/vms/platformvm/warp/message/validator_uptime.go new file mode 100644 index 000000000000..47d12f4306a3 --- /dev/null +++ b/vms/platformvm/warp/message/validator_uptime.go @@ -0,0 +1,51 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" +) + +// ValidatorUptime is signed when the ValidationID is known and the validator +// has been up for TotalUptime seconds. +type ValidatorUptime struct { + ValidationID ids.ID `serialize:"true"` + TotalUptime uint64 `serialize:"true"` // in seconds + + bytes []byte +} + +// NewValidatorUptime creates a new *ValidatorUptime and initializes it. +func NewValidatorUptime(validationID ids.ID, totalUptime uint64) (*ValidatorUptime, error) { + bhp := &ValidatorUptime{ + ValidationID: validationID, + TotalUptime: totalUptime, + } + return bhp, Initialize(bhp) +} + +// ParseValidatorUptime converts a slice of bytes into an initialized ValidatorUptime. +func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { + payloadIntf, err := Parse(b) + if err != nil { + return nil, err + } + payload, ok := payloadIntf.(*ValidatorUptime) + if !ok { + return nil, fmt.Errorf("%w: %T", ErrWrongType, payloadIntf) + } + return payload, nil +} + +// Bytes returns the binary representation of this payload. It assumes that the +// payload is initialized from either NewValidatorUptime or Parse. +func (b *ValidatorUptime) Bytes() []byte { + return b.bytes +} + +func (b *ValidatorUptime) initialize(bytes []byte) { + b.bytes = bytes +} From f5de4e1c7ed023524ecf76a56b47ce5c425fe1e8 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Tue, 28 Oct 2025 16:30:05 -0400 Subject: [PATCH 02/53] Clean up post validators to AvalancheGo --- vms/evm/warp/backend.go | 19 ++- vms/evm/warp/backend_test.go | 22 +-- vms/evm/warp/service.go | 26 ++- vms/evm/warp/validators/state.go | 55 ------ vms/evm/warp/validators/state_test.go | 48 ------ vms/evm/warp/verifier_backend.go | 6 +- vms/evm/warp/verifier_backend_test.go | 158 +++++++++--------- .../warp/warptest/noop_validator_reader.go | 35 ---- vms/platformvm/warp/message/codec.go | 1 + 9 files changed, 115 insertions(+), 255 deletions(-) delete mode 100644 vms/evm/warp/validators/state.go delete mode 100644 vms/evm/warp/validators/state_test.go delete mode 100644 vms/evm/warp/warptest/noop_validator_reader.go diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index c28767f258a7..df58d19dbd76 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -12,17 +12,19 @@ import ( "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/libevm/log" + + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) var ( _ Backend = (*backend)(nil) + ErrValidateBlock = errors.New("failed to validate block message") + ErrVerifyWarpMessage = errors.New("failed to verify warp message") errParsingOffChainMessage = errors.New("failed to parse off-chain message") messageCacheSize = 500 @@ -57,7 +59,7 @@ type backend struct { db database.Database warpSigner avalancheWarp.Signer blockClient BlockClient - validatorReader warptest.ValidatorReader + uptimeTracker *uptimetracker.UptimeTracker signatureCache cache.Cacher[ids.ID, []byte] messageCache *lru.Cache[ids.ID, *avalancheWarp.UnsignedMessage] offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.UnsignedMessage @@ -70,7 +72,7 @@ func NewBackend( sourceChainID ids.ID, warpSigner avalancheWarp.Signer, blockClient BlockClient, - validatorReader warptest.ValidatorReader, + uptimeTracker *uptimetracker.UptimeTracker, db database.Database, signatureCache cache.Cacher[ids.ID, []byte], offchainMessages [][]byte, @@ -82,7 +84,7 @@ func NewBackend( warpSigner: warpSigner, blockClient: blockClient, signatureCache: signatureCache, - validatorReader: validatorReader, + uptimeTracker: uptimeTracker, messageCache: lru.NewCache[ids.ID, *avalancheWarp.UnsignedMessage](messageCacheSize), stats: newVerifierStats(), offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), @@ -141,8 +143,9 @@ func (b *backend) GetMessageSignature(ctx context.Context, unsignedMessage *aval } if err := b.Verify(ctx, unsignedMessage, nil); err != nil { - return nil, fmt.Errorf("failed to validate warp message: %w", err) + return nil, fmt.Errorf("%w: %w", ErrVerifyWarpMessage, err) } + return b.signMessage(unsignedMessage) } @@ -164,7 +167,7 @@ func (b *backend) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte } if err := b.verifyBlockMessage(ctx, blockHashPayload); err != nil { - return nil, fmt.Errorf("failed to validate block message: %w", err) + return nil, fmt.Errorf("%w: %w", ErrValidateBlock, err) } sig, err := b.signMessage(unsignedMessage) diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 2c7f599d15df..0dfa7f290eaa 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -45,7 +45,7 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) // Add testUnsignedMessage to the warp backend @@ -57,7 +57,7 @@ func TestAddAndGetValidMessage(t *testing.T) { expectedSig, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) - require.Equal(t, expectedSig, signature[:]) + require.Equal(t, expectedSig, signature) } func TestAddAndGetUnknownMessage(t *testing.T) { @@ -67,12 +67,12 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) // Try getting a signature for a message that was not added. _, err = backend.GetMessageSignature(context.TODO(), testUnsignedMessage) - require.Error(t, err) + require.ErrorIs(t, err, ErrVerifyWarpMessage) } func TestGetBlockSignature(t *testing.T) { @@ -86,7 +86,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, nil, db, messageSignatureCache, nil) require.NoError(err) blockHashPayload, err := payload.NewHash(blkID) @@ -98,10 +98,10 @@ func TestGetBlockSignature(t *testing.T) { signature, err := backend.GetBlockSignature(context.TODO(), blkID) require.NoError(err) - require.Equal(expectedSig, signature[:]) + require.Equal(expectedSig, signature) _, err = backend.GetBlockSignature(context.TODO(), ids.GenerateTestID()) - require.Error(err) + require.ErrorIs(err, ErrValidateBlock) } func TestZeroSizedCache(t *testing.T) { @@ -113,7 +113,7 @@ func TestZeroSizedCache(t *testing.T) { // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) // Add testUnsignedMessage to the warp backend @@ -125,7 +125,7 @@ func TestZeroSizedCache(t *testing.T) { expectedSig, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) - require.Equal(t, expectedSig, signature[:]) + require.Equal(t, expectedSig, signature) } func TestOffChainMessages(t *testing.T) { @@ -153,7 +153,7 @@ func TestOffChainMessages(t *testing.T) { require.NoError(err) expectedSignatureBytes, err := warpSigner.Sign(msg) require.NoError(err) - require.Equal(expectedSignatureBytes, signature[:]) + require.Equal(expectedSignatureBytes, signature) }, }, "unknown message": { @@ -172,7 +172,7 @@ func TestOffChainMessages(t *testing.T) { db := memdb.New() messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, warptest.NoOpValidatorReader{}, db, messageSignatureCache, test.offchainMessages) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, test.offchainMessages) require.ErrorIs(err, test.err) if test.check != nil { test.check(require, backend) diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 9dee0be6a878..027876aead6d 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -11,7 +11,6 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/vms/evm/warp/validators" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" @@ -23,23 +22,21 @@ var errNoValidators = errors.New("cannot aggregate signatures from subnet with n // API introduces snowman specific functionality to the evm type API struct { - chainContext *snow.Context - backend Backend - signatureAggregator *acp118.SignatureAggregator - requirePrimaryNetworkSigners func() bool + chainContext *snow.Context + backend Backend + signatureAggregator *acp118.SignatureAggregator } -func NewAPI(chainCtx *snow.Context, backend Backend, signatureAggregator *acp118.SignatureAggregator, requirePrimaryNetworkSigners func() bool) *API { +func NewAPI(chainCtx *snow.Context, backend Backend, signatureAggregator *acp118.SignatureAggregator) *API { return &API{ - backend: backend, - chainContext: chainCtx, - signatureAggregator: signatureAggregator, - requirePrimaryNetworkSigners: requirePrimaryNetworkSigners, + backend: backend, + chainContext: chainCtx, + signatureAggregator: signatureAggregator, } } // GetMessage returns the Warp message associated with a messageID. -func (a *API) GetMessage(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { +func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { message, err := a.backend.GetMessage(messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) @@ -57,7 +54,7 @@ func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexuti if err != nil { return nil, fmt.Errorf("failed to get signature for message %s with error %w", messageID, err) } - return signature[:], nil + return signature, nil } // GetBlockSignature returns the BLS signature associated with a blockID. @@ -66,7 +63,7 @@ func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.By if err != nil { return nil, fmt.Errorf("failed to get signature for block %s with error %w", blockID, err) } - return signature[:], nil + return signature, nil } // GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] @@ -107,8 +104,7 @@ func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.Uns return nil, err } - state := validators.NewState(validatorState, a.chainContext.SubnetID, a.chainContext.ChainID, a.requirePrimaryNetworkSigners()) - validatorSet, err := warp.GetCanonicalValidatorSetFromSubnetID(ctx, state, pChainHeight, subnetID) + validatorSet, err := validatorState.GetWarpValidatorSet(ctx, pChainHeight, subnetID) if err != nil { return nil, fmt.Errorf("failed to get validator set: %w", err) } diff --git a/vms/evm/warp/validators/state.go b/vms/evm/warp/validators/state.go deleted file mode 100644 index 58530750698f..000000000000 --- a/vms/evm/warp/validators/state.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package validators - -import ( - "context" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/validators" - "github.com/ava-labs/avalanchego/utils/constants" -) - -var _ validators.State = (*State)(nil) - -// State provides a special case used to handle Avalanche Warp Message verification for messages sent -// from the Primary Network. Subnets have strictly fewer validators than the Primary Network, so we require -// signatures from a threshold of the RECEIVING subnet validator set rather than the full Primary Network -// since the receiving subnet already relies on a majority of its validators being correct. -type State struct { - validators.State - mySubnetID ids.ID - sourceChainID ids.ID - requirePrimaryNetworkSigners bool -} - -// NewState returns a wrapper of [validators.State] which special cases the handling of the Primary Network. -// -// The wrapped state will return the [mySubnetID's] validator set instead of the Primary Network when -// the Primary Network SubnetID is passed in. -func NewState(state validators.State, mySubnetID ids.ID, sourceChainID ids.ID, requirePrimaryNetworkSigners bool) *State { - return &State{ - State: state, - mySubnetID: mySubnetID, - sourceChainID: sourceChainID, - requirePrimaryNetworkSigners: requirePrimaryNetworkSigners, - } -} - -func (s *State) GetValidatorSet( - ctx context.Context, - height uint64, - subnetID ids.ID, -) (map[ids.NodeID]*validators.GetValidatorOutput, error) { - // If the subnetID is anything other than the Primary Network, or Primary - // Network signers are required (except P-Chain), this is a direct passthrough. - usePrimary := s.requirePrimaryNetworkSigners && s.sourceChainID != constants.PlatformChainID - if usePrimary || subnetID != constants.PrimaryNetworkID { - return s.State.GetValidatorSet(ctx, height, subnetID) - } - - // If the requested subnet is the primary network, then we return the validator - // set for the Subnet that is receiving the message instead. - return s.State.GetValidatorSet(ctx, height, s.mySubnetID) -} diff --git a/vms/evm/warp/validators/state_test.go b/vms/evm/warp/validators/state_test.go deleted file mode 100644 index 49ffaf2e3a07..000000000000 --- a/vms/evm/warp/validators/state_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package validators - -import ( - "context" - "testing" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/snowtest" - "github.com/ava-labs/avalanchego/snow/validators" - "github.com/ava-labs/avalanchego/snow/validators/validatorsmock" - "github.com/ava-labs/avalanchego/utils/constants" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestGetValidatorSetPrimaryNetwork(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - mySubnetID := ids.GenerateTestID() - otherSubnetID := ids.GenerateTestID() - - mockState := validatorsmock.NewState(ctrl) - snowCtx := snowtest.Context(t, snowtest.CChainID) - snowCtx.SubnetID = mySubnetID - snowCtx.ValidatorState = mockState - state := NewState(snowCtx.ValidatorState, snowCtx.SubnetID, snowCtx.ChainID, false) - // Expect that requesting my validator set returns my validator set - mockState.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), mySubnetID).Return(make(map[ids.NodeID]*validators.GetValidatorOutput), nil) - output, err := state.GetValidatorSet(context.Background(), 10, mySubnetID) - require.NoError(err) - require.Len(output, 0) - - // Expect that requesting the Primary Network validator set overrides and returns my validator set - mockState.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), mySubnetID).Return(make(map[ids.NodeID]*validators.GetValidatorOutput), nil) - output, err = state.GetValidatorSet(context.Background(), 10, constants.PrimaryNetworkID) - require.NoError(err) - require.Len(output, 0) - - // Expect that requesting other validator set returns that validator set - mockState.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), otherSubnetID).Return(make(map[ids.NodeID]*validators.GetValidatorOutput), nil) - output, err = state.GetValidatorSet(context.Background(), 10, otherSubnetID) - require.NoError(err) - require.Len(output, 0) -} diff --git a/vms/evm/warp/verifier_backend.go b/vms/evm/warp/verifier_backend.go index 9192744c0f26..ce5d3f830d77 100644 --- a/vms/evm/warp/verifier_backend.go +++ b/vms/evm/warp/verifier_backend.go @@ -110,11 +110,11 @@ func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCa } func (b *backend) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { - vdr, currentUptime, _, err := b.validatorReader.GetValidatorAndUptime(uptimeMsg.ValidationID) + currentUptime, _, err := b.uptimeTracker.GetUptime(uptimeMsg.ValidationID) if err != nil { return &common.AppError{ Code: VerifyErrCode, - Message: fmt.Sprintf("failed to get uptime for validationID %s: %s", uptimeMsg.ValidationID, err.Error()), + Message: "failed to get uptime: " + err.Error(), } } @@ -123,7 +123,7 @@ func (b *backend) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *commo if currentUptimeSeconds < uptimeMsg.TotalUptime { return &common.AppError{ Code: VerifyErrCode, - Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for nodeID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, vdr.NodeID), + Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for validationID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, uptimeMsg.ValidationID), } } diff --git a/vms/evm/warp/verifier_backend_test.go b/vms/evm/warp/verifier_backend_test.go index 3cc5c3e1301a..99709c0ba968 100644 --- a/vms/evm/warp/verifier_backend_test.go +++ b/vms/evm/warp/verifier_backend_test.go @@ -5,7 +5,7 @@ package warp import ( "context" - "sync" + "fmt" "testing" "time" @@ -17,18 +17,19 @@ import ( "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/snow/validators/validatorstest" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" - "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - // TODO: FIGURE OUT HOW TO GET RID OF THIS IMPORT - "github.com/ava-labs/subnet-evm/plugin/evm/validators" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + + "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) func TestAddressedCallSignatures(t *testing.T) { @@ -47,7 +48,7 @@ func TestAddressedCallSignatures(t *testing.T) { tests := map[string]struct { setup func(backend Backend) (request []byte, expectedResponse []byte) verifyStats func(t *testing.T, stats *verifierStats) - err error + err *common.AppError }{ "known message": { setup: func(backend Backend) (request []byte, expectedResponse []byte) { @@ -59,20 +60,20 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) backend.AddMessage(msg) - return msg.Bytes(), signature[:] + 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()) + require.Zero(t, stats.messageParseFail.Snapshot().Count()) + require.Zero(t, stats.blockValidationFail.Snapshot().Count()) }, }, "offchain message": { setup: func(_ Backend) (request []byte, expectedResponse []byte) { - return offchainMessage.Bytes(), offchainSignature[:] + 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()) + require.Zero(t, stats.messageParseFail.Snapshot().Count()) + require.Zero(t, stats.blockValidationFail.Snapshot().Count()) }, }, "unknown message": { @@ -84,8 +85,8 @@ func TestAddressedCallSignatures(t *testing.T) { 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()) + require.Equal(t, int64(1), stats.messageParseFail.Snapshot().Count()) + require.Zero(t, stats.blockValidationFail.Snapshot().Count()) }, err: &common.AppError{Code: ParseErrCode}, }, @@ -105,16 +106,7 @@ func TestAddressedCallSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - warpBackend, err := NewBackend( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - warptest.EmptyBlockClient, - nil, - database, - sigCache, - [][]byte{offchainMessage.Bytes()}, - ) + warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockClient, nil, database, sigCache, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) @@ -123,18 +115,12 @@ func TestAddressedCallSignatures(t *testing.T) { protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - if test.err != nil { - require.Error(t, appErr) - require.ErrorIs(t, appErr, test.err) - } else { - require.Nil(t, appErr) - } - + require.ErrorIs(t, appErr, test.err) 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") + require.Empty(t, responseBytes, "expected response to be empty") return } // check cache is populated @@ -179,7 +165,7 @@ func TestBlockSignatures(t *testing.T) { tests := map[string]struct { setup func() (request []byte, expectedResponse []byte) verifyStats func(t *testing.T, stats *verifierStats) - err error + err *common.AppError }{ "known block": { setup: func() (request []byte, expectedResponse []byte) { @@ -189,11 +175,11 @@ func TestBlockSignatures(t *testing.T) { require.NoError(t, err) signature, err := snowCtx.WarpSigner.Sign(unsignedMessage) require.NoError(t, err) - return toMessageBytes(knownBlkID), signature[:] + 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()) + require.Zero(t, stats.blockValidationFail.Snapshot().Count()) + require.Zero(t, stats.messageParseFail.Snapshot().Count()) }, }, "unknown block": { @@ -202,8 +188,8 @@ func TestBlockSignatures(t *testing.T) { 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()) + require.Equal(t, int64(1), stats.blockValidationFail.Snapshot().Count()) + require.Zero(t, stats.messageParseFail.Snapshot().Count()) }, err: &common.AppError{Code: VerifyErrCode}, }, @@ -228,7 +214,7 @@ func TestBlockSignatures(t *testing.T) { snowCtx.ChainID, snowCtx.WarpSigner, blockClient, - warptest.NoOpValidatorReader{}, + nil, database, sigCache, nil, @@ -241,18 +227,13 @@ func TestBlockSignatures(t *testing.T) { protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - if test.err != nil { - require.NotNil(t, appErr) - require.ErrorIs(t, test.err, appErr) - } else { - require.Nil(t, appErr) - } + require.ErrorIs(t, appErr, test.err) 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") + require.Empty(t, responseBytes, "expected response to be empty") return } // check cache is populated @@ -274,12 +255,16 @@ func TestUptimeSignatures(t *testing.T) { database := memdb.New() snowCtx := snowtest.Context(t, snowtest.CChainID) - getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID, totalUptime uint64) ([]byte, *avalancheWarp.UnsignedMessage) { + validationID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + startTime := uint64(time.Now().Unix()) + + getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID) ([]byte, *warp.UnsignedMessage) { uptimePayload, err := message.NewValidatorUptime(vID, 80) require.NoError(t, err) addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) require.NoError(t, err) - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) + unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) require.NoError(t, err) protoMsg := &sdk.SignatureRequest{Message: unsignedMessage.Bytes()} @@ -295,19 +280,42 @@ func TestUptimeSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - chainCtx := snowtest.Context(t, snowtest.CChainID) + + // Create a validator state that includes our test validator + // TODO(JonathanOppenheimer): see func NewTestValidatorState() -- this should be examined + // when we address the issue of that function. + validatorState := &validatorstest.State{ + GetCurrentValidatorSetF: func(context.Context, ids.ID) (map[ids.ID]*validators.GetCurrentValidatorOutput, uint64, error) { + return map[ids.ID]*validators.GetCurrentValidatorOutput{ + validationID: { + ValidationID: validationID, + NodeID: nodeID, + Weight: 1, + StartTime: startTime, + IsActive: true, + IsL1Validator: true, + }, + }, 0, nil + }, + } + clk := &mockable.Clock{} - validatorsManager, err := validators.NewManager(chainCtx, memdb.New(), clk) + uptimeTracker, err := uptimetracker.New( + validatorState, + snowCtx.SubnetID, + memdb.New(), + clk, + ) require.NoError(t, err) - lock := &sync.RWMutex{} - newLockedValidatorManager := validators.NewLockedValidatorReader(validatorsManager, lock) - validatorsManager.StartTracking([]ids.NodeID{}) + + require.NoError(t, uptimeTracker.Sync(context.Background())) + warpBackend, err := NewBackend( snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockClient, - newLockedValidatorManager, + uptimeTracker, database, sigCache, nil, @@ -316,51 +324,41 @@ func TestUptimeSignatures(t *testing.T) { handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) // sourceAddress nonZero - protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID(), 80) + protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) _, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Contains(t, appErr.Error(), "source address should be empty") + require.Equal(t, "2: source address should be empty for offchain addressed messages", appErr.Error()) // not existing validationID vID := ids.GenerateTestID() - protoBytes, _ = getUptimeMessageBytes([]byte{}, vID, 80) + protoBytes, _ = getUptimeMessageBytes([]byte{}, vID) _, appErr = handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Contains(t, appErr.Error(), "failed to get validator") + require.Equal(t, fmt.Sprintf("2: failed to get uptime: validationID not found: %s", vID), appErr.Error()) // uptime is less than requested (not connected) - validationID := ids.GenerateTestID() - nodeID := ids.GenerateTestNodeID() - require.NoError(t, validatorsManager.AddValidator(warptest.Validator{ - ValidationID: validationID, - NodeID: nodeID, - Weight: 1, - StartTimestamp: clk.Unix(), - IsActive: true, - IsL1Validator: true, - })) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID, 80) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Contains(t, appErr.Error(), "current uptime 0 is less than queried uptime 80") + require.Equal(t, fmt.Sprintf("2: current uptime 0 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - // uptime is less than requested (not enough) - require.NoError(t, validatorsManager.Connect(nodeID)) + // uptime is less than requested (not enough time) + require.NoError(t, uptimeTracker.Connect(nodeID)) clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID, 80) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Contains(t, appErr.Error(), "current uptime 40 is less than queried uptime 80") + require.Equal(t, fmt.Sprintf("2: current uptime 40 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - // valid uptime + // valid uptime (enough time has passed) clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID, 80) + protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) responseBytes, appErr := handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) require.Nil(t, appErr) expectedSignature, err := snowCtx.WarpSigner.Sign(msg) require.NoError(t, err) response := &sdk.SignatureResponse{} require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.Equal(t, expectedSignature[:], response.Signature) + require.Equal(t, expectedSignature, response.Signature) } } diff --git a/vms/evm/warp/warptest/noop_validator_reader.go b/vms/evm/warp/warptest/noop_validator_reader.go deleted file mode 100644 index 69d598996fb8..000000000000 --- a/vms/evm/warp/warptest/noop_validator_reader.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// warptest exposes common functionality for testing the warp package. -package warptest - -import ( - "time" - - "github.com/ava-labs/avalanchego/ids" -) - -var _ ValidatorReader = (*NoOpValidatorReader)(nil) - -type Validator struct { - ValidationID ids.ID `json:"validationID"` - NodeID ids.NodeID `json:"nodeID"` - Weight uint64 `json:"weight"` - StartTimestamp uint64 `json:"startTimestamp"` - IsActive bool `json:"isActive"` - IsL1Validator bool `json:"isL1Validator"` -} - -type ValidatorReader interface { - // GetValidatorAndUptime returns the calculated uptime of the validator specified by validationID - // and the last updated time. - // GetValidatorAndUptime holds the VM lock while performing the operation and can be called concurrently. - GetValidatorAndUptime(validationID ids.ID) (Validator, time.Duration, time.Time, error) -} - -type NoOpValidatorReader struct{} - -func (NoOpValidatorReader) GetValidatorAndUptime(ids.ID) (Validator, time.Duration, time.Time, error) { - return Validator{}, 0, time.Time{}, nil -} diff --git a/vms/platformvm/warp/message/codec.go b/vms/platformvm/warp/message/codec.go index d2873bdcfd0c..97ea2e0c695b 100644 --- a/vms/platformvm/warp/message/codec.go +++ b/vms/platformvm/warp/message/codec.go @@ -20,6 +20,7 @@ func init() { lc := linearcodec.NewDefault() err := errors.Join( + lc.RegisterType(&ValidatorUptime{}), lc.RegisterType(&SubnetToL1Conversion{}), lc.RegisterType(&RegisterL1Validator{}), lc.RegisterType(&L1ValidatorRegistration{}), From 017a635deb8a822ce1d1725f48b12064d380be59 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Tue, 28 Oct 2025 16:35:34 -0400 Subject: [PATCH 03/53] lint --- vms/evm/warp/backend.go | 3 ++- vms/evm/warp/backend_test.go | 16 +++++++------- vms/evm/warp/client.go | 3 ++- vms/evm/warp/service.go | 5 +++-- vms/evm/warp/verifier_backend.go | 6 +++--- vms/evm/warp/verifier_backend_test.go | 30 +++++++++++++-------------- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index df58d19dbd76..bf376c38ca4d 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -8,6 +8,8 @@ import ( "errors" "fmt" + "github.com/ava-labs/libevm/log" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database" @@ -16,7 +18,6 @@ import ( "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/ava-labs/libevm/log" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 0dfa7f290eaa..8a0de2a5451b 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -4,9 +4,10 @@ package warp import ( - "context" "testing" + "github.com/stretchr/testify/require" + "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" @@ -16,7 +17,6 @@ import ( "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/stretchr/testify/require" ) var ( @@ -52,7 +52,7 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, backend.AddMessage(testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(context.TODO(), testUnsignedMessage) + signature, err := backend.GetMessageSignature(t.Context(), testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -71,7 +71,7 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) // Try getting a signature for a message that was not added. - _, err = backend.GetMessageSignature(context.TODO(), testUnsignedMessage) + _, err = backend.GetMessageSignature(t.Context(), testUnsignedMessage) require.ErrorIs(t, err, ErrVerifyWarpMessage) } @@ -96,11 +96,11 @@ func TestGetBlockSignature(t *testing.T) { expectedSig, err := warpSigner.Sign(unsignedMessage) require.NoError(err) - signature, err := backend.GetBlockSignature(context.TODO(), blkID) + signature, err := backend.GetBlockSignature(t.Context(), blkID) require.NoError(err) require.Equal(expectedSig, signature) - _, err = backend.GetBlockSignature(context.TODO(), ids.GenerateTestID()) + _, err = backend.GetBlockSignature(t.Context(), ids.GenerateTestID()) require.ErrorIs(err, ErrValidateBlock) } @@ -120,7 +120,7 @@ func TestZeroSizedCache(t *testing.T) { require.NoError(t, backend.AddMessage(testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(context.TODO(), testUnsignedMessage) + signature, err := backend.GetMessageSignature(t.Context(), testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -149,7 +149,7 @@ func TestOffChainMessages(t *testing.T) { require.NoError(err) require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - signature, err := b.GetMessageSignature(context.TODO(), testUnsignedMessage) + signature, err := b.GetMessageSignature(t.Context(), testUnsignedMessage) require.NoError(err) expectedSignatureBytes, err := warpSigner.Sign(msg) require.NoError(err) diff --git a/vms/evm/warp/client.go b/vms/evm/warp/client.go index f59db180b993..91dbbd9fefec 100644 --- a/vms/evm/warp/client.go +++ b/vms/evm/warp/client.go @@ -7,9 +7,10 @@ import ( "context" "fmt" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/libevm/common/hexutil" "github.com/ava-labs/libevm/rpc" + + "github.com/ava-labs/avalanchego/ids" ) var _ Client = (*client)(nil) diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 027876aead6d..0b78beed6ff6 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -8,14 +8,15 @@ import ( "errors" "fmt" + "github.com/ava-labs/libevm/common/hexutil" + "github.com/ava-labs/libevm/log" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/ava-labs/libevm/common/hexutil" - "github.com/ava-labs/libevm/log" ) var errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") diff --git a/vms/evm/warp/verifier_backend.go b/vms/evm/warp/verifier_backend.go index ce5d3f830d77..befa29071584 100644 --- a/vms/evm/warp/verifier_backend.go +++ b/vms/evm/warp/verifier_backend.go @@ -7,12 +7,12 @@ import ( "context" "fmt" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" - "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/snow/engine/common" - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) const ( diff --git a/vms/evm/warp/verifier_backend_test.go b/vms/evm/warp/verifier_backend_test.go index 99709c0ba968..0278a6737123 100644 --- a/vms/evm/warp/verifier_backend_test.go +++ b/vms/evm/warp/verifier_backend_test.go @@ -9,6 +9,9 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database/memdb" @@ -22,14 +25,10 @@ import ( "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) func TestAddressedCallSignatures(t *testing.T) { @@ -58,8 +57,7 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) signature, err := snowCtx.WarpSigner.Sign(msg) require.NoError(t, err) - - backend.AddMessage(msg) + require.NoError(t, backend.AddMessage(msg)) return msg.Bytes(), signature }, verifyStats: func(t *testing.T, stats *verifierStats) { @@ -114,7 +112,7 @@ func TestAddressedCallSignatures(t *testing.T) { protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) test.verifyStats(t, warpBackend.(*backend).stats) @@ -226,7 +224,7 @@ func TestBlockSignatures(t *testing.T) { protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) test.verifyStats(t, warpBackend.(*backend).stats) @@ -308,7 +306,7 @@ func TestUptimeSignatures(t *testing.T) { ) require.NoError(t, err) - require.NoError(t, uptimeTracker.Sync(context.Background())) + require.NoError(t, uptimeTracker.Sync(t.Context())) warpBackend, err := NewBackend( snowCtx.NetworkID, @@ -325,20 +323,20 @@ func TestUptimeSignatures(t *testing.T) { // sourceAddress nonZero protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) - _, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + _, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) require.Equal(t, "2: source address should be empty for offchain addressed messages", appErr.Error()) // not existing validationID vID := ids.GenerateTestID() protoBytes, _ = getUptimeMessageBytes([]byte{}, vID) - _, appErr = handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + _, appErr = handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) require.Equal(t, fmt.Sprintf("2: failed to get uptime: validationID not found: %s", vID), appErr.Error()) // uptime is less than requested (not connected) protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) require.Equal(t, fmt.Sprintf("2: current uptime 0 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) @@ -346,14 +344,14 @@ func TestUptimeSignatures(t *testing.T) { require.NoError(t, uptimeTracker.Connect(nodeID)) clk.Set(clk.Time().Add(40 * time.Second)) protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) require.Equal(t, fmt.Sprintf("2: current uptime 40 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) // valid uptime (enough time has passed) clk.Set(clk.Time().Add(40 * time.Second)) protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) - responseBytes, appErr := handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + responseBytes, appErr := handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) require.Nil(t, appErr) expectedSignature, err := snowCtx.WarpSigner.Sign(msg) require.NoError(t, err) From dce47f587bce15781995b1c34a86bcff5ec219d7 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Tue, 28 Oct 2025 16:39:46 -0400 Subject: [PATCH 04/53] change codec registration order --- vms/platformvm/warp/message/codec.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/warp/message/codec.go b/vms/platformvm/warp/message/codec.go index 97ea2e0c695b..2c11ced7fb2d 100644 --- a/vms/platformvm/warp/message/codec.go +++ b/vms/platformvm/warp/message/codec.go @@ -20,11 +20,11 @@ func init() { lc := linearcodec.NewDefault() err := errors.Join( - lc.RegisterType(&ValidatorUptime{}), lc.RegisterType(&SubnetToL1Conversion{}), lc.RegisterType(&RegisterL1Validator{}), lc.RegisterType(&L1ValidatorRegistration{}), lc.RegisterType(&L1ValidatorWeight{}), + lc.RegisterType(&ValidatorUptime{}), Codec.RegisterCodec(CodecVersion, lc), ) if err != nil { From c8a971b4ad718c13dfb74d419c8ff17edbb071bd Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 11:42:43 -0500 Subject: [PATCH 05/53] unalias imports --- vms/evm/warp/backend.go | 39 +++++++++++++-------------- vms/evm/warp/verifier_backend.go | 5 ++-- vms/evm/warp/warptest/block_client.go | 4 +-- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index bf376c38ca4d..9723486aa39f 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -17,9 +17,8 @@ import ( "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) var ( @@ -39,16 +38,16 @@ type BlockClient interface { // 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 *avalancheWarp.UnsignedMessage) error + AddMessage(unsignedMessage *warp.UnsignedMessage) error // GetMessageSignature validates the message and returns the signature of the requested message. - GetMessageSignature(ctx context.Context, message *avalancheWarp.UnsignedMessage) ([]byte, error) + 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) (*avalancheWarp.UnsignedMessage, error) + GetMessage(messageHash ids.ID) (*warp.UnsignedMessage, error) acp118.Verifier } @@ -58,12 +57,12 @@ type backend struct { networkID uint32 sourceChainID ids.ID db database.Database - warpSigner avalancheWarp.Signer + warpSigner warp.Signer blockClient BlockClient uptimeTracker *uptimetracker.UptimeTracker signatureCache cache.Cacher[ids.ID, []byte] - messageCache *lru.Cache[ids.ID, *avalancheWarp.UnsignedMessage] - offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.UnsignedMessage + messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] + offchainAddressedCallMsgs map[ids.ID]*warp.UnsignedMessage stats *verifierStats } @@ -71,7 +70,7 @@ type backend struct { func NewBackend( networkID uint32, sourceChainID ids.ID, - warpSigner avalancheWarp.Signer, + warpSigner warp.Signer, blockClient BlockClient, uptimeTracker *uptimetracker.UptimeTracker, db database.Database, @@ -86,26 +85,26 @@ func NewBackend( blockClient: blockClient, signatureCache: signatureCache, uptimeTracker: uptimeTracker, - messageCache: lru.NewCache[ids.ID, *avalancheWarp.UnsignedMessage](messageCacheSize), + messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), stats: newVerifierStats(), - offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), + offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), } return b, b.initOffChainMessages(offchainMessages) } func (b *backend) initOffChainMessages(offchainMessages [][]byte) error { for i, offchainMsg := range offchainMessages { - unsignedMsg, err := avalancheWarp.ParseUnsignedMessage(offchainMsg) + 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("%w at index %d", avalancheWarp.ErrWrongNetworkID, i) + return fmt.Errorf("%w at index %d", warp.ErrWrongNetworkID, i) } if unsignedMsg.SourceChainID != b.sourceChainID { - return fmt.Errorf("%w at index %d", avalancheWarp.ErrWrongSourceChainID, i) + return fmt.Errorf("%w at index %d", warp.ErrWrongSourceChainID, i) } _, err = payload.ParseAddressedCall(unsignedMsg.Payload) @@ -118,7 +117,7 @@ func (b *backend) initOffChainMessages(offchainMessages [][]byte) error { return nil } -func (b *backend) AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) error { +func (b *backend) AddMessage(unsignedMessage *warp.UnsignedMessage) error { messageID := unsignedMessage.ID() log.Debug("Adding warp message to backend", "messageID", messageID) @@ -135,7 +134,7 @@ func (b *backend) AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) err return nil } -func (b *backend) GetMessageSignature(ctx context.Context, unsignedMessage *avalancheWarp.UnsignedMessage) ([]byte, error) { +func (b *backend) GetMessageSignature(ctx context.Context, unsignedMessage *warp.UnsignedMessage) ([]byte, error) { messageID := unsignedMessage.ID() log.Debug("Getting warp message from backend", "messageID", messageID) @@ -158,7 +157,7 @@ func (b *backend) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte return nil, fmt.Errorf("failed to create new block hash payload: %w", err) } - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(b.networkID, b.sourceChainID, blockHashPayload.Bytes()) + 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) } @@ -178,7 +177,7 @@ func (b *backend) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte return sig, nil } -func (b *backend) GetMessage(messageID ids.ID) (*avalancheWarp.UnsignedMessage, error) { +func (b *backend) GetMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { if message, ok := b.messageCache.Get(messageID); ok { return message, nil } @@ -191,7 +190,7 @@ func (b *backend) GetMessage(messageID ids.ID) (*avalancheWarp.UnsignedMessage, return nil, err } - unsignedMessage, err := avalancheWarp.ParseUnsignedMessage(unsignedMessageBytes) + unsignedMessage, err := warp.ParseUnsignedMessage(unsignedMessageBytes) if err != nil { return nil, fmt.Errorf("failed to parse unsigned message %s: %w", messageID.String(), err) } @@ -200,7 +199,7 @@ func (b *backend) GetMessage(messageID ids.ID) (*avalancheWarp.UnsignedMessage, return unsignedMessage, nil } -func (b *backend) signMessage(unsignedMessage *avalancheWarp.UnsignedMessage) ([]byte, error) { +func (b *backend) signMessage(unsignedMessage *warp.UnsignedMessage) ([]byte, error) { sig, err := b.warpSigner.Sign(unsignedMessage) if err != nil { return nil, fmt.Errorf("failed to sign warp message: %w", err) diff --git a/vms/evm/warp/verifier_backend.go b/vms/evm/warp/verifier_backend.go index befa29071584..1a9a5b10baf8 100644 --- a/vms/evm/warp/verifier_backend.go +++ b/vms/evm/warp/verifier_backend.go @@ -9,10 +9,9 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) const ( @@ -22,7 +21,7 @@ const ( // Verify verifies the signature of the message // It also implements the acp118.Verifier interface -func (b *backend) Verify(ctx context.Context, unsignedMessage *avalancheWarp.UnsignedMessage, _ []byte) *common.AppError { +func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) *common.AppError { messageID := unsignedMessage.ID() // Known on-chain messages should be signed if _, err := b.GetMessage(messageID); err == nil { diff --git a/vms/evm/warp/warptest/block_client.go b/vms/evm/warp/warptest/block_client.go index 7f98d4f79786..74fecdc5195c 100644 --- a/vms/evm/warp/warptest/block_client.go +++ b/vms/evm/warp/warptest/block_client.go @@ -20,8 +20,8 @@ var EmptyBlockClient BlockClient = MakeBlockClient() type BlockClient func(ctx context.Context, blockID ids.ID) (snowman.Block, error) -func (f BlockClient) GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) { - return f(ctx, blockID) +func (b BlockClient) GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) { + return b(ctx, blockID) } // MakeBlockClient returns a new BlockClient that returns the provided blocks. From d1f2a07279aac68e1591ca3ee9ee432c0dda8b76 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 11:45:44 -0500 Subject: [PATCH 06/53] Refine comments --- vms/evm/warp/backend.go | 7 ------- vms/evm/warp/client.go | 4 ++-- vms/evm/warp/verifier_stats.go | 9 +++------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 9723486aa39f..90dc08b0a2f1 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -37,16 +37,9 @@ type BlockClient interface { // 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) acp118.Verifier diff --git a/vms/evm/warp/client.go b/vms/evm/warp/client.go index 91dbbd9fefec..9009145df302 100644 --- a/vms/evm/warp/client.go +++ b/vms/evm/warp/client.go @@ -23,12 +23,12 @@ type Client interface { GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) } -// client implementation for interacting with EVM [chain] +// client implementation for interacting with EVM chain type client struct { client *rpc.Client } -// NewClient returns a Client for interacting with EVM [chain] +// NewClient returns a Client for interacting with EVM chain func NewClient(uri, chain string) (Client, error) { innerClient, err := rpc.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", uri, chain)) if err != nil { diff --git a/vms/evm/warp/verifier_stats.go b/vms/evm/warp/verifier_stats.go index 6c870ce848fa..67486b4ab3f6 100644 --- a/vms/evm/warp/verifier_stats.go +++ b/vms/evm/warp/verifier_stats.go @@ -6,13 +6,10 @@ package warp import "github.com/ava-labs/libevm/metrics" type verifierStats struct { - messageParseFail metrics.Counter - // AddressedCall metrics + messageParseFail metrics.Counter addressedCallValidationFail metrics.Counter - // BlockRequest metrics - blockValidationFail metrics.Counter - // Uptime metrics - uptimeValidationFail metrics.Counter + blockValidationFail metrics.Counter + uptimeValidationFail metrics.Counter } func newVerifierStats() *verifierStats { From 8b7447429507cb1235e26818cc3ded9ee99f0498 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 11:46:28 -0500 Subject: [PATCH 07/53] Update vms/evm/warp/backend.go Signed-off-by: Jonathan Oppenheimer <147infiniti@gmail.com> --- vms/evm/warp/backend.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 90dc08b0a2f1..0f5d19987d31 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -23,11 +23,12 @@ import ( var ( _ Backend = (*backend)(nil) + + messageCacheSize = 500 + + errParsingOffChainMessage = errors.New("failed to parse off-chain message") ErrValidateBlock = errors.New("failed to validate block message") ErrVerifyWarpMessage = errors.New("failed to verify warp message") - errParsingOffChainMessage = errors.New("failed to parse off-chain message") - - messageCacheSize = 500 ) type BlockClient interface { From 7b7e10e253ae299827a2f1e689406e2c475f6335 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 11:58:01 -0500 Subject: [PATCH 08/53] use payload --- .../warp/message/validator_uptime.go | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/vms/platformvm/warp/message/validator_uptime.go b/vms/platformvm/warp/message/validator_uptime.go index 47d12f4306a3..3f16d11b9ffc 100644 --- a/vms/platformvm/warp/message/validator_uptime.go +++ b/vms/platformvm/warp/message/validator_uptime.go @@ -12,19 +12,19 @@ import ( // ValidatorUptime is signed when the ValidationID is known and the validator // has been up for TotalUptime seconds. type ValidatorUptime struct { + payload + ValidationID ids.ID `serialize:"true"` TotalUptime uint64 `serialize:"true"` // in seconds - - bytes []byte } // NewValidatorUptime creates a new *ValidatorUptime and initializes it. func NewValidatorUptime(validationID ids.ID, totalUptime uint64) (*ValidatorUptime, error) { - bhp := &ValidatorUptime{ + vu := &ValidatorUptime{ ValidationID: validationID, TotalUptime: totalUptime, } - return bhp, Initialize(bhp) + return vu, Initialize(vu) } // ParseValidatorUptime converts a slice of bytes into an initialized ValidatorUptime. @@ -39,13 +39,3 @@ func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { } return payload, nil } - -// Bytes returns the binary representation of this payload. It assumes that the -// payload is initialized from either NewValidatorUptime or Parse. -func (b *ValidatorUptime) Bytes() []byte { - return b.bytes -} - -func (b *ValidatorUptime) initialize(bytes []byte) { - b.bytes = bytes -} From b12444e06e9a460f3b4d6251a8f35d3c80d91690 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 12:12:18 -0500 Subject: [PATCH 09/53] Remove constructor --- vms/evm/warp/verifier_backend_test.go | 6 +++++- vms/platformvm/warp/message/validator_uptime.go | 9 --------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/vms/evm/warp/verifier_backend_test.go b/vms/evm/warp/verifier_backend_test.go index 0278a6737123..6d08a6e2c894 100644 --- a/vms/evm/warp/verifier_backend_test.go +++ b/vms/evm/warp/verifier_backend_test.go @@ -258,7 +258,11 @@ func TestUptimeSignatures(t *testing.T) { startTime := uint64(time.Now().Unix()) getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID) ([]byte, *warp.UnsignedMessage) { - uptimePayload, err := message.NewValidatorUptime(vID, 80) + uptimePayload := &message.ValidatorUptime{ + ValidationID: vID, + TotalUptime: 80, + } + err := message.Initialize(uptimePayload) require.NoError(t, err) addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) require.NoError(t, err) diff --git a/vms/platformvm/warp/message/validator_uptime.go b/vms/platformvm/warp/message/validator_uptime.go index 3f16d11b9ffc..aa2cf94b4e7e 100644 --- a/vms/platformvm/warp/message/validator_uptime.go +++ b/vms/platformvm/warp/message/validator_uptime.go @@ -18,15 +18,6 @@ type ValidatorUptime struct { TotalUptime uint64 `serialize:"true"` // in seconds } -// NewValidatorUptime creates a new *ValidatorUptime and initializes it. -func NewValidatorUptime(validationID ids.ID, totalUptime uint64) (*ValidatorUptime, error) { - vu := &ValidatorUptime{ - ValidationID: validationID, - TotalUptime: totalUptime, - } - return vu, Initialize(vu) -} - // ParseValidatorUptime converts a slice of bytes into an initialized ValidatorUptime. func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { payloadIntf, err := Parse(b) From d9c117f60e036ec2899f265b25ab23b1feafead0 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 12:12:41 -0500 Subject: [PATCH 10/53] Update vms/evm/warp/verifier_backend_test.go Signed-off-by: Jonathan Oppenheimer <147infiniti@gmail.com> --- vms/evm/warp/verifier_backend_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/evm/warp/verifier_backend_test.go b/vms/evm/warp/verifier_backend_test.go index 6d08a6e2c894..da333a7369c5 100644 --- a/vms/evm/warp/verifier_backend_test.go +++ b/vms/evm/warp/verifier_backend_test.go @@ -229,7 +229,7 @@ func TestBlockSignatures(t *testing.T) { test.verifyStats(t, warpBackend.(*backend).stats) - // If the expected response is empty, assert that the handler returns an empty response and return early. + // If the expected response is empty, require that the handler returns an empty response and return early. if len(expectedResponse) == 0 { require.Empty(t, responseBytes, "expected response to be empty") return From 1f3ebd4b7c4e2b33361048fdb2b0ee45a9342ccb Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 12:32:18 -0500 Subject: [PATCH 11/53] Simplify structs / rename items --- vms/evm/warp/backend.go | 20 ++++++++++---------- vms/evm/warp/backend_test.go | 4 ++-- vms/evm/warp/client.go | 27 ++++++++------------------- vms/evm/warp/verifier_backend.go | 2 +- vms/evm/warp/verifier_backend_test.go | 11 +++++------ vms/evm/warp/warptest/block_client.go | 12 ++++++------ 6 files changed, 32 insertions(+), 44 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 0f5d19987d31..451f05f0cd3c 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -22,17 +22,17 @@ import ( ) var ( - _ Backend = (*backend)(nil) - + _ Backend = (*backend)(nil) + messageCacheSize = 500 - - errParsingOffChainMessage = errors.New("failed to parse off-chain message") - ErrValidateBlock = errors.New("failed to validate block message") - ErrVerifyWarpMessage = errors.New("failed to verify warp message") + + errParsingOffChainMessage = errors.New("failed to parse off-chain message") + ErrValidateBlock = errors.New("failed to validate block message") + ErrVerifyWarpMessage = errors.New("failed to verify warp message") ) -type BlockClient interface { - GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) +type BlockStore interface { + GetBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) } // Backend tracks signature-eligible warp messages and provides an interface to fetch them. @@ -52,7 +52,7 @@ type backend struct { sourceChainID ids.ID db database.Database warpSigner warp.Signer - blockClient BlockClient + blockClient BlockStore uptimeTracker *uptimetracker.UptimeTracker signatureCache cache.Cacher[ids.ID, []byte] messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] @@ -65,7 +65,7 @@ func NewBackend( networkID uint32, sourceChainID ids.ID, warpSigner warp.Signer, - blockClient BlockClient, + blockClient BlockStore, uptimeTracker *uptimetracker.UptimeTracker, db database.Database, signatureCache cache.Cacher[ids.ID, []byte], diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 8a0de2a5451b..177a576573d9 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -79,14 +79,14 @@ func TestGetBlockSignature(t *testing.T) { require := require.New(t) blkID := ids.GenerateTestID() - blockClient := warptest.MakeBlockClient(blkID) + blockStore := warptest.MakeBlockStore(blkID) db := memdb.New() sk, err := localsigner.New() require.NoError(err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, nil, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockStore, nil, db, messageSignatureCache, nil) require.NoError(err) blockHashPayload, err := payload.NewHash(blkID) diff --git a/vms/evm/warp/client.go b/vms/evm/warp/client.go index 9009145df302..223210f7c3ad 100644 --- a/vms/evm/warp/client.go +++ b/vms/evm/warp/client.go @@ -13,33 +13,22 @@ import ( "github.com/ava-labs/avalanchego/ids" ) -var _ Client = (*client)(nil) - -type Client interface { - GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) - GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) - GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) - GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) - GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) -} - -// client implementation for interacting with EVM chain -type client struct { +type Client struct { client *rpc.Client } // NewClient returns a Client for interacting with EVM chain -func NewClient(uri, chain string) (Client, error) { +func NewClient(uri, chain string) (*Client, error) { innerClient, err := rpc.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", uri, chain)) if err != nil { return nil, fmt.Errorf("failed to dial client. err: %w", err) } - return &client{ + return &Client{ client: innerClient, }, nil } -func (c *client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) { +func (c *Client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessage", messageID); err != nil { return nil, fmt.Errorf("call to warp_getMessage failed. err: %w", err) @@ -47,7 +36,7 @@ func (c *client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, erro return res, nil } -func (c *client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { +func (c *Client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessageSignature", messageID); err != nil { return nil, fmt.Errorf("call to warp_getMessageSignature failed. err: %w", err) @@ -55,7 +44,7 @@ func (c *client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]b return res, nil } -func (c *client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { +func (c *Client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetIDStr); err != nil { return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed. err: %w", err) @@ -63,7 +52,7 @@ func (c *client) GetMessageAggregateSignature(ctx context.Context, messageID ids return res, nil } -func (c *client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { +func (c *Client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getBlockSignature", blockID); err != nil { return nil, fmt.Errorf("call to warp_getBlockSignature failed. err: %w", err) @@ -71,7 +60,7 @@ func (c *client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, return res, nil } -func (c *client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { +func (c *Client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetIDStr); err != nil { return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed. err: %w", err) diff --git a/vms/evm/warp/verifier_backend.go b/vms/evm/warp/verifier_backend.go index 1a9a5b10baf8..b920a20e22ba 100644 --- a/vms/evm/warp/verifier_backend.go +++ b/vms/evm/warp/verifier_backend.go @@ -60,7 +60,7 @@ func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMess // of an accepted block indicating it should be signed by the VM. func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { blockID := blockHashPayload.Hash - _, err := b.blockClient.GetAcceptedBlock(ctx, blockID) + _, err := b.blockClient.GetBlock(ctx, blockID) if err != nil { b.stats.IncBlockValidationFail() return &common.AppError{ diff --git a/vms/evm/warp/verifier_backend_test.go b/vms/evm/warp/verifier_backend_test.go index da333a7369c5..fa211a3c0e53 100644 --- a/vms/evm/warp/verifier_backend_test.go +++ b/vms/evm/warp/verifier_backend_test.go @@ -104,7 +104,7 @@ func TestAddressedCallSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockClient, nil, database, sigCache, [][]byte{offchainMessage.Bytes()}) + warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockStore, nil, database, sigCache, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) @@ -144,7 +144,7 @@ func TestBlockSignatures(t *testing.T) { snowCtx := snowtest.Context(t, snowtest.CChainID) knownBlkID := ids.GenerateTestID() - blockClient := warptest.MakeBlockClient(knownBlkID) + blockStore := warptest.MakeBlockStore(knownBlkID) toMessageBytes := func(id ids.ID) []byte { idPayload, err := payload.NewHash(id) @@ -211,7 +211,7 @@ func TestBlockSignatures(t *testing.T) { snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, - blockClient, + blockStore, nil, database, sigCache, @@ -262,8 +262,7 @@ func TestUptimeSignatures(t *testing.T) { ValidationID: vID, TotalUptime: 80, } - err := message.Initialize(uptimePayload) - require.NoError(t, err) + require.NoError(t, message.Initialize(uptimePayload)) addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) require.NoError(t, err) unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) @@ -316,7 +315,7 @@ func TestUptimeSignatures(t *testing.T) { snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, - warptest.EmptyBlockClient, + warptest.EmptyBlockStore, uptimeTracker, database, sigCache, diff --git a/vms/evm/warp/warptest/block_client.go b/vms/evm/warp/warptest/block_client.go index 74fecdc5195c..5e00d0936320 100644 --- a/vms/evm/warp/warptest/block_client.go +++ b/vms/evm/warp/warptest/block_client.go @@ -15,19 +15,19 @@ import ( "github.com/ava-labs/avalanchego/snow/snowtest" ) -// EmptyBlockClient returns an error if a block is requested -var EmptyBlockClient BlockClient = MakeBlockClient() +// EmptyBlockStore returns an error if a block is requested +var EmptyBlockStore BlockStore = MakeBlockStore() -type BlockClient func(ctx context.Context, blockID ids.ID) (snowman.Block, error) +type BlockStore func(ctx context.Context, blockID ids.ID) (snowman.Block, error) -func (b BlockClient) GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) { +func (b BlockStore) GetBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) { return b(ctx, blockID) } -// MakeBlockClient returns a new BlockClient that returns the provided blocks. +// MakeBlockStore returns a new BlockStore that returns the provided blocks. // If a block is requested that isn't part of the provided blocks, an error is // returned. -func MakeBlockClient(blkIDs ...ids.ID) BlockClient { +func MakeBlockStore(blkIDs ...ids.ID) BlockStore { return func(_ context.Context, blkID ids.ID) (snowman.Block, error) { if !slices.Contains(blkIDs, blkID) { return nil, database.ErrNotFound From 44078f0cfa62e7b5d628c5bb006f2557d278d0bf Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 13:56:49 -0500 Subject: [PATCH 12/53] fold metrics in --- vms/evm/warp/backend.go | 30 +++++++++++++-------- vms/evm/warp/verifier_backend.go | 12 ++++----- vms/evm/warp/verifier_backend_test.go | 38 +++++++++++++-------------- vms/evm/warp/verifier_stats.go | 38 --------------------------- 4 files changed, 44 insertions(+), 74 deletions(-) delete mode 100644 vms/evm/warp/verifier_stats.go diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 451f05f0cd3c..07bbe5c1caf1 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -9,6 +9,7 @@ import ( "fmt" "github.com/ava-labs/libevm/log" + "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" @@ -57,7 +58,11 @@ type backend struct { signatureCache cache.Cacher[ids.ID, []byte] messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] offchainAddressedCallMsgs map[ids.ID]*warp.UnsignedMessage - stats *verifierStats + + messageParseFail metrics.Counter + addressedCallValidationFail metrics.Counter + blockValidationFail metrics.Counter + uptimeValidationFail metrics.Counter } // NewBackend creates a new Backend, and initializes the signature cache and message tracking database. @@ -72,16 +77,19 @@ func NewBackend( offchainMessages [][]byte, ) (Backend, error) { b := &backend{ - networkID: networkID, - sourceChainID: sourceChainID, - db: db, - warpSigner: warpSigner, - blockClient: blockClient, - signatureCache: signatureCache, - uptimeTracker: uptimeTracker, - messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), - stats: newVerifierStats(), - offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), + networkID: networkID, + sourceChainID: sourceChainID, + db: db, + warpSigner: warpSigner, + blockClient: blockClient, + signatureCache: signatureCache, + uptimeTracker: uptimeTracker, + messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), + offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), + messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), + addressedCallValidationFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_validation_fail", nil), + blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), + uptimeValidationFail: metrics.NewRegisteredCounter("warp_backend_uptime_validation_fail", nil), } return b, b.initOffChainMessages(offchainMessages) } diff --git a/vms/evm/warp/verifier_backend.go b/vms/evm/warp/verifier_backend.go index b920a20e22ba..3b8f2181b8b9 100644 --- a/vms/evm/warp/verifier_backend.go +++ b/vms/evm/warp/verifier_backend.go @@ -35,7 +35,7 @@ func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMess parsed, err := payload.Parse(unsignedMessage.Payload) if err != nil { - b.stats.IncMessageParseFail() + b.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, Message: "failed to parse payload: " + err.Error(), @@ -48,7 +48,7 @@ func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMess case *payload.Hash: return b.verifyBlockMessage(ctx, p) default: - b.stats.IncMessageParseFail() + b.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, Message: fmt.Sprintf("unknown payload type: %T", p), @@ -62,7 +62,7 @@ func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payl blockID := blockHashPayload.Hash _, err := b.blockClient.GetBlock(ctx, blockID) if err != nil { - b.stats.IncBlockValidationFail() + b.blockValidationFail.Inc(1) return &common.AppError{ Code: VerifyErrCode, Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), @@ -77,7 +77,7 @@ func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCa // Further, parse the payload to see if it is a known type. parsed, err := message.Parse(addressedCall.Payload) if err != nil { - b.stats.IncMessageParseFail() + b.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, Message: "failed to parse addressed call message: " + err.Error(), @@ -94,11 +94,11 @@ func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCa switch p := parsed.(type) { case *message.ValidatorUptime: if err := b.verifyUptimeMessage(p); err != nil { - b.stats.IncUptimeValidationFail() + b.uptimeValidationFail.Inc(1) return err } default: - b.stats.IncMessageParseFail() + b.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, Message: fmt.Sprintf("unknown message type: %T", p), diff --git a/vms/evm/warp/verifier_backend_test.go b/vms/evm/warp/verifier_backend_test.go index fa211a3c0e53..c45824405919 100644 --- a/vms/evm/warp/verifier_backend_test.go +++ b/vms/evm/warp/verifier_backend_test.go @@ -46,7 +46,7 @@ func TestAddressedCallSignatures(t *testing.T) { tests := map[string]struct { setup func(backend Backend) (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, stats *verifierStats) + verifyStats func(t *testing.T, b *backend) err *common.AppError }{ "known message": { @@ -60,18 +60,18 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, backend.AddMessage(msg)) return msg.Bytes(), signature }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Zero(t, stats.messageParseFail.Snapshot().Count()) - require.Zero(t, stats.blockValidationFail.Snapshot().Count()) + verifyStats: func(t *testing.T, b *backend) { + require.Zero(t, b.messageParseFail.Snapshot().Count()) + require.Zero(t, b.blockValidationFail.Snapshot().Count()) }, }, "offchain message": { setup: func(_ Backend) (request []byte, expectedResponse []byte) { return offchainMessage.Bytes(), offchainSignature }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Zero(t, stats.messageParseFail.Snapshot().Count()) - require.Zero(t, stats.blockValidationFail.Snapshot().Count()) + verifyStats: func(t *testing.T, b *backend) { + require.Zero(t, b.messageParseFail.Snapshot().Count()) + require.Zero(t, b.blockValidationFail.Snapshot().Count()) }, }, "unknown message": { @@ -82,9 +82,9 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) return unknownMessage.Bytes(), nil }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Equal(t, int64(1), stats.messageParseFail.Snapshot().Count()) - require.Zero(t, stats.blockValidationFail.Snapshot().Count()) + verifyStats: func(t *testing.T, b *backend) { + require.Equal(t, int64(1), b.messageParseFail.Snapshot().Count()) + require.Zero(t, b.blockValidationFail.Snapshot().Count()) }, err: &common.AppError{Code: ParseErrCode}, }, @@ -114,7 +114,7 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, warpBackend.(*backend).stats) + test.verifyStats(t, warpBackend.(*backend)) // If the expected response is empty, assert that the handler returns an empty response and return early. if len(expectedResponse) == 0 { @@ -162,7 +162,7 @@ func TestBlockSignatures(t *testing.T) { tests := map[string]struct { setup func() (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, stats *verifierStats) + verifyStats func(t *testing.T, b *backend) err *common.AppError }{ "known block": { @@ -175,9 +175,9 @@ func TestBlockSignatures(t *testing.T) { require.NoError(t, err) return toMessageBytes(knownBlkID), signature }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Zero(t, stats.blockValidationFail.Snapshot().Count()) - require.Zero(t, stats.messageParseFail.Snapshot().Count()) + verifyStats: func(t *testing.T, b *backend) { + require.Zero(t, b.blockValidationFail.Snapshot().Count()) + require.Zero(t, b.messageParseFail.Snapshot().Count()) }, }, "unknown block": { @@ -185,9 +185,9 @@ func TestBlockSignatures(t *testing.T) { unknownBlockID := ids.GenerateTestID() return toMessageBytes(unknownBlockID), nil }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Equal(t, int64(1), stats.blockValidationFail.Snapshot().Count()) - require.Zero(t, stats.messageParseFail.Snapshot().Count()) + verifyStats: func(t *testing.T, b *backend) { + require.Equal(t, int64(1), b.blockValidationFail.Snapshot().Count()) + require.Zero(t, b.messageParseFail.Snapshot().Count()) }, err: &common.AppError{Code: VerifyErrCode}, }, @@ -227,7 +227,7 @@ func TestBlockSignatures(t *testing.T) { responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, warpBackend.(*backend).stats) + test.verifyStats(t, warpBackend.(*backend)) // If the expected response is empty, require that the handler returns an empty response and return early. if len(expectedResponse) == 0 { diff --git a/vms/evm/warp/verifier_stats.go b/vms/evm/warp/verifier_stats.go deleted file mode 100644 index 67486b4ab3f6..000000000000 --- a/vms/evm/warp/verifier_stats.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import "github.com/ava-labs/libevm/metrics" - -type verifierStats struct { - messageParseFail metrics.Counter - addressedCallValidationFail metrics.Counter - blockValidationFail metrics.Counter - uptimeValidationFail metrics.Counter -} - -func newVerifierStats() *verifierStats { - return &verifierStats{ - messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), - addressedCallValidationFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_validation_fail", nil), - blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), - uptimeValidationFail: metrics.NewRegisteredCounter("warp_backend_uptime_validation_fail", nil), - } -} - -func (h *verifierStats) IncAddressedCallValidationFail() { - h.addressedCallValidationFail.Inc(1) -} - -func (h *verifierStats) IncBlockValidationFail() { - h.blockValidationFail.Inc(1) -} - -func (h *verifierStats) IncMessageParseFail() { - h.messageParseFail.Inc(1) -} - -func (h *verifierStats) IncUptimeValidationFail() { - h.uptimeValidationFail.Inc(1) -} From 1cb5f50d0fa9ff1bd6a70db52c3c21869bb82cac Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 15:51:58 -0500 Subject: [PATCH 13/53] First pass refactor --- vms/evm/warp/backend.go | 359 ++++++++++++++++------- vms/evm/warp/backend_test.go | 393 ++++++++++++++++++++++++-- vms/evm/warp/service.go | 36 ++- vms/evm/warp/verifier_backend.go | 130 --------- vms/evm/warp/verifier_backend_test.go | 365 ------------------------ 5 files changed, 659 insertions(+), 624 deletions(-) delete mode 100644 vms/evm/warp/verifier_backend.go delete mode 100644 vms/evm/warp/verifier_backend_test.go diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 07bbe5c1caf1..2dd3d2b4832e 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -15,15 +15,20 @@ import ( "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) var ( - _ Backend = (*backend)(nil) + _ Backend = (*backend)(nil) + _ acp118.Verifier = (*backend)(nil) + _ p2p.Handler = (*Handler)(nil) messageCacheSize = 500 @@ -32,33 +37,125 @@ var ( ErrVerifyWarpMessage = errors.New("failed to verify warp message") ) +const ( + ParseErrCode = iota + 1 + VerifyErrCode +) + +// BlockStore provides access to accepted blocks. type BlockStore interface { GetBlock(ctx context.Context, blockID ids.ID) (snowman.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(unsignedMessage *warp.UnsignedMessage) error - GetMessageSignature(ctx context.Context, message *warp.UnsignedMessage) ([]byte, error) - GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) - GetMessage(messageHash ids.ID) (*warp.UnsignedMessage, error) - - acp118.Verifier -} - -// backend implements Backend, keeps track of warp messages, and generates message signatures. -type backend struct { +// DB stores and retrieves warp messages. +type DB struct { networkID uint32 sourceChainID ids.ID db database.Database - warpSigner warp.Signer - blockClient BlockStore - uptimeTracker *uptimetracker.UptimeTracker - signatureCache cache.Cacher[ids.ID, []byte] messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] offchainAddressedCallMsgs map[ids.ID]*warp.UnsignedMessage +} + +// Add stores a warp message in the database and cache. +func (d *DB) Add(unsignedMsg *warp.UnsignedMessage) error { + msgID := unsignedMsg.ID() + log.Debug("Adding warp message to backend", "messageID", msgID) + + // 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 := d.db.Put(msgID[:], unsignedMsg.Bytes()); err != nil { + return fmt.Errorf("failed to put warp message in db: %w", err) + } + + return nil +} +// Get retrieves a warp message from cache, offchain messages, or database. +func (d *DB) Get(msgID ids.ID) (*warp.UnsignedMessage, error) { + if msg, ok := d.messageCache.Get(msgID); ok { + return msg, nil + } + if msg, ok := d.offchainAddressedCallMsgs[msgID]; ok { + return msg, nil + } + + unsignedMessageBytes, err := d.db.Get(msgID[:]) + 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", msgID.String(), err) + } + d.messageCache.Put(msgID, unsignedMessage) + + return unsignedMessage, nil +} + +// Signer signs warp messages and caches the signatures. +type Signer struct { + warpSigner warp.Signer + verifier acp118.Verifier + signatureCache cache.Cacher[ids.ID, []byte] +} + +// Sign verifies the warp message, signs it, and caches the signature. +func (s *Signer) Sign(ctx context.Context, msg *warp.UnsignedMessage) ([]byte, error) { + // Check cache first + msgID := msg.ID() + if sig, ok := s.signatureCache.Get(msgID); ok { + return sig, nil + } + + if err := s.verifier.Verify(ctx, msg, nil); err != nil { + return nil, fmt.Errorf("%w: %w", ErrVerifyWarpMessage, err) + } + + sig, err := s.warpSigner.Sign(msg) + if err != nil { + return nil, fmt.Errorf("failed to sign warp message: %w", err) + } + + s.signatureCache.Put(msgID, sig) + return sig, nil +} + +// Handler implements p2p.Handler and handles warp signature requests. +// It hides the acp118.Verifier implementation as an implementation detail. +type Handler struct { + *acp118.Handler +} + +// NewHandler creates a new p2p warp signature request handler. +func NewHandler( + signatureCache cache.Cacher[ids.ID, []byte], + verifier acp118.Verifier, + signer warp.Signer, +) p2p.Handler { + return &Handler{ + Handler: acp118.NewCachedHandler(signatureCache, verifier, signer), + } +} + +// Backend tracks signature-eligible warp messages and provides an interface to fetch them. +// The backend is used by the warp API service to retrieve messages. +type Backend interface { + AddMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) error + GetMessage(messageHash ids.ID) (*warp.UnsignedMessage, error) +} + +// backend implements Backend and keeps track of warp messages. +type backend struct { + db *DB + signer *Signer + blockClient BlockStore + uptimeTracker *uptimetracker.UptimeTracker + networkID uint32 + sourceChainID ids.ID + + // Metrics messageParseFail metrics.Counter addressedCallValidationFail metrics.Counter blockValidationFail metrics.Counter @@ -75,138 +172,190 @@ func NewBackend( db database.Database, signatureCache cache.Cacher[ids.ID, []byte], offchainMessages [][]byte, -) (Backend, error) { +) (Backend, *Signer, p2p.Handler, error) { + messageDB := &DB{ + networkID: networkID, + sourceChainID: sourceChainID, + db: db, + messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), + offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), + } + + if err := initOffChainMessages(messageDB, networkID, sourceChainID, offchainMessages); err != nil { + return nil, nil, nil, err + } + b := &backend{ - networkID: networkID, - sourceChainID: sourceChainID, - db: db, - warpSigner: warpSigner, + db: messageDB, blockClient: blockClient, - signatureCache: signatureCache, uptimeTracker: uptimeTracker, - messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), - offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), + networkID: networkID, + sourceChainID: sourceChainID, messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), addressedCallValidationFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_validation_fail", nil), blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), uptimeValidationFail: metrics.NewRegisteredCounter("warp_backend_uptime_validation_fail", nil), } - 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("%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 + signer := &Signer{ + warpSigner: warpSigner, + verifier: b, + signatureCache: signatureCache, } + b.signer = signer - return nil -} + handler := NewHandler(signatureCache, b, warpSigner) -func (b *backend) AddMessage(unsignedMessage *warp.UnsignedMessage) error { - messageID := unsignedMessage.ID() - log.Debug("Adding warp message to backend", "messageID", messageID) + return b, signer, handler, nil +} - // 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) +func (b *backend) AddMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) error { + if err := b.db.Add(unsignedMessage); err != nil { + return err } - if _, err := b.signMessage(unsignedMessage); err != nil { - return fmt.Errorf("failed to sign warp message: %w", err) + // Fill the signature cache now so subsequent requests can serve the + // signature without repeating verification or signing work. + if _, err := b.signer.Sign(ctx, unsignedMessage); err != nil { + return err } return nil } -func (b *backend) GetMessageSignature(ctx context.Context, unsignedMessage *warp.UnsignedMessage) ([]byte, error) { - messageID := unsignedMessage.ID() +func (b *backend) GetMessage(messageHash ids.ID) (*warp.UnsignedMessage, error) { + return b.db.Get(messageHash) +} - log.Debug("Getting warp message from backend", "messageID", messageID) - if sig, ok := b.signatureCache.Get(messageID); ok { - return sig, nil +// Verify implements acp118.Verifier and validates whether a warp message should be signed. +func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) *common.AppError { + messageID := unsignedMessage.ID() + // Known on-chain messages should be signed + if _, err := b.db.Get(messageID); err == nil { + return nil + } else if err != database.ErrNotFound { + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("failed to get message %s: %s", messageID, err.Error()), + } } - if err := b.Verify(ctx, unsignedMessage, nil); err != nil { - return nil, fmt.Errorf("%w: %w", ErrVerifyWarpMessage, err) + parsed, err := payload.Parse(unsignedMessage.Payload) + if err != nil { + b.messageParseFail.Inc(1) + return &common.AppError{ + Code: ParseErrCode, + Message: "failed to parse payload: " + err.Error(), + } } - return b.signMessage(unsignedMessage) + switch p := parsed.(type) { + case *payload.AddressedCall: + return b.verifyOffchainAddressedCall(p) + case *payload.Hash: + return b.verifyBlockMessage(ctx, p) + default: + b.messageParseFail.Inc(1) + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("unknown payload type: %T", p), + } + } } -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) +// verifyBlockMessage returns nil if blockHashPayload contains the ID +// of an accepted block indicating it should be signed by the VM. +func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { + blockID := blockHashPayload.Hash + _, err := b.blockClient.GetBlock(ctx, blockID) if err != nil { - return nil, fmt.Errorf("failed to create new block hash payload: %w", err) + b.blockValidationFail.Inc(1) + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), + } } - unsignedMessage, err := warp.NewUnsignedMessage(b.networkID, b.sourceChainID, blockHashPayload.Bytes()) + return nil +} + +// verifyOffchainAddressedCall verifies the addressed call message +func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { + // Further, parse the payload to see if it is a known type. + parsed, err := message.Parse(addressedCall.Payload) if err != nil { - return nil, fmt.Errorf("failed to create new unsigned warp message: %w", err) + b.messageParseFail.Inc(1) + return &common.AppError{ + Code: ParseErrCode, + Message: "failed to parse addressed call message: " + err.Error(), + } } - if sig, ok := b.signatureCache.Get(unsignedMessage.ID()); ok { - return sig, nil + if len(addressedCall.SourceAddress) != 0 { + return &common.AppError{ + Code: VerifyErrCode, + Message: "source address should be empty for offchain addressed messages", + } } - if err := b.verifyBlockMessage(ctx, blockHashPayload); err != nil { - return nil, fmt.Errorf("%w: %w", ErrValidateBlock, err) + switch p := parsed.(type) { + case *message.ValidatorUptime: + if err := b.verifyUptimeMessage(p); err != nil { + b.uptimeValidationFail.Inc(1) + return err + } + default: + b.messageParseFail.Inc(1) + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("unknown message type: %T", p), + } } - sig, err := b.signMessage(unsignedMessage) - if err != nil { - return nil, fmt.Errorf("failed to sign block message: %w", err) - } - return sig, nil + return nil } -func (b *backend) GetMessage(messageID ids.ID) (*warp.UnsignedMessage, 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[:]) +func (b *backend) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { + currentUptime, _, err := b.uptimeTracker.GetUptime(uptimeMsg.ValidationID) if err != nil { - return nil, err + return &common.AppError{ + Code: VerifyErrCode, + Message: "failed to get uptime: " + err.Error(), + } } - unsignedMessage, err := warp.ParseUnsignedMessage(unsignedMessageBytes) - if err != nil { - return nil, fmt.Errorf("failed to parse unsigned message %s: %w", messageID.String(), err) + currentUptimeSeconds := uint64(currentUptime.Seconds()) + // verify the current uptime against the total uptime in the message + if currentUptimeSeconds < uptimeMsg.TotalUptime { + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for validationID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, uptimeMsg.ValidationID), + } } - b.messageCache.Put(messageID, unsignedMessage) - return unsignedMessage, nil + return nil } -func (b *backend) signMessage(unsignedMessage *warp.UnsignedMessage) ([]byte, error) { - sig, err := b.warpSigner.Sign(unsignedMessage) - if err != nil { - return nil, fmt.Errorf("failed to sign warp message: %w", err) +func initOffChainMessages(db *DB, networkID uint32, sourceChainID ids.ID, 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 != networkID { + return fmt.Errorf("%w at index %d", warp.ErrWrongNetworkID, i) + } + + if unsignedMsg.SourceChainID != 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) + } + db.offchainAddressedCallMsgs[unsignedMsg.ID()] = unsignedMsg } - b.signatureCache.Put(unsignedMessage.ID(), sig) - return sig, nil + return nil } diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 177a576573d9..1e145620f77b 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -4,18 +4,32 @@ package warp import ( + "context" + "fmt" "testing" + "time" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/snow/validators/validatorstest" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" + "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) @@ -45,14 +59,15 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + backend, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) + ctx := context.Background() // Add testUnsignedMessage to the warp backend - require.NoError(t, backend.AddMessage(testUnsignedMessage)) + require.NoError(t, backend.AddMessage(ctx, testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(t.Context(), testUnsignedMessage) + signature, err := signer.Sign(ctx, testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -67,11 +82,10 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + _, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) - // Try getting a signature for a message that was not added. - _, err = backend.GetMessageSignature(t.Context(), testUnsignedMessage) + _, err = signer.Sign(context.Background(), testUnsignedMessage) require.ErrorIs(t, err, ErrVerifyWarpMessage) } @@ -86,8 +100,9 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockStore, nil, db, messageSignatureCache, nil) + _, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, blockStore, nil, db, messageSignatureCache, nil) require.NoError(err) + ctx := context.Background() blockHashPayload, err := payload.NewHash(blkID) require.NoError(err) @@ -96,12 +111,18 @@ func TestGetBlockSignature(t *testing.T) { expectedSig, err := warpSigner.Sign(unsignedMessage) require.NoError(err) - signature, err := backend.GetBlockSignature(t.Context(), blkID) + // Callers construct the block message and use Signer.Sign + signature, err := signer.Sign(ctx, unsignedMessage) require.NoError(err) require.Equal(expectedSig, signature) - _, err = backend.GetBlockSignature(t.Context(), ids.GenerateTestID()) - require.ErrorIs(err, ErrValidateBlock) + // Test that an unknown block can still be signed by Signer (verification is caller's responsibility) + unknownBlockHashPayload, err := payload.NewHash(ids.GenerateTestID()) + require.NoError(err) + unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) + require.NoError(err) + _, err = signer.Sign(ctx, unknownUnsignedMessage) + require.ErrorIs(err, ErrVerifyWarpMessage) } func TestZeroSizedCache(t *testing.T) { @@ -113,14 +134,15 @@ func TestZeroSizedCache(t *testing.T) { // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + backend, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) + ctx := context.Background() // Add testUnsignedMessage to the warp backend - require.NoError(t, backend.AddMessage(testUnsignedMessage)) + require.NoError(t, backend.AddMessage(ctx, testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(t.Context(), testUnsignedMessage) + signature, err := signer.Sign(ctx, testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -131,7 +153,7 @@ func TestZeroSizedCache(t *testing.T) { func TestOffChainMessages(t *testing.T) { type test struct { offchainMessages [][]byte - check func(require *require.Assertions, b Backend) + check func(require *require.Assertions, b Backend, s *Signer) err error } sk, err := localsigner.New() @@ -144,12 +166,12 @@ func TestOffChainMessages(t *testing.T) { offchainMessages: [][]byte{ testUnsignedMessage.Bytes(), }, - check: func(require *require.Assertions, b Backend) { + check: func(require *require.Assertions, b Backend, s *Signer) { msg, err := b.GetMessage(testUnsignedMessage.ID()) require.NoError(err) require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - signature, err := b.GetMessageSignature(t.Context(), testUnsignedMessage) + signature, err := s.Sign(context.Background(), testUnsignedMessage) require.NoError(err) expectedSignatureBytes, err := warpSigner.Sign(msg) require.NoError(err) @@ -157,9 +179,12 @@ func TestOffChainMessages(t *testing.T) { }, }, "unknown message": { - check: func(require *require.Assertions, b Backend) { + check: func(require *require.Assertions, b Backend, s *Signer) { _, err := b.GetMessage(testUnsignedMessage.ID()) require.ErrorIs(err, database.ErrNotFound) + + _, err = s.Sign(context.Background(), testUnsignedMessage) + require.ErrorIs(err, ErrVerifyWarpMessage) }, }, "invalid message": { @@ -172,11 +197,341 @@ func TestOffChainMessages(t *testing.T) { db := memdb.New() messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, test.offchainMessages) + backend, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, test.offchainMessages) require.ErrorIs(err, test.err) if test.check != nil { - test.check(require, backend) + test.check(require, backend, signer) } }) } } + +func TestAddressedCallSignatures(t *testing.T) { + metricstest.WithMetrics(t) + + database := memdb.New() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + offChainPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) + require.NoError(t, err) + offchainMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offChainPayload.Bytes()) + require.NoError(t, err) + offchainSignature, err := snowCtx.WarpSigner.Sign(offchainMessage) + require.NoError(t, err) + + tests := map[string]struct { + setup func(backend Backend) (request []byte, expectedResponse []byte) + verifyStats func(t *testing.T, b *backend) + err *common.AppError + }{ + "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.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) + require.NoError(t, err) + signature, err := snowCtx.WarpSigner.Sign(msg) + require.NoError(t, err) + require.NoError(t, backend.AddMessage(context.Background(), msg)) + return msg.Bytes(), signature + }, + verifyStats: func(t *testing.T, b *backend) { + require.Zero(t, b.messageParseFail.Snapshot().Count()) + require.Zero(t, b.blockValidationFail.Snapshot().Count()) + }, + }, + "offchain message": { + setup: func(_ Backend) (request []byte, expectedResponse []byte) { + return offchainMessage.Bytes(), offchainSignature + }, + verifyStats: func(t *testing.T, b *backend) { + require.Zero(t, b.messageParseFail.Snapshot().Count()) + require.Zero(t, b.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.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) + require.NoError(t, err) + return unknownMessage.Bytes(), nil + }, + verifyStats: func(t *testing.T, b *backend) { + require.Equal(t, int64(1), b.messageParseFail.Snapshot().Count()) + require.Zero(t, b.blockValidationFail.Snapshot().Count()) + }, + err: &common.AppError{Code: ParseErrCode}, + }, + } + + for name, test := range tests { + for _, withCache := range []bool{true, false} { + if withCache { + name += "_with_cache" + } else { + name += "_no_cache" + } + t.Run(name, 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, signer, handler, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockStore, nil, database, sigCache, [][]byte{offchainMessage.Bytes()}) + require.NoError(t, err) + + requestBytes, expectedResponse := test.setup(warpBackend) + protoMsg := &sdk.SignatureRequest{Message: requestBytes} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, test.err) + test.verifyStats(t, warpBackend.(*backend)) + + // If the expected response is empty, assert that the handler returns an empty response and return early. + if len(expectedResponse) == 0 { + require.Empty(t, responseBytes, "expected response to be empty") + return + } + // check cache is populated + if withCache { + require.NotZero(t, signer.signatureCache.Len()) + } else { + require.Zero(t, signer.signatureCache.Len()) + } + response := &sdk.SignatureResponse{} + require.NoError(t, proto.Unmarshal(responseBytes, response)) + require.NoError(t, err, "error unmarshalling SignatureResponse") + + require.Equal(t, expectedResponse, response.Signature) + }) + } + } +} + +func TestBlockSignatures(t *testing.T) { + metricstest.WithMetrics(t) + + database := memdb.New() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + knownBlkID := ids.GenerateTestID() + blockStore := warptest.MakeBlockStore(knownBlkID) + + toMessageBytes := func(id ids.ID) []byte { + idPayload, err := payload.NewHash(id) + if err != nil { + panic(err) + } + + msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.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, b *backend) + err *common.AppError + }{ + "known block": { + setup: func() (request []byte, expectedResponse []byte) { + hashPayload, err := payload.NewHash(knownBlkID) + require.NoError(t, err) + unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) + require.NoError(t, err) + signature, err := snowCtx.WarpSigner.Sign(unsignedMessage) + require.NoError(t, err) + return toMessageBytes(knownBlkID), signature + }, + verifyStats: func(t *testing.T, b *backend) { + require.Zero(t, b.blockValidationFail.Snapshot().Count()) + require.Zero(t, b.messageParseFail.Snapshot().Count()) + }, + }, + "unknown block": { + setup: func() (request []byte, expectedResponse []byte) { + unknownBlockID := ids.GenerateTestID() + return toMessageBytes(unknownBlockID), nil + }, + verifyStats: func(t *testing.T, b *backend) { + require.Equal(t, int64(1), b.blockValidationFail.Snapshot().Count()) + require.Zero(t, b.messageParseFail.Snapshot().Count()) + }, + err: &common.AppError{Code: VerifyErrCode}, + }, + } + + for name, test := range tests { + for _, withCache := range []bool{true, false} { + if withCache { + name += "_with_cache" + } else { + name += "_no_cache" + } + t.Run(name, 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, signer, handler, err := NewBackend( + snowCtx.NetworkID, + snowCtx.ChainID, + snowCtx.WarpSigner, + blockStore, + nil, + database, + sigCache, + nil, + ) + require.NoError(t, err) + + requestBytes, expectedResponse := test.setup() + protoMsg := &sdk.SignatureRequest{Message: requestBytes} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, test.err) + + test.verifyStats(t, warpBackend.(*backend)) + + // If the expected response is empty, require that the handler returns an empty response and return early. + if len(expectedResponse) == 0 { + require.Empty(t, responseBytes, "expected response to be empty") + return + } + // check cache is populated + if withCache { + require.NotZero(t, signer.signatureCache.Len()) + } else { + require.Zero(t, signer.signatureCache.Len()) + } + var response sdk.SignatureResponse + err = proto.Unmarshal(responseBytes, &response) + require.NoError(t, err, "error unmarshalling SignatureResponse") + require.Equal(t, expectedResponse, response.Signature) + }) + } + } +} + +func TestUptimeSignatures(t *testing.T) { + database := memdb.New() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + validationID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + startTime := uint64(time.Now().Unix()) + + getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID) ([]byte, *warp.UnsignedMessage) { + uptimePayload := &message.ValidatorUptime{ + ValidationID: vID, + TotalUptime: 80, + } + require.NoError(t, message.Initialize(uptimePayload)) + addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) + require.NoError(t, err) + unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) + require.NoError(t, err) + + protoMsg := &sdk.SignatureRequest{Message: unsignedMessage.Bytes()} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + return protoBytes, unsignedMessage + } + + for _, withCache := range []bool{true, false} { + var sigCache cache.Cacher[ids.ID, []byte] + if withCache { + sigCache = lru.NewCache[ids.ID, []byte](100) + } else { + sigCache = &cache.Empty[ids.ID, []byte]{} + } + + // Create a validator state that includes our test validator + // TODO(JonathanOppenheimer): see func NewTestValidatorState() -- this should be examined + // when we address the issue of that function. + validatorState := &validatorstest.State{ + GetCurrentValidatorSetF: func(context.Context, ids.ID) (map[ids.ID]*validators.GetCurrentValidatorOutput, uint64, error) { + return map[ids.ID]*validators.GetCurrentValidatorOutput{ + validationID: { + ValidationID: validationID, + NodeID: nodeID, + Weight: 1, + StartTime: startTime, + IsActive: true, + IsL1Validator: true, + }, + }, 0, nil + }, + } + + clk := &mockable.Clock{} + uptimeTracker, err := uptimetracker.New( + validatorState, + snowCtx.SubnetID, + memdb.New(), + clk, + ) + require.NoError(t, err) + + require.NoError(t, uptimeTracker.Sync(t.Context())) + + _, _, handler, err := NewBackend( + snowCtx.NetworkID, + snowCtx.ChainID, + snowCtx.WarpSigner, + warptest.EmptyBlockStore, + uptimeTracker, + database, + sigCache, + nil, + ) + require.NoError(t, err) + + // sourceAddress nonZero + protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) + _, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Equal(t, "2: source address should be empty for offchain addressed messages", appErr.Error()) + + // not existing validationID + vID := ids.GenerateTestID() + protoBytes, _ = getUptimeMessageBytes([]byte{}, vID) + _, appErr = handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Equal(t, fmt.Sprintf("2: failed to get uptime: validationID not found: %s", vID), appErr.Error()) + + // uptime is less than requested (not connected) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) + _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Equal(t, fmt.Sprintf("2: current uptime 0 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) + + // uptime is less than requested (not enough time) + require.NoError(t, uptimeTracker.Connect(nodeID)) + clk.Set(clk.Time().Add(40 * time.Second)) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) + _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Equal(t, fmt.Sprintf("2: current uptime 40 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) + + // valid uptime (enough time has passed) + clk.Set(clk.Time().Add(40 * time.Second)) + protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) + responseBytes, appErr := handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) + require.Nil(t, appErr) + expectedSignature, err := snowCtx.WarpSigner.Sign(msg) + require.NoError(t, err) + response := &sdk.SignatureResponse{} + require.NoError(t, proto.Unmarshal(responseBytes, response)) + require.Equal(t, expectedSignature, response.Signature) + } +} diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 0b78beed6ff6..f03b67bae9af 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -25,12 +25,16 @@ var errNoValidators = errors.New("cannot aggregate signatures from subnet with n type API struct { chainContext *snow.Context backend Backend + signer *Signer + blockClient BlockStore signatureAggregator *acp118.SignatureAggregator } -func NewAPI(chainCtx *snow.Context, backend Backend, signatureAggregator *acp118.SignatureAggregator) *API { +func NewAPI(chainCtx *snow.Context, backend Backend, signer *Signer, blockClient BlockStore, signatureAggregator *acp118.SignatureAggregator) *API { return &API{ backend: backend, + signer: signer, + blockClient: blockClient, chainContext: chainCtx, signatureAggregator: signatureAggregator, } @@ -51,18 +55,40 @@ func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexuti if err != nil { return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) } - signature, err := a.backend.GetMessageSignature(ctx, unsignedMessage) + + signature, err := a.signer.Sign(ctx, unsignedMessage) if err != nil { - return nil, fmt.Errorf("failed to get signature for message %s with error %w", messageID, err) + return nil, fmt.Errorf("failed to sign message %s with error %w", messageID, err) } return signature, nil } // GetBlockSignature returns the BLS signature associated with a blockID. +// It constructs a warp message with a Hash payload containing the blockID, +// then returns the signature for that message. func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { - signature, err := a.backend.GetBlockSignature(ctx, blockID) + // Verify the block exists before signing + if _, err := a.blockClient.GetBlock(ctx, blockID); err != nil { + return nil, fmt.Errorf("failed to get block %s: %w", blockID, err) + } + + blockHashPayload, err := payload.NewHash(blockID) + if err != nil { + return nil, fmt.Errorf("failed to create block hash payload: %w", err) + } + + unsignedMessage, err := warp.NewUnsignedMessage( + a.chainContext.NetworkID, + a.chainContext.ChainID, + blockHashPayload.Bytes(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create unsigned warp message: %w", err) + } + + signature, err := a.signer.Sign(ctx, unsignedMessage) if err != nil { - return nil, fmt.Errorf("failed to get signature for block %s with error %w", blockID, err) + return nil, fmt.Errorf("failed to sign block %s with error %w", blockID, err) } return signature, nil } diff --git a/vms/evm/warp/verifier_backend.go b/vms/evm/warp/verifier_backend.go deleted file mode 100644 index 3b8f2181b8b9..000000000000 --- a/vms/evm/warp/verifier_backend.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "fmt" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" -) - -const ( - ParseErrCode = iota + 1 - VerifyErrCode -) - -// Verify verifies the signature of the message -// It also implements the acp118.Verifier interface -func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) *common.AppError { - messageID := unsignedMessage.ID() - // Known on-chain messages should be signed - if _, err := b.GetMessage(messageID); err == nil { - return nil - } else if err != database.ErrNotFound { - return &common.AppError{ - Code: ParseErrCode, - Message: fmt.Sprintf("failed to get message %s: %s", messageID, err.Error()), - } - } - - parsed, err := payload.Parse(unsignedMessage.Payload) - if err != nil { - b.messageParseFail.Inc(1) - return &common.AppError{ - Code: ParseErrCode, - Message: "failed to parse payload: " + err.Error(), - } - } - - switch p := parsed.(type) { - case *payload.AddressedCall: - return b.verifyOffchainAddressedCall(p) - case *payload.Hash: - return b.verifyBlockMessage(ctx, p) - default: - b.messageParseFail.Inc(1) - return &common.AppError{ - Code: ParseErrCode, - Message: fmt.Sprintf("unknown payload type: %T", p), - } - } -} - -// verifyBlockMessage returns nil if blockHashPayload contains the ID -// of an accepted block indicating it should be signed by the VM. -func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { - blockID := blockHashPayload.Hash - _, err := b.blockClient.GetBlock(ctx, blockID) - if err != nil { - b.blockValidationFail.Inc(1) - return &common.AppError{ - Code: VerifyErrCode, - Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), - } - } - - return nil -} - -// verifyOffchainAddressedCall verifies the addressed call message -func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { - // Further, parse the payload to see if it is a known type. - parsed, err := message.Parse(addressedCall.Payload) - if err != nil { - b.messageParseFail.Inc(1) - return &common.AppError{ - Code: ParseErrCode, - Message: "failed to parse addressed call message: " + err.Error(), - } - } - - if len(addressedCall.SourceAddress) != 0 { - return &common.AppError{ - Code: VerifyErrCode, - Message: "source address should be empty for offchain addressed messages", - } - } - - switch p := parsed.(type) { - case *message.ValidatorUptime: - if err := b.verifyUptimeMessage(p); err != nil { - b.uptimeValidationFail.Inc(1) - return err - } - default: - b.messageParseFail.Inc(1) - return &common.AppError{ - Code: ParseErrCode, - Message: fmt.Sprintf("unknown message type: %T", p), - } - } - - return nil -} - -func (b *backend) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { - currentUptime, _, err := b.uptimeTracker.GetUptime(uptimeMsg.ValidationID) - if err != nil { - return &common.AppError{ - Code: VerifyErrCode, - Message: "failed to get uptime: " + err.Error(), - } - } - - currentUptimeSeconds := uint64(currentUptime.Seconds()) - // verify the current uptime against the total uptime in the message - if currentUptimeSeconds < uptimeMsg.TotalUptime { - return &common.AppError{ - Code: VerifyErrCode, - Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for validationID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, uptimeMsg.ValidationID), - } - } - - return nil -} diff --git a/vms/evm/warp/verifier_backend_test.go b/vms/evm/warp/verifier_backend_test.go deleted file mode 100644 index c45824405919..000000000000 --- a/vms/evm/warp/verifier_backend_test.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/ava-labs/avalanchego/cache" - "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/proto/pb/sdk" - "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/snowtest" - "github.com/ava-labs/avalanchego/snow/validators" - "github.com/ava-labs/avalanchego/snow/validators/validatorstest" - "github.com/ava-labs/avalanchego/utils/timer/mockable" - "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" - "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" - "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" -) - -func TestAddressedCallSignatures(t *testing.T) { - metricstest.WithMetrics(t) - - database := memdb.New() - snowCtx := snowtest.Context(t, snowtest.CChainID) - - offChainPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) - require.NoError(t, err) - offchainMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offChainPayload.Bytes()) - require.NoError(t, err) - offchainSignature, err := snowCtx.WarpSigner.Sign(offchainMessage) - require.NoError(t, err) - - tests := map[string]struct { - setup func(backend Backend) (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, b *backend) - err *common.AppError - }{ - "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.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) - require.NoError(t, err) - signature, err := snowCtx.WarpSigner.Sign(msg) - require.NoError(t, err) - require.NoError(t, backend.AddMessage(msg)) - return msg.Bytes(), signature - }, - verifyStats: func(t *testing.T, b *backend) { - require.Zero(t, b.messageParseFail.Snapshot().Count()) - require.Zero(t, b.blockValidationFail.Snapshot().Count()) - }, - }, - "offchain message": { - setup: func(_ Backend) (request []byte, expectedResponse []byte) { - return offchainMessage.Bytes(), offchainSignature - }, - verifyStats: func(t *testing.T, b *backend) { - require.Zero(t, b.messageParseFail.Snapshot().Count()) - require.Zero(t, b.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.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) - require.NoError(t, err) - return unknownMessage.Bytes(), nil - }, - verifyStats: func(t *testing.T, b *backend) { - require.Equal(t, int64(1), b.messageParseFail.Snapshot().Count()) - require.Zero(t, b.blockValidationFail.Snapshot().Count()) - }, - err: &common.AppError{Code: ParseErrCode}, - }, - } - - for name, test := range tests { - for _, withCache := range []bool{true, false} { - if withCache { - name += "_with_cache" - } else { - name += "_no_cache" - } - t.Run(name, 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(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockStore, nil, database, sigCache, [][]byte{offchainMessage.Bytes()}) - require.NoError(t, err) - handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) - - requestBytes, expectedResponse := test.setup(warpBackend) - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, warpBackend.(*backend)) - - // If the expected response is empty, assert that the handler returns an empty response and return early. - if len(expectedResponse) == 0 { - require.Empty(t, responseBytes, "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()) - } - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.NoError(t, err, "error unmarshalling SignatureResponse") - - require.Equal(t, expectedResponse, response.Signature) - }) - } - } -} - -func TestBlockSignatures(t *testing.T) { - metricstest.WithMetrics(t) - - database := memdb.New() - snowCtx := snowtest.Context(t, snowtest.CChainID) - - knownBlkID := ids.GenerateTestID() - blockStore := warptest.MakeBlockStore(knownBlkID) - - toMessageBytes := func(id ids.ID) []byte { - idPayload, err := payload.NewHash(id) - if err != nil { - panic(err) - } - - msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.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, b *backend) - err *common.AppError - }{ - "known block": { - setup: func() (request []byte, expectedResponse []byte) { - hashPayload, err := payload.NewHash(knownBlkID) - require.NoError(t, err) - unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) - require.NoError(t, err) - signature, err := snowCtx.WarpSigner.Sign(unsignedMessage) - require.NoError(t, err) - return toMessageBytes(knownBlkID), signature - }, - verifyStats: func(t *testing.T, b *backend) { - require.Zero(t, b.blockValidationFail.Snapshot().Count()) - require.Zero(t, b.messageParseFail.Snapshot().Count()) - }, - }, - "unknown block": { - setup: func() (request []byte, expectedResponse []byte) { - unknownBlockID := ids.GenerateTestID() - return toMessageBytes(unknownBlockID), nil - }, - verifyStats: func(t *testing.T, b *backend) { - require.Equal(t, int64(1), b.blockValidationFail.Snapshot().Count()) - require.Zero(t, b.messageParseFail.Snapshot().Count()) - }, - err: &common.AppError{Code: VerifyErrCode}, - }, - } - - for name, test := range tests { - for _, withCache := range []bool{true, false} { - if withCache { - name += "_with_cache" - } else { - name += "_no_cache" - } - t.Run(name, 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( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - blockStore, - nil, - database, - sigCache, - nil, - ) - require.NoError(t, err) - handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) - - requestBytes, expectedResponse := test.setup() - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, test.err) - - test.verifyStats(t, warpBackend.(*backend)) - - // If the expected response is empty, require that the handler returns an empty response and return early. - if len(expectedResponse) == 0 { - require.Empty(t, responseBytes, "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()) - } - var response sdk.SignatureResponse - err = proto.Unmarshal(responseBytes, &response) - require.NoError(t, err, "error unmarshalling SignatureResponse") - require.Equal(t, expectedResponse, response.Signature) - }) - } - } -} - -func TestUptimeSignatures(t *testing.T) { - database := memdb.New() - snowCtx := snowtest.Context(t, snowtest.CChainID) - - validationID := ids.GenerateTestID() - nodeID := ids.GenerateTestNodeID() - startTime := uint64(time.Now().Unix()) - - getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID) ([]byte, *warp.UnsignedMessage) { - uptimePayload := &message.ValidatorUptime{ - ValidationID: vID, - TotalUptime: 80, - } - require.NoError(t, message.Initialize(uptimePayload)) - addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) - require.NoError(t, err) - unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) - require.NoError(t, err) - - protoMsg := &sdk.SignatureRequest{Message: unsignedMessage.Bytes()} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - return protoBytes, unsignedMessage - } - - for _, withCache := range []bool{true, false} { - var sigCache cache.Cacher[ids.ID, []byte] - if withCache { - sigCache = lru.NewCache[ids.ID, []byte](100) - } else { - sigCache = &cache.Empty[ids.ID, []byte]{} - } - - // Create a validator state that includes our test validator - // TODO(JonathanOppenheimer): see func NewTestValidatorState() -- this should be examined - // when we address the issue of that function. - validatorState := &validatorstest.State{ - GetCurrentValidatorSetF: func(context.Context, ids.ID) (map[ids.ID]*validators.GetCurrentValidatorOutput, uint64, error) { - return map[ids.ID]*validators.GetCurrentValidatorOutput{ - validationID: { - ValidationID: validationID, - NodeID: nodeID, - Weight: 1, - StartTime: startTime, - IsActive: true, - IsL1Validator: true, - }, - }, 0, nil - }, - } - - clk := &mockable.Clock{} - uptimeTracker, err := uptimetracker.New( - validatorState, - snowCtx.SubnetID, - memdb.New(), - clk, - ) - require.NoError(t, err) - - require.NoError(t, uptimeTracker.Sync(t.Context())) - - warpBackend, err := NewBackend( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - warptest.EmptyBlockStore, - uptimeTracker, - database, - sigCache, - nil, - ) - require.NoError(t, err) - handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) - - // sourceAddress nonZero - protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) - _, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, "2: source address should be empty for offchain addressed messages", appErr.Error()) - - // not existing validationID - vID := ids.GenerateTestID() - protoBytes, _ = getUptimeMessageBytes([]byte{}, vID) - _, appErr = handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: failed to get uptime: validationID not found: %s", vID), appErr.Error()) - - // uptime is less than requested (not connected) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: current uptime 0 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - - // uptime is less than requested (not enough time) - require.NoError(t, uptimeTracker.Connect(nodeID)) - clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: current uptime 40 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - - // valid uptime (enough time has passed) - clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) - responseBytes, appErr := handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.Nil(t, appErr) - expectedSignature, err := snowCtx.WarpSigner.Sign(msg) - require.NoError(t, err) - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.Equal(t, expectedSignature, response.Signature) - } -} From 5454474dab738da773d56ff1cdd740ce7bf834ff Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 15:54:14 -0500 Subject: [PATCH 14/53] constants first --- vms/evm/warp/backend.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 2dd3d2b4832e..7d4f05066868 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -25,6 +25,11 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) +const ( + ParseErrCode = iota + 1 + VerifyErrCode +) + var ( _ Backend = (*backend)(nil) _ acp118.Verifier = (*backend)(nil) @@ -37,11 +42,6 @@ var ( ErrVerifyWarpMessage = errors.New("failed to verify warp message") ) -const ( - ParseErrCode = iota + 1 - VerifyErrCode -) - // BlockStore provides access to accepted blocks. type BlockStore interface { GetBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) From 4674da5b5eb124167657476435f97518bb359776 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 16:27:04 -0500 Subject: [PATCH 15/53] Refactor part 2: get rid of interface --- vms/evm/warp/backend.go | 83 +++++++++++++------------- vms/evm/warp/backend_test.go | 111 ++++++++++++++++++----------------- vms/evm/warp/service.go | 12 ++-- 3 files changed, 103 insertions(+), 103 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 7d4f05066868..58cda5d99876 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -31,9 +31,8 @@ const ( ) var ( - _ Backend = (*backend)(nil) - _ acp118.Verifier = (*backend)(nil) _ p2p.Handler = (*Handler)(nil) + _ acp118.Verifier = (*verifier)(nil) messageCacheSize = 500 @@ -139,17 +138,16 @@ func NewHandler( } } -// Backend tracks signature-eligible warp messages and provides an interface to fetch them. -// The backend is used by the warp API service to retrieve messages. -type Backend interface { - AddMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) error - GetMessage(messageHash ids.ID) (*warp.UnsignedMessage, error) +// Components bundles the warp message handling components. +type Components struct { + DB *DB + Signer *Signer + Handler p2p.Handler } -// backend implements Backend and keeps track of warp messages. -type backend struct { +// verifier implements acp118.Verifier and validates whether a warp message should be signed. +type verifier struct { db *DB - signer *Signer blockClient BlockStore uptimeTracker *uptimetracker.UptimeTracker networkID uint32 @@ -162,8 +160,8 @@ type backend struct { uptimeValidationFail metrics.Counter } -// NewBackend creates a new Backend, and initializes the signature cache and message tracking database. -func NewBackend( +// New creates warp backend components and initializes the signature cache and message tracking database. +func New( networkID uint32, sourceChainID ids.ID, warpSigner warp.Signer, @@ -172,7 +170,7 @@ func NewBackend( db database.Database, signatureCache cache.Cacher[ids.ID, []byte], offchainMessages [][]byte, -) (Backend, *Signer, p2p.Handler, error) { +) (*Components, error) { messageDB := &DB{ networkID: networkID, sourceChainID: sourceChainID, @@ -182,10 +180,10 @@ func NewBackend( } if err := initOffChainMessages(messageDB, networkID, sourceChainID, offchainMessages); err != nil { - return nil, nil, nil, err + return nil, err } - b := &backend{ + v := &verifier{ db: messageDB, blockClient: blockClient, uptimeTracker: uptimeTracker, @@ -199,38 +197,39 @@ func NewBackend( signer := &Signer{ warpSigner: warpSigner, - verifier: b, + verifier: v, signatureCache: signatureCache, } - b.signer = signer - handler := NewHandler(signatureCache, b, warpSigner) + handler := NewHandler(signatureCache, v, warpSigner) - return b, signer, handler, nil + return &Components{ + DB: messageDB, + Signer: signer, + Handler: handler, + }, nil } -func (b *backend) AddMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) error { - if err := b.db.Add(unsignedMessage); err != nil { +// AddAndSign adds a warp message to the database and signs it. +// This is the typical entry point when a message is created on-chain (e.g., via the warp precompile). +func AddAndSign(ctx context.Context, db *DB, signer *Signer, unsignedMessage *warp.UnsignedMessage) error { + if err := db.Add(unsignedMessage); err != nil { return err } // Fill the signature cache now so subsequent requests can serve the // signature without repeating verification or signing work. - if _, err := b.signer.Sign(ctx, unsignedMessage); err != nil { + if _, err := signer.Sign(ctx, unsignedMessage); err != nil { return err } return nil } -func (b *backend) GetMessage(messageHash ids.ID) (*warp.UnsignedMessage, error) { - return b.db.Get(messageHash) -} - // Verify implements acp118.Verifier and validates whether a warp message should be signed. -func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) *common.AppError { +func (v *verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) *common.AppError { messageID := unsignedMessage.ID() // Known on-chain messages should be signed - if _, err := b.db.Get(messageID); err == nil { + if _, err := v.db.Get(messageID); err == nil { return nil } else if err != database.ErrNotFound { return &common.AppError{ @@ -241,7 +240,7 @@ func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMess parsed, err := payload.Parse(unsignedMessage.Payload) if err != nil { - b.messageParseFail.Inc(1) + v.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, Message: "failed to parse payload: " + err.Error(), @@ -250,11 +249,11 @@ func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMess switch p := parsed.(type) { case *payload.AddressedCall: - return b.verifyOffchainAddressedCall(p) + return v.verifyOffchainAddressedCall(p) case *payload.Hash: - return b.verifyBlockMessage(ctx, p) + return v.verifyBlockMessage(ctx, p) default: - b.messageParseFail.Inc(1) + v.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, Message: fmt.Sprintf("unknown payload type: %T", p), @@ -264,11 +263,11 @@ func (b *backend) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMess // verifyBlockMessage returns nil if blockHashPayload contains the ID // of an accepted block indicating it should be signed by the VM. -func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { +func (v *verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { blockID := blockHashPayload.Hash - _, err := b.blockClient.GetBlock(ctx, blockID) + _, err := v.blockClient.GetBlock(ctx, blockID) if err != nil { - b.blockValidationFail.Inc(1) + v.blockValidationFail.Inc(1) return &common.AppError{ Code: VerifyErrCode, Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), @@ -279,11 +278,11 @@ func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payl } // verifyOffchainAddressedCall verifies the addressed call message -func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { +func (v *verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { // Further, parse the payload to see if it is a known type. parsed, err := message.Parse(addressedCall.Payload) if err != nil { - b.messageParseFail.Inc(1) + v.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, Message: "failed to parse addressed call message: " + err.Error(), @@ -299,12 +298,12 @@ func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCa switch p := parsed.(type) { case *message.ValidatorUptime: - if err := b.verifyUptimeMessage(p); err != nil { - b.uptimeValidationFail.Inc(1) + if err := v.verifyUptimeMessage(p); err != nil { + v.uptimeValidationFail.Inc(1) return err } default: - b.messageParseFail.Inc(1) + v.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, Message: fmt.Sprintf("unknown message type: %T", p), @@ -314,8 +313,8 @@ func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCa return nil } -func (b *backend) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { - currentUptime, _, err := b.uptimeTracker.GetUptime(uptimeMsg.ValidationID) +func (v *verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { + currentUptime, _, err := v.uptimeTracker.GetUptime(uptimeMsg.ValidationID) if err != nil { return &common.AppError{ Code: VerifyErrCode, diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 1e145620f77b..cadad8e8a79c 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -59,15 +59,15 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + components, err := New(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) ctx := context.Background() // Add testUnsignedMessage to the warp backend - require.NoError(t, backend.AddMessage(ctx, testUnsignedMessage)) + require.NoError(t, AddAndSign(ctx, components.DB, components.Signer, testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := signer.Sign(ctx, testUnsignedMessage) + signature, err := components.Signer.Sign(ctx, testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -82,10 +82,10 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - _, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + components, err := New(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) - _, err = signer.Sign(context.Background(), testUnsignedMessage) + _, err = components.Signer.Sign(context.Background(), testUnsignedMessage) require.ErrorIs(t, err, ErrVerifyWarpMessage) } @@ -100,7 +100,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - _, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, blockStore, nil, db, messageSignatureCache, nil) + components, err := New(networkID, sourceChainID, warpSigner, blockStore, nil, db, messageSignatureCache, nil) require.NoError(err) ctx := context.Background() @@ -112,16 +112,16 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) // Callers construct the block message and use Signer.Sign - signature, err := signer.Sign(ctx, unsignedMessage) + signature, err := components.Signer.Sign(ctx, unsignedMessage) require.NoError(err) require.Equal(expectedSig, signature) - // Test that an unknown block can still be signed by Signer (verification is caller's responsibility) + // Test that an unknown block fails verification unknownBlockHashPayload, err := payload.NewHash(ids.GenerateTestID()) require.NoError(err) unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) require.NoError(err) - _, err = signer.Sign(ctx, unknownUnsignedMessage) + _, err = components.Signer.Sign(ctx, unknownUnsignedMessage) require.ErrorIs(err, ErrVerifyWarpMessage) } @@ -134,15 +134,15 @@ func TestZeroSizedCache(t *testing.T) { // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + components, err := New(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) require.NoError(t, err) ctx := context.Background() // Add testUnsignedMessage to the warp backend - require.NoError(t, backend.AddMessage(ctx, testUnsignedMessage)) + require.NoError(t, AddAndSign(ctx, components.DB, components.Signer, testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := signer.Sign(ctx, testUnsignedMessage) + signature, err := components.Signer.Sign(ctx, testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -153,7 +153,7 @@ func TestZeroSizedCache(t *testing.T) { func TestOffChainMessages(t *testing.T) { type test struct { offchainMessages [][]byte - check func(require *require.Assertions, b Backend, s *Signer) + check func(require *require.Assertions, c *Components) err error } sk, err := localsigner.New() @@ -166,12 +166,12 @@ func TestOffChainMessages(t *testing.T) { offchainMessages: [][]byte{ testUnsignedMessage.Bytes(), }, - check: func(require *require.Assertions, b Backend, s *Signer) { - msg, err := b.GetMessage(testUnsignedMessage.ID()) + check: func(require *require.Assertions, c *Components) { + msg, err := c.DB.Get(testUnsignedMessage.ID()) require.NoError(err) require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - signature, err := s.Sign(context.Background(), testUnsignedMessage) + signature, err := c.Signer.Sign(context.Background(), testUnsignedMessage) require.NoError(err) expectedSignatureBytes, err := warpSigner.Sign(msg) require.NoError(err) @@ -179,11 +179,11 @@ func TestOffChainMessages(t *testing.T) { }, }, "unknown message": { - check: func(require *require.Assertions, b Backend, s *Signer) { - _, err := b.GetMessage(testUnsignedMessage.ID()) + check: func(require *require.Assertions, c *Components) { + _, err := c.DB.Get(testUnsignedMessage.ID()) require.ErrorIs(err, database.ErrNotFound) - _, err = s.Sign(context.Background(), testUnsignedMessage) + _, err = c.Signer.Sign(context.Background(), testUnsignedMessage) require.ErrorIs(err, ErrVerifyWarpMessage) }, }, @@ -197,10 +197,10 @@ func TestOffChainMessages(t *testing.T) { db := memdb.New() messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, signer, _, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, test.offchainMessages) + components, err := New(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, test.offchainMessages) require.ErrorIs(err, test.err) if test.check != nil { - test.check(require, backend, signer) + test.check(require, components) } }) } @@ -220,46 +220,46 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) tests := map[string]struct { - setup func(backend Backend) (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, b *backend) + setup func(components *Components) (request []byte, expectedResponse []byte) + verifyStats func(t *testing.T, v *verifier) err *common.AppError }{ "known message": { - setup: func(backend Backend) (request []byte, expectedResponse []byte) { + setup: func(components *Components) (request []byte, expectedResponse []byte) { knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) require.NoError(t, err) signature, err := snowCtx.WarpSigner.Sign(msg) require.NoError(t, err) - require.NoError(t, backend.AddMessage(context.Background(), msg)) + require.NoError(t, AddAndSign(context.Background(), components.DB, components.Signer, msg)) return msg.Bytes(), signature }, - verifyStats: func(t *testing.T, b *backend) { - require.Zero(t, b.messageParseFail.Snapshot().Count()) - require.Zero(t, b.blockValidationFail.Snapshot().Count()) + verifyStats: func(t *testing.T, v *verifier) { + require.Zero(t, v.messageParseFail.Snapshot().Count()) + require.Zero(t, v.blockValidationFail.Snapshot().Count()) }, }, "offchain message": { - setup: func(_ Backend) (request []byte, expectedResponse []byte) { + setup: func(_ *Components) (request []byte, expectedResponse []byte) { return offchainMessage.Bytes(), offchainSignature }, - verifyStats: func(t *testing.T, b *backend) { - require.Zero(t, b.messageParseFail.Snapshot().Count()) - require.Zero(t, b.blockValidationFail.Snapshot().Count()) + verifyStats: func(t *testing.T, v *verifier) { + require.Zero(t, v.messageParseFail.Snapshot().Count()) + require.Zero(t, v.blockValidationFail.Snapshot().Count()) }, }, "unknown message": { - setup: func(_ Backend) (request []byte, expectedResponse []byte) { + setup: func(_ *Components) (request []byte, expectedResponse []byte) { unknownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("unknown message")) require.NoError(t, err) unknownMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) require.NoError(t, err) return unknownMessage.Bytes(), nil }, - verifyStats: func(t *testing.T, b *backend) { - require.Equal(t, int64(1), b.messageParseFail.Snapshot().Count()) - require.Zero(t, b.blockValidationFail.Snapshot().Count()) + verifyStats: func(t *testing.T, v *verifier) { + require.Equal(t, int64(1), v.messageParseFail.Snapshot().Count()) + require.Zero(t, v.blockValidationFail.Snapshot().Count()) }, err: &common.AppError{Code: ParseErrCode}, }, @@ -279,16 +279,16 @@ func TestAddressedCallSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - warpBackend, signer, handler, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockStore, nil, database, sigCache, [][]byte{offchainMessage.Bytes()}) + components, err := New(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockStore, nil, database, sigCache, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) - requestBytes, expectedResponse := test.setup(warpBackend) + requestBytes, expectedResponse := test.setup(components) protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + responseBytes, appErr := components.Handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, warpBackend.(*backend)) + test.verifyStats(t, components.Signer.verifier.(*verifier)) // If the expected response is empty, assert that the handler returns an empty response and return early. if len(expectedResponse) == 0 { @@ -297,9 +297,9 @@ func TestAddressedCallSignatures(t *testing.T) { } // check cache is populated if withCache { - require.NotZero(t, signer.signatureCache.Len()) + require.NotZero(t, components.Signer.signatureCache.Len()) } else { - require.Zero(t, signer.signatureCache.Len()) + require.Zero(t, components.Signer.signatureCache.Len()) } response := &sdk.SignatureResponse{} require.NoError(t, proto.Unmarshal(responseBytes, response)) @@ -336,7 +336,7 @@ func TestBlockSignatures(t *testing.T) { tests := map[string]struct { setup func() (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, b *backend) + verifyStats func(t *testing.T, v *verifier) err *common.AppError }{ "known block": { @@ -349,9 +349,9 @@ func TestBlockSignatures(t *testing.T) { require.NoError(t, err) return toMessageBytes(knownBlkID), signature }, - verifyStats: func(t *testing.T, b *backend) { - require.Zero(t, b.blockValidationFail.Snapshot().Count()) - require.Zero(t, b.messageParseFail.Snapshot().Count()) + verifyStats: func(t *testing.T, v *verifier) { + require.Zero(t, v.blockValidationFail.Snapshot().Count()) + require.Zero(t, v.messageParseFail.Snapshot().Count()) }, }, "unknown block": { @@ -359,9 +359,9 @@ func TestBlockSignatures(t *testing.T) { unknownBlockID := ids.GenerateTestID() return toMessageBytes(unknownBlockID), nil }, - verifyStats: func(t *testing.T, b *backend) { - require.Equal(t, int64(1), b.blockValidationFail.Snapshot().Count()) - require.Zero(t, b.messageParseFail.Snapshot().Count()) + verifyStats: func(t *testing.T, v *verifier) { + require.Equal(t, int64(1), v.blockValidationFail.Snapshot().Count()) + require.Zero(t, v.messageParseFail.Snapshot().Count()) }, err: &common.AppError{Code: VerifyErrCode}, }, @@ -381,7 +381,7 @@ func TestBlockSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - warpBackend, signer, handler, err := NewBackend( + components, err := New( snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, @@ -397,10 +397,10 @@ func TestBlockSignatures(t *testing.T) { protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + responseBytes, appErr := components.Handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, warpBackend.(*backend)) + test.verifyStats(t, components.Signer.verifier.(*verifier)) // If the expected response is empty, require that the handler returns an empty response and return early. if len(expectedResponse) == 0 { @@ -409,9 +409,9 @@ func TestBlockSignatures(t *testing.T) { } // check cache is populated if withCache { - require.NotZero(t, signer.signatureCache.Len()) + require.NotZero(t, components.Signer.signatureCache.Len()) } else { - require.Zero(t, signer.signatureCache.Len()) + require.Zero(t, components.Signer.signatureCache.Len()) } var response sdk.SignatureResponse err = proto.Unmarshal(responseBytes, &response) @@ -484,7 +484,7 @@ func TestUptimeSignatures(t *testing.T) { require.NoError(t, uptimeTracker.Sync(t.Context())) - _, _, handler, err := NewBackend( + components, err := New( snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, @@ -495,6 +495,7 @@ func TestUptimeSignatures(t *testing.T) { nil, ) require.NoError(t, err) + handler := components.Handler // sourceAddress nonZero protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index f03b67bae9af..c0b0b1e67070 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -24,15 +24,15 @@ var errNoValidators = errors.New("cannot aggregate signatures from subnet with n // API introduces snowman specific functionality to the evm type API struct { chainContext *snow.Context - backend Backend + db *DB signer *Signer blockClient BlockStore signatureAggregator *acp118.SignatureAggregator } -func NewAPI(chainCtx *snow.Context, backend Backend, signer *Signer, blockClient BlockStore, signatureAggregator *acp118.SignatureAggregator) *API { +func NewAPI(chainCtx *snow.Context, db *DB, signer *Signer, blockClient BlockStore, signatureAggregator *acp118.SignatureAggregator) *API { return &API{ - backend: backend, + db: db, signer: signer, blockClient: blockClient, chainContext: chainCtx, @@ -42,7 +42,7 @@ func NewAPI(chainCtx *snow.Context, backend Backend, signer *Signer, blockClient // GetMessage returns the Warp message associated with a messageID. func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { - message, err := a.backend.GetMessage(messageID) + message, err := a.db.Get(messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) } @@ -51,7 +51,7 @@ func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, er // GetMessageSignature returns the BLS signature associated with a messageID. func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - unsignedMessage, err := a.backend.GetMessage(messageID) + unsignedMessage, err := a.db.Get(messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) } @@ -95,7 +95,7 @@ func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.By // GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { - unsignedMessage, err := a.backend.GetMessage(messageID) + unsignedMessage, err := a.db.Get(messageID) if err != nil { return nil, err } From e2b157a91e909f2a496194f59a8e89247071fa82 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 16:46:56 -0500 Subject: [PATCH 16/53] Compartmentalize warp components --- vms/evm/warp/backend.go | 119 ++++++++++++++++++----------------- vms/evm/warp/backend_test.go | 119 ++++++++++++++++++----------------- 2 files changed, 123 insertions(+), 115 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 58cda5d99876..7879189bc1a1 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -55,6 +55,28 @@ type DB struct { offchainAddressedCallMsgs map[ids.ID]*warp.UnsignedMessage } +// NewDB creates a new warp message database. +func NewDB( + networkID uint32, + sourceChainID ids.ID, + db database.Database, + offchainMessages [][]byte, +) (*DB, error) { + messageDB := &DB{ + networkID: networkID, + sourceChainID: sourceChainID, + db: db, + messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), + offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), + } + + if err := initOffChainMessages(messageDB, networkID, sourceChainID, offchainMessages); err != nil { + return nil, err + } + + return messageDB, nil +} + // Add stores a warp message in the database and cache. func (d *DB) Add(unsignedMsg *warp.UnsignedMessage) error { msgID := unsignedMsg.ID() @@ -100,6 +122,19 @@ type Signer struct { signatureCache cache.Cacher[ids.ID, []byte] } +// NewSigner creates a new warp message signer. +func NewSigner( + warpSigner warp.Signer, + verifier acp118.Verifier, + signatureCache cache.Cacher[ids.ID, []byte], +) *Signer { + return &Signer{ + warpSigner: warpSigner, + verifier: verifier, + signatureCache: signatureCache, + } +} + // Sign verifies the warp message, signs it, and caches the signature. func (s *Signer) Sign(ctx context.Context, msg *warp.UnsignedMessage) ([]byte, error) { // Check cache first @@ -138,13 +173,6 @@ func NewHandler( } } -// Components bundles the warp message handling components. -type Components struct { - DB *DB - Signer *Signer - Handler p2p.Handler -} - // verifier implements acp118.Verifier and validates whether a warp message should be signed. type verifier struct { db *DB @@ -160,31 +188,18 @@ type verifier struct { uptimeValidationFail metrics.Counter } -// New creates warp backend components and initializes the signature cache and message tracking database. -func New( - networkID uint32, - sourceChainID ids.ID, - warpSigner warp.Signer, +var _ acp118.Verifier = (*verifier)(nil) + +// NewVerifier creates a new warp message verifier. +func NewVerifier( + db *DB, blockClient BlockStore, uptimeTracker *uptimetracker.UptimeTracker, - db database.Database, - signatureCache cache.Cacher[ids.ID, []byte], - offchainMessages [][]byte, -) (*Components, error) { - messageDB := &DB{ - networkID: networkID, - sourceChainID: sourceChainID, - db: db, - messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), - offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), - } - - if err := initOffChainMessages(messageDB, networkID, sourceChainID, offchainMessages); err != nil { - return nil, err - } - - v := &verifier{ - db: messageDB, + networkID uint32, + sourceChainID ids.ID, +) acp118.Verifier { + return &verifier{ + db: db, blockClient: blockClient, uptimeTracker: uptimeTracker, networkID: networkID, @@ -194,35 +209,6 @@ func New( blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), uptimeValidationFail: metrics.NewRegisteredCounter("warp_backend_uptime_validation_fail", nil), } - - signer := &Signer{ - warpSigner: warpSigner, - verifier: v, - signatureCache: signatureCache, - } - - handler := NewHandler(signatureCache, v, warpSigner) - - return &Components{ - DB: messageDB, - Signer: signer, - Handler: handler, - }, nil -} - -// AddAndSign adds a warp message to the database and signs it. -// This is the typical entry point when a message is created on-chain (e.g., via the warp precompile). -func AddAndSign(ctx context.Context, db *DB, signer *Signer, unsignedMessage *warp.UnsignedMessage) error { - if err := db.Add(unsignedMessage); err != nil { - return err - } - - // Fill the signature cache now so subsequent requests can serve the - // signature without repeating verification or signing work. - if _, err := signer.Sign(ctx, unsignedMessage); err != nil { - return err - } - return nil } // Verify implements acp118.Verifier and validates whether a warp message should be signed. @@ -334,6 +320,21 @@ func (v *verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *comm return nil } +// AddAndSign adds a warp message to the database and signs it. +// This is the typical entry point when a message is created on-chain (e.g., via the warp precompile). +func AddAndSign(ctx context.Context, db *DB, signer *Signer, unsignedMessage *warp.UnsignedMessage) error { + if err := db.Add(unsignedMessage); err != nil { + return err + } + + // Fill the signature cache now so subsequent requests can serve the + // signature without repeating verification or signing work. + if _, err := signer.Sign(ctx, unsignedMessage); err != nil { + return err + } + return nil +} + func initOffChainMessages(db *DB, networkID uint32, sourceChainID ids.ID, offchainMessages [][]byte) error { for i, offchainMsg := range offchainMessages { unsignedMsg, err := warp.ParseUnsignedMessage(offchainMsg) diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index cadad8e8a79c..3e1cffba4291 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -59,15 +59,19 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - components, err := New(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + + messageDB, err := NewDB(networkID, sourceChainID, db, nil) require.NoError(t, err) + verifier := NewVerifier(messageDB, nil, nil, networkID, sourceChainID) + signer := NewSigner(warpSigner, verifier, messageSignatureCache) + ctx := context.Background() // Add testUnsignedMessage to the warp backend - require.NoError(t, AddAndSign(ctx, components.DB, components.Signer, testUnsignedMessage)) + require.NoError(t, AddAndSign(ctx, messageDB, signer, testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := components.Signer.Sign(ctx, testUnsignedMessage) + signature, err := signer.Sign(ctx, testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -82,10 +86,13 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - components, err := New(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + + messageDB, err := NewDB(networkID, sourceChainID, db, nil) require.NoError(t, err) + verifier := NewVerifier(messageDB, nil, nil, networkID, sourceChainID) + signer := NewSigner(warpSigner, verifier, messageSignatureCache) - _, err = components.Signer.Sign(context.Background(), testUnsignedMessage) + _, err = signer.Sign(context.Background(), testUnsignedMessage) require.ErrorIs(t, err, ErrVerifyWarpMessage) } @@ -100,8 +107,12 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - components, err := New(networkID, sourceChainID, warpSigner, blockStore, nil, db, messageSignatureCache, nil) + + messageDB, err := NewDB(networkID, sourceChainID, db, nil) require.NoError(err) + verifier := NewVerifier(messageDB, blockStore, nil, networkID, sourceChainID) + signer := NewSigner(warpSigner, verifier, messageSignatureCache) + ctx := context.Background() blockHashPayload, err := payload.NewHash(blkID) @@ -112,7 +123,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) // Callers construct the block message and use Signer.Sign - signature, err := components.Signer.Sign(ctx, unsignedMessage) + signature, err := signer.Sign(ctx, unsignedMessage) require.NoError(err) require.Equal(expectedSig, signature) @@ -121,7 +132,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) require.NoError(err) - _, err = components.Signer.Sign(ctx, unknownUnsignedMessage) + _, err = signer.Sign(ctx, unknownUnsignedMessage) require.ErrorIs(err, ErrVerifyWarpMessage) } @@ -134,15 +145,19 @@ func TestZeroSizedCache(t *testing.T) { // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - components, err := New(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) + + messageDB, err := NewDB(networkID, sourceChainID, db, nil) require.NoError(t, err) + verifier := NewVerifier(messageDB, nil, nil, networkID, sourceChainID) + signer := NewSigner(warpSigner, verifier, messageSignatureCache) + ctx := context.Background() // Add testUnsignedMessage to the warp backend - require.NoError(t, AddAndSign(ctx, components.DB, components.Signer, testUnsignedMessage)) + require.NoError(t, AddAndSign(ctx, messageDB, signer, testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := components.Signer.Sign(ctx, testUnsignedMessage) + signature, err := signer.Sign(ctx, testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -153,7 +168,7 @@ func TestZeroSizedCache(t *testing.T) { func TestOffChainMessages(t *testing.T) { type test struct { offchainMessages [][]byte - check func(require *require.Assertions, c *Components) + check func(require *require.Assertions, db *DB, signer *Signer) err error } sk, err := localsigner.New() @@ -166,12 +181,12 @@ func TestOffChainMessages(t *testing.T) { offchainMessages: [][]byte{ testUnsignedMessage.Bytes(), }, - check: func(require *require.Assertions, c *Components) { - msg, err := c.DB.Get(testUnsignedMessage.ID()) + check: func(require *require.Assertions, db *DB, signer *Signer) { + msg, err := db.Get(testUnsignedMessage.ID()) require.NoError(err) require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - signature, err := c.Signer.Sign(context.Background(), testUnsignedMessage) + signature, err := signer.Sign(context.Background(), testUnsignedMessage) require.NoError(err) expectedSignatureBytes, err := warpSigner.Sign(msg) require.NoError(err) @@ -179,11 +194,11 @@ func TestOffChainMessages(t *testing.T) { }, }, "unknown message": { - check: func(require *require.Assertions, c *Components) { - _, err := c.DB.Get(testUnsignedMessage.ID()) + check: func(require *require.Assertions, db *DB, signer *Signer) { + _, err := db.Get(testUnsignedMessage.ID()) require.ErrorIs(err, database.ErrNotFound) - _, err = c.Signer.Sign(context.Background(), testUnsignedMessage) + _, err = signer.Sign(context.Background(), testUnsignedMessage) require.ErrorIs(err, ErrVerifyWarpMessage) }, }, @@ -197,10 +212,12 @@ func TestOffChainMessages(t *testing.T) { db := memdb.New() messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - components, err := New(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, test.offchainMessages) + messageDB, err := NewDB(networkID, sourceChainID, db, test.offchainMessages) require.ErrorIs(err, test.err) if test.check != nil { - test.check(require, components) + verifier := NewVerifier(messageDB, nil, nil, networkID, sourceChainID) + signer := NewSigner(warpSigner, verifier, messageSignatureCache) + test.check(require, messageDB, signer) } }) } @@ -220,19 +237,19 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) tests := map[string]struct { - setup func(components *Components) (request []byte, expectedResponse []byte) + setup func(db *DB, signer *Signer) (request []byte, expectedResponse []byte) verifyStats func(t *testing.T, v *verifier) err *common.AppError }{ "known message": { - setup: func(components *Components) (request []byte, expectedResponse []byte) { + setup: func(db *DB, signer *Signer) (request []byte, expectedResponse []byte) { knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) require.NoError(t, err) signature, err := snowCtx.WarpSigner.Sign(msg) require.NoError(t, err) - require.NoError(t, AddAndSign(context.Background(), components.DB, components.Signer, msg)) + require.NoError(t, AddAndSign(context.Background(), db, signer, msg)) return msg.Bytes(), signature }, verifyStats: func(t *testing.T, v *verifier) { @@ -241,7 +258,7 @@ func TestAddressedCallSignatures(t *testing.T) { }, }, "offchain message": { - setup: func(_ *Components) (request []byte, expectedResponse []byte) { + setup: func(_ *DB, _ *Signer) (request []byte, expectedResponse []byte) { return offchainMessage.Bytes(), offchainSignature }, verifyStats: func(t *testing.T, v *verifier) { @@ -250,7 +267,7 @@ func TestAddressedCallSignatures(t *testing.T) { }, }, "unknown message": { - setup: func(_ *Components) (request []byte, expectedResponse []byte) { + setup: func(_ *DB, _ *Signer) (request []byte, expectedResponse []byte) { unknownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("unknown message")) require.NoError(t, err) unknownMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) @@ -279,16 +296,19 @@ func TestAddressedCallSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - components, err := New(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockStore, nil, database, sigCache, [][]byte{offchainMessage.Bytes()}) + db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) + v := NewVerifier(db, warptest.EmptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) + signer := NewSigner(snowCtx.WarpSigner, v, sigCache) + handler := NewHandler(sigCache, v, snowCtx.WarpSigner) - requestBytes, expectedResponse := test.setup(components) + requestBytes, expectedResponse := test.setup(db, signer) protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) - responseBytes, appErr := components.Handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, components.Signer.verifier.(*verifier)) + test.verifyStats(t, v.(*verifier)) // If the expected response is empty, assert that the handler returns an empty response and return early. if len(expectedResponse) == 0 { @@ -297,9 +317,9 @@ func TestAddressedCallSignatures(t *testing.T) { } // check cache is populated if withCache { - require.NotZero(t, components.Signer.signatureCache.Len()) + require.NotZero(t, signer.signatureCache.Len()) } else { - require.Zero(t, components.Signer.signatureCache.Len()) + require.Zero(t, signer.signatureCache.Len()) } response := &sdk.SignatureResponse{} require.NoError(t, proto.Unmarshal(responseBytes, response)) @@ -381,26 +401,20 @@ func TestBlockSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - components, err := New( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - blockStore, - nil, - database, - sigCache, - nil, - ) + db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, nil) require.NoError(t, err) + v := NewVerifier(db, blockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) + signer := NewSigner(snowCtx.WarpSigner, v, sigCache) + handler := NewHandler(sigCache, v, snowCtx.WarpSigner) requestBytes, expectedResponse := test.setup() protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) - responseBytes, appErr := components.Handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, components.Signer.verifier.(*verifier)) + test.verifyStats(t, v.(*verifier)) // If the expected response is empty, require that the handler returns an empty response and return early. if len(expectedResponse) == 0 { @@ -409,9 +423,9 @@ func TestBlockSignatures(t *testing.T) { } // check cache is populated if withCache { - require.NotZero(t, components.Signer.signatureCache.Len()) + require.NotZero(t, signer.signatureCache.Len()) } else { - require.Zero(t, components.Signer.signatureCache.Len()) + require.Zero(t, signer.signatureCache.Len()) } var response sdk.SignatureResponse err = proto.Unmarshal(responseBytes, &response) @@ -484,18 +498,11 @@ func TestUptimeSignatures(t *testing.T) { require.NoError(t, uptimeTracker.Sync(t.Context())) - components, err := New( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - warptest.EmptyBlockStore, - uptimeTracker, - database, - sigCache, - nil, - ) + db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, nil) + require.NoError(t, err) + verifier := NewVerifier(db, warptest.EmptyBlockStore, uptimeTracker, snowCtx.NetworkID, snowCtx.ChainID) + handler := NewHandler(sigCache, verifier, snowCtx.WarpSigner) require.NoError(t, err) - handler := components.Handler // sourceAddress nonZero protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) From 67ff47f524757da0f516e249d042bdfda0ae9906 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 5 Nov 2025 16:47:40 -0500 Subject: [PATCH 17/53] remove duplicated check --- vms/evm/warp/backend.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 7879189bc1a1..3eac5e7781ea 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -188,8 +188,6 @@ type verifier struct { uptimeValidationFail metrics.Counter } -var _ acp118.Verifier = (*verifier)(nil) - // NewVerifier creates a new warp message verifier. func NewVerifier( db *DB, From 7b0c9088aad29340d542fd0e07c5d0e0b487aaa1 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 6 Nov 2025 10:42:49 -0500 Subject: [PATCH 18/53] remove handler --- vms/evm/warp/backend.go | 19 ------------------- vms/evm/warp/backend_test.go | 7 ++++--- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 3eac5e7781ea..9b52fa710688 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -15,7 +15,6 @@ import ( "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/common" @@ -31,7 +30,6 @@ const ( ) var ( - _ p2p.Handler = (*Handler)(nil) _ acp118.Verifier = (*verifier)(nil) messageCacheSize = 500 @@ -156,23 +154,6 @@ func (s *Signer) Sign(ctx context.Context, msg *warp.UnsignedMessage) ([]byte, e return sig, nil } -// Handler implements p2p.Handler and handles warp signature requests. -// It hides the acp118.Verifier implementation as an implementation detail. -type Handler struct { - *acp118.Handler -} - -// NewHandler creates a new p2p warp signature request handler. -func NewHandler( - signatureCache cache.Cacher[ids.ID, []byte], - verifier acp118.Verifier, - signer warp.Signer, -) p2p.Handler { - return &Handler{ - Handler: acp118.NewCachedHandler(signatureCache, verifier, signer), - } -} - // verifier implements acp118.Verifier and validates whether a warp message should be signed. type verifier struct { db *DB diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 3e1cffba4291..9c8db36dcdb0 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -17,6 +17,7 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/snowtest" @@ -300,7 +301,7 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) v := NewVerifier(db, warptest.EmptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) signer := NewSigner(snowCtx.WarpSigner, v, sigCache) - handler := NewHandler(sigCache, v, snowCtx.WarpSigner) + handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) requestBytes, expectedResponse := test.setup(db, signer) protoMsg := &sdk.SignatureRequest{Message: requestBytes} @@ -405,7 +406,7 @@ func TestBlockSignatures(t *testing.T) { require.NoError(t, err) v := NewVerifier(db, blockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) signer := NewSigner(snowCtx.WarpSigner, v, sigCache) - handler := NewHandler(sigCache, v, snowCtx.WarpSigner) + handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) requestBytes, expectedResponse := test.setup() protoMsg := &sdk.SignatureRequest{Message: requestBytes} @@ -501,7 +502,7 @@ func TestUptimeSignatures(t *testing.T) { db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, nil) require.NoError(t, err) verifier := NewVerifier(db, warptest.EmptyBlockStore, uptimeTracker, snowCtx.NetworkID, snowCtx.ChainID) - handler := NewHandler(sigCache, verifier, snowCtx.WarpSigner) + handler := acp118.NewCachedHandler(sigCache, verifier, snowCtx.WarpSigner) require.NoError(t, err) // sourceAddress nonZero From d06652e161916e717f9eaf97d9b5cbce7f7b813f Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 6 Nov 2025 10:51:52 -0500 Subject: [PATCH 19/53] Export verifier and remove AddAndSign --- vms/evm/warp/backend.go | 33 +++++++++------------------------ vms/evm/warp/backend_test.go | 24 ++++++++++++------------ 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 9b52fa710688..70b8455a63ed 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -30,7 +30,7 @@ const ( ) var ( - _ acp118.Verifier = (*verifier)(nil) + _ acp118.Verifier = (*Verifier)(nil) messageCacheSize = 500 @@ -154,8 +154,8 @@ func (s *Signer) Sign(ctx context.Context, msg *warp.UnsignedMessage) ([]byte, e return sig, nil } -// verifier implements acp118.Verifier and validates whether a warp message should be signed. -type verifier struct { +// Verifier implements acp118.Verifier and validates whether a warp message should be signed. +type Verifier struct { db *DB blockClient BlockStore uptimeTracker *uptimetracker.UptimeTracker @@ -176,8 +176,8 @@ func NewVerifier( uptimeTracker *uptimetracker.UptimeTracker, networkID uint32, sourceChainID ids.ID, -) acp118.Verifier { - return &verifier{ +) *Verifier { + return &Verifier{ db: db, blockClient: blockClient, uptimeTracker: uptimeTracker, @@ -191,7 +191,7 @@ func NewVerifier( } // Verify implements acp118.Verifier and validates whether a warp message should be signed. -func (v *verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) *common.AppError { +func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) *common.AppError { messageID := unsignedMessage.ID() // Known on-chain messages should be signed if _, err := v.db.Get(messageID); err == nil { @@ -228,7 +228,7 @@ func (v *verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes // verifyBlockMessage returns nil if blockHashPayload contains the ID // of an accepted block indicating it should be signed by the VM. -func (v *verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { +func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { blockID := blockHashPayload.Hash _, err := v.blockClient.GetBlock(ctx, blockID) if err != nil { @@ -243,7 +243,7 @@ func (v *verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *pay } // verifyOffchainAddressedCall verifies the addressed call message -func (v *verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { +func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { // Further, parse the payload to see if it is a known type. parsed, err := message.Parse(addressedCall.Payload) if err != nil { @@ -278,7 +278,7 @@ func (v *verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC return nil } -func (v *verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { +func (v *Verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { currentUptime, _, err := v.uptimeTracker.GetUptime(uptimeMsg.ValidationID) if err != nil { return &common.AppError{ @@ -299,21 +299,6 @@ func (v *verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *comm return nil } -// AddAndSign adds a warp message to the database and signs it. -// This is the typical entry point when a message is created on-chain (e.g., via the warp precompile). -func AddAndSign(ctx context.Context, db *DB, signer *Signer, unsignedMessage *warp.UnsignedMessage) error { - if err := db.Add(unsignedMessage); err != nil { - return err - } - - // Fill the signature cache now so subsequent requests can serve the - // signature without repeating verification or signing work. - if _, err := signer.Sign(ctx, unsignedMessage); err != nil { - return err - } - return nil -} - func initOffChainMessages(db *DB, networkID uint32, sourceChainID ids.ID, offchainMessages [][]byte) error { for i, offchainMsg := range offchainMessages { unsignedMsg, err := warp.ParseUnsignedMessage(offchainMsg) diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 9c8db36dcdb0..4ea735eac406 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -69,7 +69,7 @@ func TestAddAndGetValidMessage(t *testing.T) { ctx := context.Background() // Add testUnsignedMessage to the warp backend - require.NoError(t, AddAndSign(ctx, messageDB, signer, testUnsignedMessage)) + require.NoError(t, messageDB.Add(testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. signature, err := signer.Sign(ctx, testUnsignedMessage) @@ -155,7 +155,7 @@ func TestZeroSizedCache(t *testing.T) { ctx := context.Background() // Add testUnsignedMessage to the warp backend - require.NoError(t, AddAndSign(ctx, messageDB, signer, testUnsignedMessage)) + require.NoError(t, messageDB.Add(testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. signature, err := signer.Sign(ctx, testUnsignedMessage) @@ -239,7 +239,7 @@ func TestAddressedCallSignatures(t *testing.T) { tests := map[string]struct { setup func(db *DB, signer *Signer) (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, v *verifier) + verifyStats func(t *testing.T, v *Verifier) err *common.AppError }{ "known message": { @@ -250,10 +250,10 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) signature, err := snowCtx.WarpSigner.Sign(msg) require.NoError(t, err) - require.NoError(t, AddAndSign(context.Background(), db, signer, msg)) + require.NoError(t, db.Add(msg)) return msg.Bytes(), signature }, - verifyStats: func(t *testing.T, v *verifier) { + verifyStats: func(t *testing.T, v *Verifier) { require.Zero(t, v.messageParseFail.Snapshot().Count()) require.Zero(t, v.blockValidationFail.Snapshot().Count()) }, @@ -262,7 +262,7 @@ func TestAddressedCallSignatures(t *testing.T) { setup: func(_ *DB, _ *Signer) (request []byte, expectedResponse []byte) { return offchainMessage.Bytes(), offchainSignature }, - verifyStats: func(t *testing.T, v *verifier) { + verifyStats: func(t *testing.T, v *Verifier) { require.Zero(t, v.messageParseFail.Snapshot().Count()) require.Zero(t, v.blockValidationFail.Snapshot().Count()) }, @@ -275,7 +275,7 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) return unknownMessage.Bytes(), nil }, - verifyStats: func(t *testing.T, v *verifier) { + verifyStats: func(t *testing.T, v *Verifier) { require.Equal(t, int64(1), v.messageParseFail.Snapshot().Count()) require.Zero(t, v.blockValidationFail.Snapshot().Count()) }, @@ -309,7 +309,7 @@ func TestAddressedCallSignatures(t *testing.T) { require.NoError(t, err) responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, v.(*verifier)) + test.verifyStats(t, v) // If the expected response is empty, assert that the handler returns an empty response and return early. if len(expectedResponse) == 0 { @@ -357,7 +357,7 @@ func TestBlockSignatures(t *testing.T) { tests := map[string]struct { setup func() (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, v *verifier) + verifyStats func(t *testing.T, v *Verifier) err *common.AppError }{ "known block": { @@ -370,7 +370,7 @@ func TestBlockSignatures(t *testing.T) { require.NoError(t, err) return toMessageBytes(knownBlkID), signature }, - verifyStats: func(t *testing.T, v *verifier) { + verifyStats: func(t *testing.T, v *Verifier) { require.Zero(t, v.blockValidationFail.Snapshot().Count()) require.Zero(t, v.messageParseFail.Snapshot().Count()) }, @@ -380,7 +380,7 @@ func TestBlockSignatures(t *testing.T) { unknownBlockID := ids.GenerateTestID() return toMessageBytes(unknownBlockID), nil }, - verifyStats: func(t *testing.T, v *verifier) { + verifyStats: func(t *testing.T, v *Verifier) { require.Equal(t, int64(1), v.blockValidationFail.Snapshot().Count()) require.Zero(t, v.messageParseFail.Snapshot().Count()) }, @@ -415,7 +415,7 @@ func TestBlockSignatures(t *testing.T) { responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, v.(*verifier)) + test.verifyStats(t, v) // If the expected response is empty, require that the handler returns an empty response and return early. if len(expectedResponse) == 0 { From 7d5e9c07421bb5a96b6a4215e1ee3c4cecd88536 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 6 Nov 2025 10:58:46 -0500 Subject: [PATCH 20/53] Lint --- vms/evm/warp/backend.go | 11 +---------- vms/evm/warp/backend_test.go | 38 +++++++++++++++--------------------- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 70b8455a63ed..a43466786da2 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -46,8 +46,6 @@ type BlockStore interface { // DB stores and retrieves warp messages. type DB struct { - networkID uint32 - sourceChainID ids.ID db database.Database messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] offchainAddressedCallMsgs map[ids.ID]*warp.UnsignedMessage @@ -61,8 +59,6 @@ func NewDB( offchainMessages [][]byte, ) (*DB, error) { messageDB := &DB{ - networkID: networkID, - sourceChainID: sourceChainID, db: db, messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), @@ -159,8 +155,6 @@ type Verifier struct { db *DB blockClient BlockStore uptimeTracker *uptimetracker.UptimeTracker - networkID uint32 - sourceChainID ids.ID // Metrics messageParseFail metrics.Counter @@ -174,15 +168,11 @@ func NewVerifier( db *DB, blockClient BlockStore, uptimeTracker *uptimetracker.UptimeTracker, - networkID uint32, - sourceChainID ids.ID, ) *Verifier { return &Verifier{ db: db, blockClient: blockClient, uptimeTracker: uptimeTracker, - networkID: networkID, - sourceChainID: sourceChainID, messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), addressedCallValidationFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_validation_fail", nil), blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), @@ -255,6 +245,7 @@ func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC } if len(addressedCall.SourceAddress) != 0 { + v.addressedCallValidationFail.Inc(1) return &common.AppError{ Code: VerifyErrCode, Message: "source address should be empty for offchain addressed messages", diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 4ea735eac406..098cd021f1d3 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -63,16 +63,14 @@ func TestAddAndGetValidMessage(t *testing.T) { messageDB, err := NewDB(networkID, sourceChainID, db, nil) require.NoError(t, err) - verifier := NewVerifier(messageDB, nil, nil, networkID, sourceChainID) + verifier := NewVerifier(messageDB, nil, nil) signer := NewSigner(warpSigner, verifier, messageSignatureCache) - ctx := context.Background() - // Add testUnsignedMessage to the warp backend require.NoError(t, messageDB.Add(testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := signer.Sign(ctx, testUnsignedMessage) + signature, err := signer.Sign(t.Context(), testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -90,10 +88,10 @@ func TestAddAndGetUnknownMessage(t *testing.T) { messageDB, err := NewDB(networkID, sourceChainID, db, nil) require.NoError(t, err) - verifier := NewVerifier(messageDB, nil, nil, networkID, sourceChainID) + verifier := NewVerifier(messageDB, nil, nil) signer := NewSigner(warpSigner, verifier, messageSignatureCache) - _, err = signer.Sign(context.Background(), testUnsignedMessage) + _, err = signer.Sign(t.Context(), testUnsignedMessage) require.ErrorIs(t, err, ErrVerifyWarpMessage) } @@ -111,11 +109,9 @@ func TestGetBlockSignature(t *testing.T) { messageDB, err := NewDB(networkID, sourceChainID, db, nil) require.NoError(err) - verifier := NewVerifier(messageDB, blockStore, nil, networkID, sourceChainID) + verifier := NewVerifier(messageDB, blockStore, nil) signer := NewSigner(warpSigner, verifier, messageSignatureCache) - ctx := context.Background() - blockHashPayload, err := payload.NewHash(blkID) require.NoError(err) unsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) @@ -124,7 +120,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) // Callers construct the block message and use Signer.Sign - signature, err := signer.Sign(ctx, unsignedMessage) + signature, err := signer.Sign(t.Context(), unsignedMessage) require.NoError(err) require.Equal(expectedSig, signature) @@ -133,7 +129,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) require.NoError(err) - _, err = signer.Sign(ctx, unknownUnsignedMessage) + _, err = signer.Sign(t.Context(), unknownUnsignedMessage) require.ErrorIs(err, ErrVerifyWarpMessage) } @@ -149,16 +145,14 @@ func TestZeroSizedCache(t *testing.T) { messageDB, err := NewDB(networkID, sourceChainID, db, nil) require.NoError(t, err) - verifier := NewVerifier(messageDB, nil, nil, networkID, sourceChainID) + verifier := NewVerifier(messageDB, nil, nil) signer := NewSigner(warpSigner, verifier, messageSignatureCache) - ctx := context.Background() - // Add testUnsignedMessage to the warp backend require.NoError(t, messageDB.Add(testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := signer.Sign(ctx, testUnsignedMessage) + signature, err := signer.Sign(t.Context(), testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -187,7 +181,7 @@ func TestOffChainMessages(t *testing.T) { require.NoError(err) require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - signature, err := signer.Sign(context.Background(), testUnsignedMessage) + signature, err := signer.Sign(t.Context(), testUnsignedMessage) require.NoError(err) expectedSignatureBytes, err := warpSigner.Sign(msg) require.NoError(err) @@ -199,7 +193,7 @@ func TestOffChainMessages(t *testing.T) { _, err := db.Get(testUnsignedMessage.ID()) require.ErrorIs(err, database.ErrNotFound) - _, err = signer.Sign(context.Background(), testUnsignedMessage) + _, err = signer.Sign(t.Context(), testUnsignedMessage) require.ErrorIs(err, ErrVerifyWarpMessage) }, }, @@ -216,7 +210,7 @@ func TestOffChainMessages(t *testing.T) { messageDB, err := NewDB(networkID, sourceChainID, db, test.offchainMessages) require.ErrorIs(err, test.err) if test.check != nil { - verifier := NewVerifier(messageDB, nil, nil, networkID, sourceChainID) + verifier := NewVerifier(messageDB, nil, nil) signer := NewSigner(warpSigner, verifier, messageSignatureCache) test.check(require, messageDB, signer) } @@ -243,7 +237,7 @@ func TestAddressedCallSignatures(t *testing.T) { err *common.AppError }{ "known message": { - setup: func(db *DB, signer *Signer) (request []byte, expectedResponse []byte) { + setup: func(db *DB, _ *Signer) (request []byte, expectedResponse []byte) { knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) @@ -299,7 +293,7 @@ func TestAddressedCallSignatures(t *testing.T) { } db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) - v := NewVerifier(db, warptest.EmptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) + v := NewVerifier(db, warptest.EmptyBlockStore, nil) signer := NewSigner(snowCtx.WarpSigner, v, sigCache) handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) @@ -404,7 +398,7 @@ func TestBlockSignatures(t *testing.T) { } db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, nil) require.NoError(t, err) - v := NewVerifier(db, blockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) + v := NewVerifier(db, blockStore, nil) signer := NewSigner(snowCtx.WarpSigner, v, sigCache) handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) @@ -501,7 +495,7 @@ func TestUptimeSignatures(t *testing.T) { db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, nil) require.NoError(t, err) - verifier := NewVerifier(db, warptest.EmptyBlockStore, uptimeTracker, snowCtx.NetworkID, snowCtx.ChainID) + verifier := NewVerifier(db, warptest.EmptyBlockStore, uptimeTracker) handler := acp118.NewCachedHandler(sigCache, verifier, snowCtx.WarpSigner) require.NoError(t, err) From 71135c8c75922ed5cecf1774b21c9119a89817e8 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 6 Nov 2025 11:27:23 -0500 Subject: [PATCH 21/53] Josh service refactor --- vms/evm/warp/backend.go | 102 ++++----------------- vms/evm/warp/backend_test.go | 170 +++++++++-------------------------- vms/evm/warp/service.go | 110 +++++++++++++++++++---- 3 files changed, 151 insertions(+), 231 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index a43466786da2..b1a0ff384bc6 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -11,8 +11,6 @@ import ( "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/metrics" - "github.com/ava-labs/avalanchego/cache" - "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -32,11 +30,8 @@ const ( var ( _ acp118.Verifier = (*Verifier)(nil) - messageCacheSize = 500 - - errParsingOffChainMessage = errors.New("failed to parse off-chain message") - ErrValidateBlock = errors.New("failed to validate block message") - ErrVerifyWarpMessage = errors.New("failed to verify warp message") + ErrValidateBlock = errors.New("failed to validate block message") + ErrVerifyWarpMessage = errors.New("failed to verify warp message") ) // BlockStore provides access to accepted blocks. @@ -44,31 +39,16 @@ type BlockStore interface { GetBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) } -// DB stores and retrieves warp messages. +// DB stores and retrieves warp messages from the underlying database. type DB struct { - db database.Database - messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] - offchainAddressedCallMsgs map[ids.ID]*warp.UnsignedMessage + db database.Database } // NewDB creates a new warp message database. -func NewDB( - networkID uint32, - sourceChainID ids.ID, - db database.Database, - offchainMessages [][]byte, -) (*DB, error) { - messageDB := &DB{ - db: db, - messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](messageCacheSize), - offchainAddressedCallMsgs: make(map[ids.ID]*warp.UnsignedMessage), - } - - if err := initOffChainMessages(messageDB, networkID, sourceChainID, offchainMessages); err != nil { - return nil, err +func NewDB(db database.Database) *DB { + return &DB{ + db: db, } - - return messageDB, nil } // Add stores a warp message in the database and cache. @@ -86,15 +66,8 @@ func (d *DB) Add(unsignedMsg *warp.UnsignedMessage) error { return nil } -// Get retrieves a warp message from cache, offchain messages, or database. +// Get retrieves a warp message from the database. func (d *DB) Get(msgID ids.ID) (*warp.UnsignedMessage, error) { - if msg, ok := d.messageCache.Get(msgID); ok { - return msg, nil - } - if msg, ok := d.offchainAddressedCallMsgs[msgID]; ok { - return msg, nil - } - unsignedMessageBytes, err := d.db.Get(msgID[:]) if err != nil { return nil, err @@ -104,49 +77,29 @@ func (d *DB) Get(msgID ids.ID) (*warp.UnsignedMessage, error) { if err != nil { return nil, fmt.Errorf("failed to parse unsigned message %s: %w", msgID.String(), err) } - d.messageCache.Put(msgID, unsignedMessage) return unsignedMessage, nil } -// Signer signs warp messages and caches the signatures. +// Signer signs warp messages. type Signer struct { - warpSigner warp.Signer - verifier acp118.Verifier - signatureCache cache.Cacher[ids.ID, []byte] + warpSigner warp.Signer } // NewSigner creates a new warp message signer. -func NewSigner( - warpSigner warp.Signer, - verifier acp118.Verifier, - signatureCache cache.Cacher[ids.ID, []byte], -) *Signer { +func NewSigner(warpSigner warp.Signer) *Signer { return &Signer{ - warpSigner: warpSigner, - verifier: verifier, - signatureCache: signatureCache, + warpSigner: warpSigner, } } -// Sign verifies the warp message, signs it, and caches the signature. -func (s *Signer) Sign(ctx context.Context, msg *warp.UnsignedMessage) ([]byte, error) { - // Check cache first - msgID := msg.ID() - if sig, ok := s.signatureCache.Get(msgID); ok { - return sig, nil - } - - if err := s.verifier.Verify(ctx, msg, nil); err != nil { - return nil, fmt.Errorf("%w: %w", ErrVerifyWarpMessage, err) - } - +// Sign signs a warp message. +// Callers are responsible for verification and caching. +func (s *Signer) Sign(msg *warp.UnsignedMessage) ([]byte, error) { sig, err := s.warpSigner.Sign(msg) if err != nil { return nil, fmt.Errorf("failed to sign warp message: %w", err) } - - s.signatureCache.Put(msgID, sig) return sig, nil } @@ -289,28 +242,3 @@ func (v *Verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *comm return nil } - -func initOffChainMessages(db *DB, networkID uint32, sourceChainID ids.ID, 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 != networkID { - return fmt.Errorf("%w at index %d", warp.ErrWrongNetworkID, i) - } - - if unsignedMsg.SourceChainID != 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) - } - db.offchainAddressedCallMsgs[unsignedMsg.ID()] = unsignedMsg - } - - return nil -} diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 098cd021f1d3..ca1feeed2a05 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -14,7 +14,6 @@ import ( "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -59,18 +58,15 @@ func TestAddAndGetValidMessage(t *testing.T) { sk, err := localsigner.New() require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - messageDB, err := NewDB(networkID, sourceChainID, db, nil) - require.NoError(t, err) - verifier := NewVerifier(messageDB, nil, nil) - signer := NewSigner(warpSigner, verifier, messageSignatureCache) + messageDB := NewDB(db) + signer := NewSigner(warpSigner) // Add testUnsignedMessage to the warp backend require.NoError(t, messageDB.Add(testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := signer.Sign(t.Context(), testUnsignedMessage) + signature, err := signer.Sign(testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -81,18 +77,12 @@ func TestAddAndGetValidMessage(t *testing.T) { func TestAddAndGetUnknownMessage(t *testing.T) { db := memdb.New() - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := warp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - - messageDB, err := NewDB(networkID, sourceChainID, db, nil) - require.NoError(t, err) + messageDB := NewDB(db) verifier := NewVerifier(messageDB, nil, nil) - signer := NewSigner(warpSigner, verifier, messageSignatureCache) - _, err = signer.Sign(t.Context(), testUnsignedMessage) - require.ErrorIs(t, err, ErrVerifyWarpMessage) + // Try to verify an unknown message - should fail + appErr := verifier.Verify(t.Context(), testUnsignedMessage, nil) + require.ErrorIs(t, appErr, &common.AppError{Code: ParseErrCode}) } func TestGetBlockSignature(t *testing.T) { @@ -105,12 +95,10 @@ func TestGetBlockSignature(t *testing.T) { sk, err := localsigner.New() require.NoError(err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - messageDB, err := NewDB(networkID, sourceChainID, db, nil) - require.NoError(err) + messageDB := NewDB(db) verifier := NewVerifier(messageDB, blockStore, nil) - signer := NewSigner(warpSigner, verifier, messageSignatureCache) + signer := NewSigner(warpSigner) blockHashPayload, err := payload.NewHash(blkID) require.NoError(err) @@ -119,8 +107,12 @@ func TestGetBlockSignature(t *testing.T) { expectedSig, err := warpSigner.Sign(unsignedMessage) require.NoError(err) - // Callers construct the block message and use Signer.Sign - signature, err := signer.Sign(t.Context(), unsignedMessage) + // Verify the block message + appErr := verifier.Verify(t.Context(), unsignedMessage, nil) + require.Nil(appErr) + + // Then sign it + signature, err := signer.Sign(unsignedMessage) require.NoError(err) require.Equal(expectedSig, signature) @@ -129,30 +121,30 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) require.NoError(err) - _, err = signer.Sign(t.Context(), unknownUnsignedMessage) - require.ErrorIs(err, ErrVerifyWarpMessage) + unknownAppErr := verifier.Verify(t.Context(), unknownUnsignedMessage, nil) + require.ErrorIs(unknownAppErr, &common.AppError{Code: VerifyErrCode}) } -func TestZeroSizedCache(t *testing.T) { +func TestVerifierKnownMessage(t *testing.T) { db := memdb.New() sk, err := localsigner.New() require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) - // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. - messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - - messageDB, err := NewDB(networkID, sourceChainID, db, nil) - require.NoError(t, err) + messageDB := NewDB(db) verifier := NewVerifier(messageDB, nil, nil) - signer := NewSigner(warpSigner, verifier, messageSignatureCache) + signer := NewSigner(warpSigner) - // Add testUnsignedMessage to the warp backend + // Add testUnsignedMessage to the database require.NoError(t, messageDB.Add(testUnsignedMessage)) - // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := signer.Sign(t.Context(), testUnsignedMessage) + // Known messages in the DB should pass verification + appErr := verifier.Verify(t.Context(), testUnsignedMessage, nil) + require.Nil(t, appErr) + + // And can be signed + signature, err := signer.Sign(testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -160,84 +152,19 @@ func TestZeroSizedCache(t *testing.T) { require.Equal(t, expectedSig, signature) } -func TestOffChainMessages(t *testing.T) { - type test struct { - offchainMessages [][]byte - check func(require *require.Assertions, db *DB, signer *Signer) - err error - } - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := warp.NewSigner(sk, networkID, sourceChainID) - - for name, test := range map[string]test{ - "no offchain messages": {}, - "single off-chain message": { - offchainMessages: [][]byte{ - testUnsignedMessage.Bytes(), - }, - check: func(require *require.Assertions, db *DB, signer *Signer) { - msg, err := db.Get(testUnsignedMessage.ID()) - require.NoError(err) - require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - - signature, err := signer.Sign(t.Context(), testUnsignedMessage) - require.NoError(err) - expectedSignatureBytes, err := warpSigner.Sign(msg) - require.NoError(err) - require.Equal(expectedSignatureBytes, signature) - }, - }, - "unknown message": { - check: func(require *require.Assertions, db *DB, signer *Signer) { - _, err := db.Get(testUnsignedMessage.ID()) - require.ErrorIs(err, database.ErrNotFound) - - _, err = signer.Sign(t.Context(), testUnsignedMessage) - require.ErrorIs(err, ErrVerifyWarpMessage) - }, - }, - "invalid message": { - offchainMessages: [][]byte{{1, 2, 3}}, - err: errParsingOffChainMessage, - }, - } { - t.Run(name, func(t *testing.T) { - require := require.New(t) - db := memdb.New() - - messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - messageDB, err := NewDB(networkID, sourceChainID, db, test.offchainMessages) - require.ErrorIs(err, test.err) - if test.check != nil { - verifier := NewVerifier(messageDB, nil, nil) - signer := NewSigner(warpSigner, verifier, messageSignatureCache) - test.check(require, messageDB, signer) - } - }) - } -} - -func TestAddressedCallSignatures(t *testing.T) { +func TestKnownMessageSignature(t *testing.T) { metricstest.WithMetrics(t) database := memdb.New() snowCtx := snowtest.Context(t, snowtest.CChainID) - offChainPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) - require.NoError(t, err) - offchainMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offChainPayload.Bytes()) - require.NoError(t, err) - offchainSignature, err := snowCtx.WarpSigner.Sign(offchainMessage) - require.NoError(t, err) - tests := map[string]struct { - setup func(db *DB, signer *Signer) (request []byte, expectedResponse []byte) + setup func(db *DB) (request []byte, expectedResponse []byte) verifyStats func(t *testing.T, v *Verifier) err *common.AppError }{ "known message": { - setup: func(db *DB, _ *Signer) (request []byte, expectedResponse []byte) { + setup: func(db *DB) (request []byte, expectedResponse []byte) { knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) @@ -252,17 +179,8 @@ func TestAddressedCallSignatures(t *testing.T) { require.Zero(t, v.blockValidationFail.Snapshot().Count()) }, }, - "offchain message": { - setup: func(_ *DB, _ *Signer) (request []byte, expectedResponse []byte) { - return offchainMessage.Bytes(), offchainSignature - }, - verifyStats: func(t *testing.T, v *Verifier) { - require.Zero(t, v.messageParseFail.Snapshot().Count()) - require.Zero(t, v.blockValidationFail.Snapshot().Count()) - }, - }, "unknown message": { - setup: func(_ *DB, _ *Signer) (request []byte, expectedResponse []byte) { + setup: func(_ *DB) (request []byte, expectedResponse []byte) { unknownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("unknown message")) require.NoError(t, err) unknownMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) @@ -291,13 +209,11 @@ func TestAddressedCallSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, [][]byte{offchainMessage.Bytes()}) - require.NoError(t, err) + db := NewDB(database) v := NewVerifier(db, warptest.EmptyBlockStore, nil) - signer := NewSigner(snowCtx.WarpSigner, v, sigCache) handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) - requestBytes, expectedResponse := test.setup(db, signer) + requestBytes, expectedResponse := test.setup(db) protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) @@ -310,11 +226,11 @@ func TestAddressedCallSignatures(t *testing.T) { require.Empty(t, responseBytes, "expected response to be empty") return } - // check cache is populated + // check cache is populated (handler's cache) if withCache { - require.NotZero(t, signer.signatureCache.Len()) + require.NotZero(t, sigCache.Len()) } else { - require.Zero(t, signer.signatureCache.Len()) + require.Zero(t, sigCache.Len()) } response := &sdk.SignatureResponse{} require.NoError(t, proto.Unmarshal(responseBytes, response)) @@ -396,10 +312,8 @@ func TestBlockSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, nil) - require.NoError(t, err) + db := NewDB(database) v := NewVerifier(db, blockStore, nil) - signer := NewSigner(snowCtx.WarpSigner, v, sigCache) handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) requestBytes, expectedResponse := test.setup() @@ -416,11 +330,11 @@ func TestBlockSignatures(t *testing.T) { require.Empty(t, responseBytes, "expected response to be empty") return } - // check cache is populated + // check cache is populated (handler's cache) if withCache { - require.NotZero(t, signer.signatureCache.Len()) + require.NotZero(t, sigCache.Len()) } else { - require.Zero(t, signer.signatureCache.Len()) + require.Zero(t, sigCache.Len()) } var response sdk.SignatureResponse err = proto.Unmarshal(responseBytes, &response) @@ -493,11 +407,9 @@ func TestUptimeSignatures(t *testing.T) { require.NoError(t, uptimeTracker.Sync(t.Context())) - db, err := NewDB(snowCtx.NetworkID, snowCtx.ChainID, database, nil) - require.NoError(t, err) + db := NewDB(database) verifier := NewVerifier(db, warptest.EmptyBlockStore, uptimeTracker) handler := acp118.NewCachedHandler(sigCache, verifier, snowCtx.WarpSigner) - require.NoError(t, err) // sourceAddress nonZero protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index c0b0b1e67070..e1ba045e084f 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -11,6 +11,8 @@ import ( "github.com/ava-labs/libevm/common/hexutil" "github.com/ava-labs/libevm/log" + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow" @@ -21,45 +23,113 @@ import ( var errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") -// API introduces snowman specific functionality to the evm +// API introduces snowman specific functionality to the evm. +// It provides caching and orchestration over the core warp primitives. type API struct { chainContext *snow.Context db *DB signer *Signer - blockClient BlockStore + verifier *Verifier signatureAggregator *acp118.SignatureAggregator + + // Caching + messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] + signatureCache cache.Cacher[ids.ID, []byte] + offchainMessages map[ids.ID]*warp.UnsignedMessage } -func NewAPI(chainCtx *snow.Context, db *DB, signer *Signer, blockClient BlockStore, signatureAggregator *acp118.SignatureAggregator) *API { +func NewAPI( + chainCtx *snow.Context, + db *DB, + signer *Signer, + verifier *Verifier, + signatureCache cache.Cacher[ids.ID, []byte], + signatureAggregator *acp118.SignatureAggregator, + offchainMessages [][]byte, +) (*API, error) { + offchainMsgs := make(map[ids.ID]*warp.UnsignedMessage) + for i, offchainMsg := range offchainMessages { + unsignedMsg, err := warp.ParseUnsignedMessage(offchainMsg) + if err != nil { + return nil, fmt.Errorf("failed to parse off-chain message at index %d: %w", i, err) + } + + if unsignedMsg.NetworkID != chainCtx.NetworkID { + return nil, fmt.Errorf("wrong network ID at index %d", i) + } + + if unsignedMsg.SourceChainID != chainCtx.ChainID { + return nil, fmt.Errorf("wrong source chain ID at index %d", i) + } + + _, err = payload.ParseAddressedCall(unsignedMsg.Payload) + if err != nil { + return nil, fmt.Errorf("failed to parse off-chain message at index %d as AddressedCall: %w", i, err) + } + offchainMsgs[unsignedMsg.ID()] = unsignedMsg + } + return &API{ db: db, signer: signer, - blockClient: blockClient, + verifier: verifier, chainContext: chainCtx, signatureAggregator: signatureAggregator, - } + messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](500), + signatureCache: signatureCache, + offchainMessages: offchainMsgs, + }, nil } // GetMessage returns the Warp message associated with a messageID. func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { - message, err := a.db.Get(messageID) + message, err := a.getMessage(messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) } return hexutil.Bytes(message.Bytes()), nil } +// getMessage retrieves a message from cache, offchain messages, or database. +func (a *API) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { + if msg, ok := a.messageCache.Get(messageID); ok { + return msg, nil + } + + if msg, ok := a.offchainMessages[messageID]; ok { + return msg, nil + } + + msg, err := a.db.Get(messageID) + if err != nil { + return nil, err + } + + a.messageCache.Put(messageID, msg) + return msg, nil +} + // GetMessageSignature returns the BLS signature associated with a messageID. func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - unsignedMessage, err := a.db.Get(messageID) + if sig, ok := a.signatureCache.Get(messageID); ok { + return sig, nil + } + + unsignedMessage, err := a.getMessage(messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) } - signature, err := a.signer.Sign(ctx, unsignedMessage) + if err := a.verifier.Verify(ctx, unsignedMessage, nil); err != nil { + return nil, fmt.Errorf("failed to verify message %s: %w", messageID, err) + } + + signature, err := a.signer.Sign(unsignedMessage) if err != nil { return nil, fmt.Errorf("failed to sign message %s with error %w", messageID, err) } + + a.signatureCache.Put(messageID, signature) return signature, nil } @@ -67,11 +137,6 @@ func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexuti // It constructs a warp message with a Hash payload containing the blockID, // then returns the signature for that message. func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { - // Verify the block exists before signing - if _, err := a.blockClient.GetBlock(ctx, blockID); err != nil { - return nil, fmt.Errorf("failed to get block %s: %w", blockID, err) - } - blockHashPayload, err := payload.NewHash(blockID) if err != nil { return nil, fmt.Errorf("failed to create block hash payload: %w", err) @@ -86,16 +151,31 @@ func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.By return nil, fmt.Errorf("failed to create unsigned warp message: %w", err) } - signature, err := a.signer.Sign(ctx, unsignedMessage) + msgID := unsignedMessage.ID() + // Check signature cache first + if sig, ok := a.signatureCache.Get(msgID); ok { + return sig, nil + } + + // Verify before signing + if err := a.verifier.Verify(ctx, unsignedMessage, nil); err != nil { + return nil, fmt.Errorf("failed to verify block %s: %w", blockID, err) + } + + // Sign + signature, err := a.signer.Sign(unsignedMessage) if err != nil { return nil, fmt.Errorf("failed to sign block %s with error %w", blockID, err) } + + // Cache the signature + a.signatureCache.Put(msgID, signature) return signature, nil } // GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { - unsignedMessage, err := a.db.Get(messageID) + unsignedMessage, err := a.getMessage(messageID) if err != nil { return nil, err } From abafd3df8b4f748565deb3f22205672255c68809 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 12 Nov 2025 14:54:04 -0500 Subject: [PATCH 22/53] Initial Josh easy fixes --- vms/evm/warp/backend.go | 32 +++++++------------- vms/evm/warp/backend_test.go | 35 ++++++++++++++++++---- vms/evm/warp/warptest/block_client.go | 43 --------------------------- 3 files changed, 41 insertions(+), 69 deletions(-) delete mode 100644 vms/evm/warp/warptest/block_client.go diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index b1a0ff384bc6..3efc255d2083 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -5,16 +5,13 @@ package warp import ( "context" - "errors" "fmt" - "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" "github.com/ava-labs/avalanchego/vms/platformvm/warp" @@ -27,16 +24,11 @@ const ( VerifyErrCode ) -var ( - _ acp118.Verifier = (*Verifier)(nil) - - ErrValidateBlock = errors.New("failed to validate block message") - ErrVerifyWarpMessage = errors.New("failed to verify warp message") -) +var _ acp118.Verifier = (*Verifier)(nil) // BlockStore provides access to accepted blocks. type BlockStore interface { - GetBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) + GetBlock(ctx context.Context, blockID ids.ID) error } // DB stores and retrieves warp messages from the underlying database. @@ -54,7 +46,6 @@ func NewDB(db database.Database) *DB { // Add stores a warp message in the database and cache. func (d *DB) Add(unsignedMsg *warp.UnsignedMessage) error { msgID := unsignedMsg.ID() - log.Debug("Adding warp message to backend", "messageID", msgID) // 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. @@ -173,8 +164,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes // of an accepted block indicating it should be signed by the VM. func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { blockID := blockHashPayload.Hash - _, err := v.blockClient.GetBlock(ctx, blockID) - if err != nil { + if err := v.blockClient.GetBlock(ctx, blockID); err != nil { v.blockValidationFail.Inc(1) return &common.AppError{ Code: VerifyErrCode, @@ -205,20 +195,20 @@ func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC } } - switch p := parsed.(type) { - case *message.ValidatorUptime: - if err := v.verifyUptimeMessage(p); err != nil { - v.uptimeValidationFail.Inc(1) - return err - } - default: + uptimeMsg, ok := parsed.(*message.ValidatorUptime) + if !ok { v.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, - Message: fmt.Sprintf("unknown message type: %T", p), + Message: fmt.Sprintf("unknown message type: %T", parsed), } } + if err := v.verifyUptimeMessage(uptimeMsg); err != nil { + v.uptimeValidationFail.Inc(1) + return err + } + return nil } diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index ca1feeed2a05..9d0388989595 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -6,6 +6,7 @@ package warp import ( "context" "fmt" + "slices" "testing" "time" @@ -14,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -27,7 +29,6 @@ import ( "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" - "github.com/ava-labs/avalanchego/vms/evm/warp/warptest" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" @@ -39,6 +40,11 @@ var ( testSourceAddress = utils.RandomBytes(20) testPayload = []byte("test") testUnsignedMessage *warp.UnsignedMessage + + // emptyBlockStore returns an error if a block is requested + emptyBlockStore BlockStore = testBlockStore(func(_ context.Context, _ ids.ID) error { + return database.ErrNotFound + }) ) func init() { @@ -52,6 +58,25 @@ func init() { } } +// testBlockStore implements BlockStore for testing +type testBlockStore func(ctx context.Context, blockID ids.ID) error + +func (t testBlockStore) GetBlock(ctx context.Context, blockID ids.ID) error { + return t(ctx, blockID) +} + +// makeBlockStore returns a new BlockStore that returns the provided blocks. +// If a block is requested that isn't part of the provided blocks, an error is +// returned. +func makeBlockStore(blkIDs ...ids.ID) BlockStore { + return testBlockStore(func(_ context.Context, blkID ids.ID) error { + if !slices.Contains(blkIDs, blkID) { + return database.ErrNotFound + } + return nil + }) +} + func TestAddAndGetValidMessage(t *testing.T) { db := memdb.New() @@ -89,7 +114,7 @@ func TestGetBlockSignature(t *testing.T) { require := require.New(t) blkID := ids.GenerateTestID() - blockStore := warptest.MakeBlockStore(blkID) + blockStore := makeBlockStore(blkID) db := memdb.New() sk, err := localsigner.New() @@ -210,7 +235,7 @@ func TestKnownMessageSignature(t *testing.T) { sigCache = &cache.Empty[ids.ID, []byte]{} } db := NewDB(database) - v := NewVerifier(db, warptest.EmptyBlockStore, nil) + v := NewVerifier(db, emptyBlockStore, nil) handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) requestBytes, expectedResponse := test.setup(db) @@ -249,7 +274,7 @@ func TestBlockSignatures(t *testing.T) { snowCtx := snowtest.Context(t, snowtest.CChainID) knownBlkID := ids.GenerateTestID() - blockStore := warptest.MakeBlockStore(knownBlkID) + blockStore := makeBlockStore(knownBlkID) toMessageBytes := func(id ids.ID) []byte { idPayload, err := payload.NewHash(id) @@ -408,7 +433,7 @@ func TestUptimeSignatures(t *testing.T) { require.NoError(t, uptimeTracker.Sync(t.Context())) db := NewDB(database) - verifier := NewVerifier(db, warptest.EmptyBlockStore, uptimeTracker) + verifier := NewVerifier(db, emptyBlockStore, uptimeTracker) handler := acp118.NewCachedHandler(sigCache, verifier, snowCtx.WarpSigner) // sourceAddress nonZero diff --git a/vms/evm/warp/warptest/block_client.go b/vms/evm/warp/warptest/block_client.go deleted file mode 100644 index 5e00d0936320..000000000000 --- a/vms/evm/warp/warptest/block_client.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// warptest exposes common functionality for testing the warp package. -package warptest - -import ( - "context" - "slices" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/consensus/snowman/snowmantest" - "github.com/ava-labs/avalanchego/snow/snowtest" -) - -// EmptyBlockStore returns an error if a block is requested -var EmptyBlockStore BlockStore = MakeBlockStore() - -type BlockStore func(ctx context.Context, blockID ids.ID) (snowman.Block, error) - -func (b BlockStore) GetBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) { - return b(ctx, blockID) -} - -// MakeBlockStore returns a new BlockStore that returns the provided blocks. -// If a block is requested that isn't part of the provided blocks, an error is -// returned. -func MakeBlockStore(blkIDs ...ids.ID) BlockStore { - return func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if !slices.Contains(blkIDs, blkID) { - return nil, database.ErrNotFound - } - - return &snowmantest.Block{ - Decidable: snowtest.Decidable{ - IDV: blkID, - Status: snowtest.Accepted, - }, - }, nil - } -} From 2a88f16c50a4971fba46118b07175261802070d7 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 12 Nov 2025 15:45:49 -0500 Subject: [PATCH 23/53] Remove use of payload and shared codec --- vms/evm/warp/backend.go | 26 +++++--------- vms/evm/warp/backend_test.go | 9 ++--- vms/evm/warp/message/codec.go | 33 +++++++++++++++++ .../warp/message/validator_uptime.go | 35 +++++++++++++------ vms/platformvm/warp/message/codec.go | 1 - 5 files changed, 68 insertions(+), 36 deletions(-) create mode 100644 vms/evm/warp/message/codec.go rename vms/{platformvm => evm}/warp/message/validator_uptime.go (53%) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index 3efc255d2083..e8f9b4501300 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -14,8 +14,8 @@ import ( "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" + "github.com/ava-labs/avalanchego/vms/evm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) @@ -133,7 +133,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes } else if err != database.ErrNotFound { return &common.AppError{ Code: ParseErrCode, - Message: fmt.Sprintf("failed to get message %s: %s", messageID, err.Error()), + Message: fmt.Sprintf("failed to get message %s: %s", messageID, err), } } @@ -142,7 +142,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes v.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, - Message: "failed to parse payload: " + err.Error(), + Message: fmt.Sprintf("failed to parse payload: %s", err), } } @@ -168,7 +168,7 @@ func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *pay v.blockValidationFail.Inc(1) return &common.AppError{ Code: VerifyErrCode, - Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), + Message: fmt.Sprintf("failed to get block %s: %s", blockID, err), } } @@ -177,16 +177,6 @@ func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *pay // verifyOffchainAddressedCall verifies the addressed call message func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { - // Further, parse the payload to see if it is a known type. - parsed, err := message.Parse(addressedCall.Payload) - if err != nil { - v.messageParseFail.Inc(1) - return &common.AppError{ - Code: ParseErrCode, - Message: "failed to parse addressed call message: " + err.Error(), - } - } - if len(addressedCall.SourceAddress) != 0 { v.addressedCallValidationFail.Inc(1) return &common.AppError{ @@ -195,12 +185,12 @@ func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC } } - uptimeMsg, ok := parsed.(*message.ValidatorUptime) - if !ok { + uptimeMsg, err := message.ParseValidatorUptime(addressedCall.Payload) + if err != nil { v.messageParseFail.Inc(1) return &common.AppError{ Code: ParseErrCode, - Message: fmt.Sprintf("unknown message type: %T", parsed), + Message: fmt.Sprintf("failed to parse addressed call message: %s", err), } } @@ -217,7 +207,7 @@ func (v *Verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *comm if err != nil { return &common.AppError{ Code: VerifyErrCode, - Message: "failed to get uptime: " + err.Error(), + Message: fmt.Sprintf("failed to get uptime: %s", err), } } diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 9d0388989595..88cbcc630274 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -29,8 +29,8 @@ import ( "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" + "github.com/ava-labs/avalanchego/vms/evm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) @@ -379,11 +379,8 @@ func TestUptimeSignatures(t *testing.T) { startTime := uint64(time.Now().Unix()) getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID) ([]byte, *warp.UnsignedMessage) { - uptimePayload := &message.ValidatorUptime{ - ValidationID: vID, - TotalUptime: 80, - } - require.NoError(t, message.Initialize(uptimePayload)) + uptimePayload, err := message.NewValidatorUptime(vID, 80) + require.NoError(t, err) addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) require.NoError(t, err) unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) diff --git a/vms/evm/warp/message/codec.go b/vms/evm/warp/message/codec.go new file mode 100644 index 000000000000..99c93e2dd499 --- /dev/null +++ b/vms/evm/warp/message/codec.go @@ -0,0 +1,33 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "errors" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/utils/units" +) + +const ( + CodecVersion = 0 + + MaxMessageSize = 24 * units.KiB +) + +var Codec codec.Manager + +func init() { + Codec = codec.NewManager(MaxMessageSize) + lc := linearcodec.NewDefault() + + err := errors.Join( + lc.RegisterType(&ValidatorUptime{}), + Codec.RegisterCodec(CodecVersion, lc), + ) + if err != nil { + panic(err) + } +} diff --git a/vms/platformvm/warp/message/validator_uptime.go b/vms/evm/warp/message/validator_uptime.go similarity index 53% rename from vms/platformvm/warp/message/validator_uptime.go rename to vms/evm/warp/message/validator_uptime.go index aa2cf94b4e7e..0c702a4006a5 100644 --- a/vms/platformvm/warp/message/validator_uptime.go +++ b/vms/evm/warp/message/validator_uptime.go @@ -4,29 +4,42 @@ package message import ( - "fmt" - "github.com/ava-labs/avalanchego/ids" ) // ValidatorUptime is signed when the ValidationID is known and the validator // has been up for TotalUptime seconds. type ValidatorUptime struct { - payload - ValidationID ids.ID `serialize:"true"` TotalUptime uint64 `serialize:"true"` // in seconds + + bytes []byte } -// ParseValidatorUptime converts a slice of bytes into an initialized ValidatorUptime. -func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { - payloadIntf, err := Parse(b) +func NewValidatorUptime(validationID ids.ID, totalUptime uint64) (*ValidatorUptime, error) { + msg := &ValidatorUptime{ + ValidationID: validationID, + TotalUptime: totalUptime, + } + bytes, err := Codec.Marshal(CodecVersion, msg) if err != nil { return nil, err } - payload, ok := payloadIntf.(*ValidatorUptime) - if !ok { - return nil, fmt.Errorf("%w: %T", ErrWrongType, payloadIntf) + msg.bytes = bytes + return msg, nil +} + +// ParseValidatorUptime converts a slice of bytes into an initialized ValidatorUptime. +func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { + var msg ValidatorUptime + if _, err := Codec.Unmarshal(b, &msg); err != nil { + return nil, err } - return payload, nil + msg.bytes = b + return &msg, nil +} + +// Bytes returns the binary representation of this payload. +func (v *ValidatorUptime) Bytes() []byte { + return v.bytes } diff --git a/vms/platformvm/warp/message/codec.go b/vms/platformvm/warp/message/codec.go index 2c11ced7fb2d..d2873bdcfd0c 100644 --- a/vms/platformvm/warp/message/codec.go +++ b/vms/platformvm/warp/message/codec.go @@ -24,7 +24,6 @@ func init() { lc.RegisterType(&RegisterL1Validator{}), lc.RegisterType(&L1ValidatorRegistration{}), lc.RegisterType(&L1ValidatorWeight{}), - lc.RegisterType(&ValidatorUptime{}), Codec.RegisterCodec(CodecVersion, lc), ) if err != nil { From 295dd4f5b803bcbe1941088c80cb692893041581 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 12 Nov 2025 15:48:59 -0500 Subject: [PATCH 24/53] Use warp signer directly --- vms/evm/warp/backend.go | 22 ---------------------- vms/evm/warp/backend_test.go | 9 +++------ vms/evm/warp/service.go | 4 ++-- 3 files changed, 5 insertions(+), 30 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index e8f9b4501300..cc8076257117 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -72,28 +72,6 @@ func (d *DB) Get(msgID ids.ID) (*warp.UnsignedMessage, error) { return unsignedMessage, nil } -// Signer signs warp messages. -type Signer struct { - warpSigner warp.Signer -} - -// NewSigner creates a new warp message signer. -func NewSigner(warpSigner warp.Signer) *Signer { - return &Signer{ - warpSigner: warpSigner, - } -} - -// Sign signs a warp message. -// Callers are responsible for verification and caching. -func (s *Signer) Sign(msg *warp.UnsignedMessage) ([]byte, error) { - sig, err := s.warpSigner.Sign(msg) - if err != nil { - return nil, fmt.Errorf("failed to sign warp message: %w", err) - } - return sig, nil -} - // Verifier implements acp118.Verifier and validates whether a warp message should be signed. type Verifier struct { db *DB diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 88cbcc630274..949f412bc695 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -85,13 +85,12 @@ func TestAddAndGetValidMessage(t *testing.T) { warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageDB := NewDB(db) - signer := NewSigner(warpSigner) // Add testUnsignedMessage to the warp backend require.NoError(t, messageDB.Add(testUnsignedMessage)) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := signer.Sign(testUnsignedMessage) + signature, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -123,7 +122,6 @@ func TestGetBlockSignature(t *testing.T) { messageDB := NewDB(db) verifier := NewVerifier(messageDB, blockStore, nil) - signer := NewSigner(warpSigner) blockHashPayload, err := payload.NewHash(blkID) require.NoError(err) @@ -137,7 +135,7 @@ func TestGetBlockSignature(t *testing.T) { require.Nil(appErr) // Then sign it - signature, err := signer.Sign(unsignedMessage) + signature, err := warpSigner.Sign(unsignedMessage) require.NoError(err) require.Equal(expectedSig, signature) @@ -159,7 +157,6 @@ func TestVerifierKnownMessage(t *testing.T) { messageDB := NewDB(db) verifier := NewVerifier(messageDB, nil, nil) - signer := NewSigner(warpSigner) // Add testUnsignedMessage to the database require.NoError(t, messageDB.Add(testUnsignedMessage)) @@ -169,7 +166,7 @@ func TestVerifierKnownMessage(t *testing.T) { require.Nil(t, appErr) // And can be signed - signature, err := signer.Sign(testUnsignedMessage) + signature, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index e1ba045e084f..9dae55812585 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -28,7 +28,7 @@ var errNoValidators = errors.New("cannot aggregate signatures from subnet with n type API struct { chainContext *snow.Context db *DB - signer *Signer + signer warp.Signer verifier *Verifier signatureAggregator *acp118.SignatureAggregator @@ -41,7 +41,7 @@ type API struct { func NewAPI( chainCtx *snow.Context, db *DB, - signer *Signer, + signer warp.Signer, verifier *Verifier, signatureCache cache.Cacher[ids.ID, []byte], signatureAggregator *acp118.SignatureAggregator, From 9d278c275ffa10146aed981715bc6e8612bf2cd0 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 12 Nov 2025 16:04:56 -0500 Subject: [PATCH 25/53] add back sign message function --- vms/evm/warp/backend_test.go | 12 +++++-- vms/evm/warp/service.go | 63 ++++++++++++++---------------------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 949f412bc695..d85401936187 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -104,8 +104,14 @@ func TestAddAndGetUnknownMessage(t *testing.T) { messageDB := NewDB(db) verifier := NewVerifier(messageDB, nil, nil) - // Try to verify an unknown message - should fail - appErr := verifier.Verify(t.Context(), testUnsignedMessage, nil) + // Create an unknown message with empty source address to test parse failure + unknownPayload, err := payload.NewAddressedCall([]byte{}, []byte("unknown message")) + require.NoError(t, err) + unknownMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownPayload.Bytes()) + require.NoError(t, err) + + // Try to verify an unknown message - should fail with parse error + appErr := verifier.Verify(t.Context(), unknownMessage, nil) require.ErrorIs(t, appErr, &common.AppError{Code: ParseErrCode}) } @@ -203,7 +209,7 @@ func TestKnownMessageSignature(t *testing.T) { }, "unknown message": { setup: func(_ *DB) (request []byte, expectedResponse []byte) { - unknownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("unknown message")) + unknownPayload, err := payload.NewAddressedCall([]byte{}, []byte("unknown message")) require.NoError(t, err) unknownMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) require.NoError(t, err) diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 9dae55812585..50665e2d7083 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -85,7 +85,7 @@ func NewAPI( func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { message, err := a.getMessage(messageID) if err != nil { - return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) + return nil, fmt.Errorf("failed to get message %s: %w", messageID, err) } return hexutil.Bytes(message.Bytes()), nil } @@ -111,26 +111,11 @@ func (a *API) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { // GetMessageSignature returns the BLS signature associated with a messageID. func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - if sig, ok := a.signatureCache.Get(messageID); ok { - return sig, nil - } - unsignedMessage, err := a.getMessage(messageID) if err != nil { - return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) - } - - if err := a.verifier.Verify(ctx, unsignedMessage, nil); err != nil { - return nil, fmt.Errorf("failed to verify message %s: %w", messageID, err) + return nil, fmt.Errorf("failed to get message %s: %w", messageID, err) } - - signature, err := a.signer.Sign(unsignedMessage) - if err != nil { - return nil, fmt.Errorf("failed to sign message %s with error %w", messageID, err) - } - - a.signatureCache.Put(messageID, signature) - return signature, nil + return a.signMessage(ctx, unsignedMessage) } // GetBlockSignature returns the BLS signature associated with a blockID. @@ -151,26 +136,7 @@ func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.By return nil, fmt.Errorf("failed to create unsigned warp message: %w", err) } - msgID := unsignedMessage.ID() - // Check signature cache first - if sig, ok := a.signatureCache.Get(msgID); ok { - return sig, nil - } - - // Verify before signing - if err := a.verifier.Verify(ctx, unsignedMessage, nil); err != nil { - return nil, fmt.Errorf("failed to verify block %s: %w", blockID, err) - } - - // Sign - signature, err := a.signer.Sign(unsignedMessage) - if err != nil { - return nil, fmt.Errorf("failed to sign block %s with error %w", blockID, err) - } - - // Cache the signature - a.signatureCache.Put(msgID, signature) - return signature, nil + return a.signMessage(ctx, unsignedMessage) } // GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] @@ -245,3 +211,24 @@ func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.Uns // gotchas that could impact signed messages becoming invalid. return hexutil.Bytes(signedMessage.Bytes()), nil } + +// signMessage verifies, signs, and caches a signature for the given unsigned message. +func (a *API) signMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) (hexutil.Bytes, error) { + msgID := unsignedMessage.ID() + + if sig, ok := a.signatureCache.Get(msgID); ok { + return sig, nil + } + + if err := a.verifier.Verify(ctx, unsignedMessage, nil); err != nil { + return nil, fmt.Errorf("failed to verify message %s: %w", msgID, err) + } + + signature, err := a.signer.Sign(unsignedMessage) + if err != nil { + return nil, fmt.Errorf("failed to sign message %s: %w", msgID, err) + } + + a.signatureCache.Put(msgID, signature) + return signature, nil +} From 6414f63c5cfb241e783b100fb30cd98f5f119996 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 12 Nov 2025 16:06:11 -0500 Subject: [PATCH 26/53] lint --- vms/evm/warp/message/validator_uptime.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vms/evm/warp/message/validator_uptime.go b/vms/evm/warp/message/validator_uptime.go index 0c702a4006a5..0611844c44d2 100644 --- a/vms/evm/warp/message/validator_uptime.go +++ b/vms/evm/warp/message/validator_uptime.go @@ -3,9 +3,7 @@ package message -import ( - "github.com/ava-labs/avalanchego/ids" -) +import "github.com/ava-labs/avalanchego/ids" // ValidatorUptime is signed when the ValidationID is known and the validator // has been up for TotalUptime seconds. From 0758cd3cc9d76b0ec4ecc63c5c5b6fbc3967680a Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 12 Nov 2025 17:22:01 -0500 Subject: [PATCH 27/53] Use acp118 adapter --- vms/evm/warp/backend.go | 34 +++++++++++++++++++++++++++++++--- vms/evm/warp/backend_test.go | 15 +++++++-------- vms/evm/warp/service.go | 2 +- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index cc8076257117..eff08fd1aaea 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -9,6 +9,7 @@ import ( "github.com/ava-labs/libevm/metrics" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -24,7 +25,7 @@ const ( VerifyErrCode ) -var _ acp118.Verifier = (*Verifier)(nil) +var _ acp118.Verifier = (*acp118Adapter)(nil) // BlockStore provides access to accepted blocks. type BlockStore interface { @@ -102,8 +103,8 @@ func NewVerifier( } } -// Verify implements acp118.Verifier and validates whether a warp message should be signed. -func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage, _ []byte) *common.AppError { +// Verify validates whether a warp message should be signed. +func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMessage) *common.AppError { messageID := unsignedMessage.ID() // Known on-chain messages should be signed if _, err := v.db.Get(messageID); err == nil { @@ -200,3 +201,30 @@ func (v *Verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *comm return nil } + +// acp118Adapter adapts the EVM warp Verifier to the acp118.Verifier interface. +// This adapter ignores the justification parameter since the EVM verifier doesn't use it. +type acp118Adapter struct { + verifier *Verifier +} + +// Verify implements acp118.Verifier by delegating to the wrapped Verifier. +// The justification parameter is ignored as it's not used by the EVM warp verifier. +func (a *acp118Adapter) Verify(ctx context.Context, message *warp.UnsignedMessage, _ []byte) *common.AppError { + return a.verifier.Verify(ctx, message) +} + +// NewHandler creates a new acp118.Handler for signing warp messages. +// This is a convenience function that wraps the verifier in an acp118Adapter +// and creates a cached handler. +func NewHandler( + signatureCache cache.Cacher[ids.ID, []byte], + verifier *Verifier, + signer warp.Signer, +) *acp118.Handler { + return acp118.NewCachedHandler( + signatureCache, + &acp118Adapter{verifier: verifier}, + signer, + ) +} diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index d85401936187..dd60bb57f259 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -18,7 +18,6 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/snowtest" @@ -111,7 +110,7 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) // Try to verify an unknown message - should fail with parse error - appErr := verifier.Verify(t.Context(), unknownMessage, nil) + appErr := verifier.Verify(t.Context(), unknownMessage) require.ErrorIs(t, appErr, &common.AppError{Code: ParseErrCode}) } @@ -137,7 +136,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) // Verify the block message - appErr := verifier.Verify(t.Context(), unsignedMessage, nil) + appErr := verifier.Verify(t.Context(), unsignedMessage) require.Nil(appErr) // Then sign it @@ -150,7 +149,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) require.NoError(err) - unknownAppErr := verifier.Verify(t.Context(), unknownUnsignedMessage, nil) + unknownAppErr := verifier.Verify(t.Context(), unknownUnsignedMessage) require.ErrorIs(unknownAppErr, &common.AppError{Code: VerifyErrCode}) } @@ -168,7 +167,7 @@ func TestVerifierKnownMessage(t *testing.T) { require.NoError(t, messageDB.Add(testUnsignedMessage)) // Known messages in the DB should pass verification - appErr := verifier.Verify(t.Context(), testUnsignedMessage, nil) + appErr := verifier.Verify(t.Context(), testUnsignedMessage) require.Nil(t, appErr) // And can be signed @@ -239,7 +238,7 @@ func TestKnownMessageSignature(t *testing.T) { } db := NewDB(database) v := NewVerifier(db, emptyBlockStore, nil) - handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) + handler := NewHandler(sigCache, v, snowCtx.WarpSigner) requestBytes, expectedResponse := test.setup(db) protoMsg := &sdk.SignatureRequest{Message: requestBytes} @@ -342,7 +341,7 @@ func TestBlockSignatures(t *testing.T) { } db := NewDB(database) v := NewVerifier(db, blockStore, nil) - handler := acp118.NewCachedHandler(sigCache, v, snowCtx.WarpSigner) + handler := NewHandler(sigCache, v, snowCtx.WarpSigner) requestBytes, expectedResponse := test.setup() protoMsg := &sdk.SignatureRequest{Message: requestBytes} @@ -434,7 +433,7 @@ func TestUptimeSignatures(t *testing.T) { db := NewDB(database) verifier := NewVerifier(db, emptyBlockStore, uptimeTracker) - handler := acp118.NewCachedHandler(sigCache, verifier, snowCtx.WarpSigner) + handler := NewHandler(sigCache, verifier, snowCtx.WarpSigner) // sourceAddress nonZero protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 50665e2d7083..807b64063971 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -220,7 +220,7 @@ func (a *API) signMessage(ctx context.Context, unsignedMessage *warp.UnsignedMes return sig, nil } - if err := a.verifier.Verify(ctx, unsignedMessage, nil); err != nil { + if err := a.verifier.Verify(ctx, unsignedMessage); err != nil { return nil, fmt.Errorf("failed to verify message %s: %w", msgID, err) } From 35a0c0eb97791899c20b889764d633ac9ff677c5 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Sun, 16 Nov 2025 00:55:47 -0500 Subject: [PATCH 28/53] add codec test --- vms/evm/warp/message/codec_test.go | 128 +++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 vms/evm/warp/message/codec_test.go diff --git a/vms/evm/warp/message/codec_test.go b/vms/evm/warp/message/codec_test.go new file mode 100644 index 000000000000..1bab8cdcce4e --- /dev/null +++ b/vms/evm/warp/message/codec_test.go @@ -0,0 +1,128 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" +) + +// TestCodecSerialization tests the registration order changes in codec.go, +// does not change, preventing unintended serialization format changes. +func TestCodecSerialization(t *testing.T) { + tests := []struct { + name string + msg *ValidatorUptime + wantBytes []byte + }{ + { + name: "zero values", + msg: &ValidatorUptime{ + ValidationID: ids.Empty, + TotalUptime: 0, + }, + wantBytes: []byte{ + // Codec version (0) + 0x00, 0x00, + // ValidationID (32 bytes of zeros) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // TotalUptime (8 bytes, uint64 = 0) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, + { + name: "non-zero values", + msg: &ValidatorUptime{ + ValidationID: ids.ID{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + }, + TotalUptime: 12345, + }, + wantBytes: []byte{ + // Codec version (0) + 0x00, 0x00, + // ValidationID (32 bytes) + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + // TotalUptime (8 bytes, uint64 = 12345 in big-endian) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x39, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + // Test marshaling produces expected bytes + gotBytes, err := Codec.Marshal(CodecVersion, tt.msg) + require.NoError(err) + require.Equal(tt.wantBytes, gotBytes, "marshaled bytes do not match expected - codec registration order may have changed") + + // Test unmarshaling the expected bytes produces the original message + var gotMsg ValidatorUptime + version, err := Codec.Unmarshal(tt.wantBytes, &gotMsg) + require.NoError(err) + require.Equal(uint16(CodecVersion), version) + require.Equal(tt.msg.ValidationID, gotMsg.ValidationID) + require.Equal(tt.msg.TotalUptime, gotMsg.TotalUptime) + }) + } +} + +// TestCodecRoundTrip verifies that messages can be marshaled and unmarshaled +// without data loss. +func TestCodecRoundTrip(t *testing.T) { + tests := []struct { + name string + msg *ValidatorUptime + }{ + { + name: "zero values", + msg: &ValidatorUptime{ + ValidationID: ids.Empty, + TotalUptime: 0, + }, + }, + { + name: "max uptime", + msg: &ValidatorUptime{ + ValidationID: ids.GenerateTestID(), + TotalUptime: ^uint64(0), // max uint64 + }, + }, + { + name: "random values", + msg: &ValidatorUptime{ + ValidationID: ids.GenerateTestID(), + TotalUptime: 987654321, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bytes, err := Codec.Marshal(CodecVersion, tt.msg) + require.NoError(t, err) + + var gotMsg ValidatorUptime + version, err := Codec.Unmarshal(bytes, &gotMsg) + require.NoError(t, err) + require.Equal(t, uint16(CodecVersion), version) + require.Equal(t, tt.msg.ValidationID, gotMsg.ValidationID) + require.Equal(t, tt.msg.TotalUptime, gotMsg.TotalUptime) + }) + } +} From dabcd5f67070167b1d56bc5a13c166745980f6c5 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Sun, 16 Nov 2025 01:01:48 -0500 Subject: [PATCH 29/53] don't require t --- vms/evm/warp/backend_test.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index dd60bb57f259..763b53c6ca62 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -115,42 +115,40 @@ func TestAddAndGetUnknownMessage(t *testing.T) { } func TestGetBlockSignature(t *testing.T) { - require := require.New(t) - blkID := ids.GenerateTestID() blockStore := makeBlockStore(blkID) db := memdb.New() sk, err := localsigner.New() - require.NoError(err) + require.NoError(t, err) warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageDB := NewDB(db) verifier := NewVerifier(messageDB, blockStore, nil) blockHashPayload, err := payload.NewHash(blkID) - require.NoError(err) + require.NoError(t, err) unsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) - require.NoError(err) + require.NoError(t, err) expectedSig, err := warpSigner.Sign(unsignedMessage) - require.NoError(err) + require.NoError(t, err) // Verify the block message appErr := verifier.Verify(t.Context(), unsignedMessage) - require.Nil(appErr) + require.Nil(t, appErr) // Then sign it signature, err := warpSigner.Sign(unsignedMessage) - require.NoError(err) - require.Equal(expectedSig, signature) + require.NoError(t, err) + require.Equal(t, expectedSig, signature) // Test that an unknown block fails verification unknownBlockHashPayload, err := payload.NewHash(ids.GenerateTestID()) - require.NoError(err) + require.NoError(t, err) unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) - require.NoError(err) + require.NoError(t, err) unknownAppErr := verifier.Verify(t.Context(), unknownUnsignedMessage) - require.ErrorIs(unknownAppErr, &common.AppError{Code: VerifyErrCode}) + require.ErrorIs(t, unknownAppErr, &common.AppError{Code: VerifyErrCode}) } func TestVerifierKnownMessage(t *testing.T) { From 6db379927d758396224cfe8f7fac0d6dd47d1f0c Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 17 Dec 2025 17:02:13 -0500 Subject: [PATCH 30/53] refactor: use evms/vm/warp --- graft/coreth/plugin/evm/vm.go | 68 ++-- graft/coreth/plugin/evm/vm_warp_test.go | 30 +- graft/coreth/plugin/evm/wrapped_block.go | 2 +- .../precompile/contracts/warp/config.go | 2 +- .../precompile/precompileconfig/config.go | 2 +- graft/coreth/tests/warp/warp_test.go | 4 +- graft/coreth/warp/backend.go | 206 ---------- graft/coreth/warp/backend_test.go | 183 --------- graft/coreth/warp/client.go | 80 ---- graft/coreth/warp/service.go | 142 ------- graft/coreth/warp/verifier_backend.go | 71 ---- graft/coreth/warp/verifier_backend_test.go | 243 ------------ graft/coreth/warp/verifier_stats.go | 27 -- graft/coreth/warp/warptest/block_client.go | 43 -- .../examples/sign-uptime-message/main.go | 4 +- graft/subnet-evm/plugin/evm/vm.go | 69 ++-- graft/subnet-evm/plugin/evm/vm_warp_test.go | 30 +- graft/subnet-evm/plugin/evm/wrapped_block.go | 2 +- .../precompile/contracts/warp/config.go | 2 +- .../precompile/precompileconfig/config.go | 2 +- graft/subnet-evm/tests/warp/warp_test.go | 60 +-- graft/subnet-evm/warp/backend.go | 210 ---------- graft/subnet-evm/warp/backend_test.go | 183 --------- graft/subnet-evm/warp/client.go | 80 ---- graft/subnet-evm/warp/messages/codec.go | 33 -- graft/subnet-evm/warp/messages/payload.go | 39 -- .../warp/messages/validator_uptime.go | 51 --- graft/subnet-evm/warp/service.go | 142 ------- graft/subnet-evm/warp/verifier_backend.go | 131 ------- .../subnet-evm/warp/verifier_backend_test.go | 366 ------------------ graft/subnet-evm/warp/verifier_stats.go | 41 -- .../subnet-evm/warp/warptest/block_client.go | 43 -- vms/evm/warp/backend.go | 4 +- vms/evm/warp/backend_test.go | 2 +- vms/evm/warp/service.go | 11 +- 35 files changed, 170 insertions(+), 2438 deletions(-) delete mode 100644 graft/coreth/warp/backend.go delete mode 100644 graft/coreth/warp/backend_test.go delete mode 100644 graft/coreth/warp/client.go delete mode 100644 graft/coreth/warp/service.go delete mode 100644 graft/coreth/warp/verifier_backend.go delete mode 100644 graft/coreth/warp/verifier_backend_test.go delete mode 100644 graft/coreth/warp/verifier_stats.go delete mode 100644 graft/coreth/warp/warptest/block_client.go delete mode 100644 graft/subnet-evm/warp/backend.go delete mode 100644 graft/subnet-evm/warp/backend_test.go delete mode 100644 graft/subnet-evm/warp/client.go delete mode 100644 graft/subnet-evm/warp/messages/codec.go delete mode 100644 graft/subnet-evm/warp/messages/payload.go delete mode 100644 graft/subnet-evm/warp/messages/validator_uptime.go delete mode 100644 graft/subnet-evm/warp/service.go delete mode 100644 graft/subnet-evm/warp/verifier_backend.go delete mode 100644 graft/subnet-evm/warp/verifier_backend_test.go delete mode 100644 graft/subnet-evm/warp/verifier_stats.go delete mode 100644 graft/subnet-evm/warp/warptest/block_client.go diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index e1a926874715..5135c69ef049 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -37,6 +37,7 @@ import ( _ "github.com/ava-labs/libevm/eth/tracers/js" _ "github.com/ava-labs/libevm/eth/tracers/native" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/cache/metercacher" "github.com/ava-labs/avalanchego/codec" @@ -63,7 +64,6 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/sync/client/stats" "github.com/ava-labs/avalanchego/graft/coreth/sync/handlers" "github.com/ava-labs/avalanchego/graft/coreth/triedb/hashdb" - "github.com/ava-labs/avalanchego/graft/coreth/warp" "github.com/ava-labs/avalanchego/graft/evm/constants" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" @@ -80,6 +80,7 @@ import ( "github.com/ava-labs/avalanchego/vms/evm/acp176" "github.com/ava-labs/avalanchego/vms/evm/acp226" "github.com/ava-labs/avalanchego/vms/evm/sync/customrawdb" + "github.com/ava-labs/avalanchego/vms/evm/warp" corethlog "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/log" warpcontract "github.com/ava-labs/avalanchego/graft/coreth/precompile/contracts/warp" @@ -246,9 +247,13 @@ type VM struct { vmsync.Server vmsync.Client - // Avalanche Warp Messaging backend + // Avalanche Warp Messaging components // Used to serve BLS signatures of warp messages over RPC - warpBackend warp.Backend + warpMsgDB *warp.DB + warpVerifier *warp.Verifier + warpSignatureCache cache.Cacher[ids.ID, []byte] + offchainWarpMessages [][]byte + warpAPI *warp.API ethTxPushGossiper avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*GossipEthTx]] @@ -429,15 +434,16 @@ func (vm *VM) Initialize( } // Initialize warp backend - offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) + vm.offchainWarpMessages = make([][]byte, len(vm.config.WarpOffChainMessages)) for i, hexMsg := range vm.config.WarpOffChainMessages { - offchainWarpMessages[i] = []byte(hexMsg) + vm.offchainWarpMessages[i] = []byte(hexMsg) } warpSignatureCache := lru.NewCache[ids.ID, []byte](warpSignatureCacheSize) meteredCache, err := metercacher.New("warp_signature_cache", vm.sdkMetrics, warpSignatureCache) if err != nil { return fmt.Errorf("failed to create warp signature cache: %w", err) } + vm.warpSignatureCache = meteredCache // clear warpdb on initialization if config enabled if vm.config.PruneWarpDB { @@ -446,14 +452,18 @@ func (vm *VM) Initialize( } } - vm.warpBackend, err = warp.NewBackend( - vm.ctx.NetworkID, - vm.ctx.ChainID, + vm.warpMsgDB = warp.NewDB(vm.warpDB) + vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, nil) + + // Create warp API (signatureAggregator will be nil until createAPIs) + vm.warpAPI, err = warp.NewAPI( + vm.ctx, + vm.warpMsgDB, vm.ctx.WarpSigner, - vm, - vm.warpDB, - meteredCache, - offchainWarpMessages, + vm.warpVerifier, + vm.warpSignatureCache, + nil, // signatureAggregator is set later in createAPIs + vm.offchainWarpMessages, ) if err != nil { return err @@ -465,7 +475,7 @@ func (vm *VM) Initialize( go vm.ctx.Log.RecoverAndPanic(vm.startContinuousProfiler) // Add p2p warp message warpHandler - warpHandler := acp118.NewCachedHandler(meteredCache, vm.warpBackend, vm.ctx.WarpSigner) + warpHandler := warp.NewHandler(vm.warpSignatureCache, vm.warpVerifier, vm.ctx.WarpSigner) if err = vm.Network.AddHandler(p2p.SignatureRequestHandlerID, warpHandler); err != nil { return err } @@ -997,25 +1007,23 @@ func (vm *VM) getBlock(_ context.Context, id ids.ID) (snowman.Block, error) { return wrapBlock(ethBlock, vm) } -// GetAcceptedBlock attempts to retrieve block [blkID] from the VM. This method -// only returns accepted blocks. -func (vm *VM) GetAcceptedBlock(ctx context.Context, blkID ids.ID) (snowman.Block, error) { +// HasBlock returns nil if the block is accepted, or an error otherwise. +// Implements warp.BlockStore. +func (vm *VM) HasBlock(ctx context.Context, blkID ids.ID) error { blk, err := vm.GetBlock(ctx, blkID) if err != nil { - return nil, err + return err } - height := blk.Height() - acceptedBlkID, err := vm.GetBlockIDAtHeight(ctx, height) + acceptedBlkID, err := vm.GetBlockIDAtHeight(ctx, blk.Height()) if err != nil { - return nil, err + return err } if acceptedBlkID != blkID { - // The provided block is not accepted. - return nil, database.ErrNotFound + return database.ErrNotFound } - return blk, nil + return nil } // SetPreference sets what the current tail of the chain is @@ -1084,7 +1092,19 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.Network.NewClient(p2p.SignatureRequestHandlerID) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - if err := handler.RegisterName("warp", warp.NewAPI(vm.ctx, vm.warpBackend, signatureAggregator)); err != nil { + warpAPI, err := warp.NewAPI( + vm.ctx, + vm.warpMsgDB, + vm.ctx.WarpSigner, + vm.warpVerifier, + vm.warpSignatureCache, + signatureAggregator, + vm.offchainWarpMessages, + ) + if err != nil { + return nil, err + } + if err := handler.RegisterName("warp", warpAPI); err != nil { return nil, err } enabledAPIs = append(enabledAPIs, "warp") diff --git a/graft/coreth/plugin/evm/vm_warp_test.go b/graft/coreth/plugin/evm/vm_warp_test.go index 353f0e45f977..5c34a34df9d1 100644 --- a/graft/coreth/plugin/evm/vm_warp_test.go +++ b/graft/coreth/plugin/evm/vm_warp_test.go @@ -28,7 +28,6 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/vmtest" "github.com/ava-labs/avalanchego/graft/coreth/precompile/contract" "github.com/ava-labs/avalanchego/graft/coreth/utils" - "github.com/ava-labs/avalanchego/graft/coreth/warp" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -45,6 +44,7 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/chain" "github.com/ava-labs/avalanchego/vms/evm/predicate" + "github.com/ava-labs/avalanchego/vms/evm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" warpcontract "github.com/ava-labs/avalanchego/graft/coreth/precompile/contracts/warp" @@ -140,17 +140,17 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.NoError(err) // Verify the signature cannot be fetched before the block is accepted - _, err = vm.warpBackend.GetMessageSignature(t.Context(), unsignedMessage) - require.ErrorIs(err, warp.ErrVerifyWarpMessage) - _, err = vm.warpBackend.GetBlockSignature(t.Context(), blk.ID()) - require.ErrorIs(err, warp.ErrValidateBlock) + _, err = vm.warpAPI.GetMessageSignature(t.Context(), unsignedMessage.ID()) + require.ErrorIs(err, warp.ErrMessageNotFound) + _, err = vm.warpAPI.GetBlockSignature(t.Context(), blk.ID()) + require.ErrorIs(err, warp.ErrBlockNotFound) require.NoError(vm.SetPreference(t.Context(), blk.ID())) require.NoError(blk.Accept(t.Context())) vm.blockChain.DrainAcceptorQueue() // Verify the message signature after accepting the block. - rawSignatureBytes, err := vm.warpBackend.GetMessageSignature(t.Context(), unsignedMessage) + rawSignatureBytes, err := vm.warpAPI.GetMessageSignature(t.Context(), unsignedMessage.ID()) require.NoError(err) blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes) require.NoError(err) @@ -167,7 +167,7 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) // Verify the blockID will now be signed by the backend and produces a valid signature. - rawSignatureBytes, err = vm.warpBackend.GetBlockSignature(t.Context(), blk.ID()) + rawSignatureBytes, err = vm.warpAPI.GetBlockSignature(t.Context(), blk.ID()) require.NoError(err) blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes) require.NoError(err) @@ -826,14 +826,14 @@ func testSignatureRequestsToVM(t *testing.T, scheme string) { require.NoError(t, err) // Add the known message and get its signature to confirm - require.NoError(t, vm.warpBackend.AddMessage(knownWarpMessage)) - knownMessageSignature, err := vm.warpBackend.GetMessageSignature(t.Context(), knownWarpMessage) + require.NoError(t, vm.warpMsgDB.Add(knownWarpMessage)) + knownMessageSignature, err := vm.warpAPI.GetMessageSignature(t.Context(), knownWarpMessage.ID()) require.NoError(t, err) // Setup known block lastAcceptedID, err := vm.LastAccepted(t.Context()) require.NoError(t, err) - knownBlockSignature, err := vm.warpBackend.GetBlockSignature(t.Context(), lastAcceptedID) + knownBlockSignature, err := vm.warpAPI.GetBlockSignature(t.Context(), lastAcceptedID) require.NoError(t, err) type testCase struct { @@ -936,9 +936,9 @@ func TestClearWarpDB(t *testing.T) { for _, payload := range payloads { unsignedMsg, err := avalancheWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, payload) require.NoError(t, err) - require.NoError(t, vm.warpBackend.AddMessage(unsignedMsg)) + require.NoError(t, vm.warpMsgDB.Add(unsignedMsg)) // ensure that the message was added - _, err = vm.warpBackend.GetMessageSignature(t.Context(), unsignedMsg) + _, err = vm.warpAPI.GetMessageSignature(t.Context(), unsignedMsg.ID()) require.NoError(t, err) messages = append(messages, unsignedMsg) } @@ -953,7 +953,7 @@ func TestClearWarpDB(t *testing.T) { // check messages are still present for _, message := range messages { - bytes, err := vm.warpBackend.GetMessageSignature(t.Context(), message) + bytes, err := vm.warpAPI.GetMessageSignature(t.Context(), message.ID()) require.NoError(t, err) require.NotEmpty(t, bytes) } @@ -972,7 +972,7 @@ func TestClearWarpDB(t *testing.T) { // ensure all messages have been deleted for _, message := range messages { - _, err := vm.warpBackend.GetMessageSignature(t.Context(), message) - require.ErrorIs(t, err, &commonEng.AppError{Code: warp.ParseErrCode}) + _, err := vm.warpAPI.GetMessageSignature(t.Context(), message.ID()) + require.ErrorIs(t, err, warp.ErrMessageNotFound) } } diff --git a/graft/coreth/plugin/evm/wrapped_block.go b/graft/coreth/plugin/evm/wrapped_block.go index c7600a9bf2cc..c1d23cde77ac 100644 --- a/graft/coreth/plugin/evm/wrapped_block.go +++ b/graft/coreth/plugin/evm/wrapped_block.go @@ -154,7 +154,7 @@ func (b *wrappedBlock) handlePrecompileAccept(rules extras.Rules) error { } acceptCtx := &precompileconfig.AcceptContext{ SnowCtx: b.vm.ctx, - Warp: b.vm.warpBackend, + Warp: b.vm.warpMsgDB, } for _, receipt := range receipts { for logIdx, log := range receipt.Logs { diff --git a/graft/coreth/precompile/contracts/warp/config.go b/graft/coreth/precompile/contracts/warp/config.go index 1c2551f7e031..45181d55c763 100644 --- a/graft/coreth/precompile/contracts/warp/config.go +++ b/graft/coreth/precompile/contracts/warp/config.go @@ -130,7 +130,7 @@ func (*Config) Accept(acceptCtx *precompileconfig.AcceptContext, blockHash commo "logData", common.Bytes2Hex(logData), "warpMessageID", unsignedMessage.ID(), ) - if err := acceptCtx.Warp.AddMessage(unsignedMessage); err != nil { + if err := acceptCtx.Warp.Add(unsignedMessage); err != nil { return fmt.Errorf("failed to add warp message during accept (TxHash: %s, LogIndex: %d): %w", txHash, logIndex, err) } return nil diff --git a/graft/coreth/precompile/precompileconfig/config.go b/graft/coreth/precompile/precompileconfig/config.go index 0fb6dc088eac..c4af6adcdff7 100644 --- a/graft/coreth/precompile/precompileconfig/config.go +++ b/graft/coreth/precompile/precompileconfig/config.go @@ -54,7 +54,7 @@ type Predicater interface { } type WarpMessageWriter interface { - AddMessage(unsignedMessage *warp.UnsignedMessage) error + Add(unsignedMessage *warp.UnsignedMessage) error } // AcceptContext defines the context passed in to a precompileconfig's Accepter diff --git a/graft/coreth/tests/warp/warp_test.go b/graft/coreth/tests/warp/warp_test.go index 328f57789129..9deaf886b5a9 100644 --- a/graft/coreth/tests/warp/warp_test.go +++ b/graft/coreth/tests/warp/warp_test.go @@ -37,7 +37,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/platformvm/api" - warpBackend "github.com/ava-labs/avalanchego/graft/coreth/warp" + warpBackend "github.com/ava-labs/avalanchego/vms/evm/warp" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ethereum "github.com/ava-labs/libevm" ginkgo "github.com/onsi/ginkgo/v2" @@ -318,7 +318,7 @@ func (w *warpTest) aggregateSignaturesViaAPI() { tc := e2e.NewTestContext() ctx := tc.DefaultContext() - warpAPIs := make(map[ids.NodeID]warpBackend.Client, len(w.sendingSubnetURIs)) + warpAPIs := make(map[ids.NodeID]*warpBackend.Client, len(w.sendingSubnetURIs)) for _, uri := range w.sendingSubnetURIs { client, err := warpBackend.NewClient(uri, w.sendingSubnet.BlockchainID.String()) require.NoError(err) diff --git a/graft/coreth/warp/backend.go b/graft/coreth/warp/backend.go deleted file mode 100644 index d59d2b7ed5b0..000000000000 --- a/graft/coreth/warp/backend.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "errors" - "fmt" - - "github.com/ava-labs/libevm/log" - - "github.com/ava-labs/avalanchego/cache" - "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -var ( - _ Backend = (*backend)(nil) - ErrValidateBlock = errors.New("failed to validate block message") - ErrVerifyWarpMessage = errors.New("failed to verify warp message") - errParsingOffChainMessage = errors.New("failed to parse off-chain message") - - messageCacheSize = 500 -) - -type BlockClient interface { - GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.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 *avalancheWarp.UnsignedMessage) error - - // GetMessageSignature validates the message and returns the signature of the requested message. - GetMessageSignature(ctx context.Context, message *avalancheWarp.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) (*avalancheWarp.UnsignedMessage, error) - - acp118.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 avalancheWarp.Signer - blockClient BlockClient - signatureCache cache.Cacher[ids.ID, []byte] - messageCache *lru.Cache[ids.ID, *avalancheWarp.UnsignedMessage] - offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.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 avalancheWarp.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, *avalancheWarp.UnsignedMessage](messageCacheSize), - stats: newVerifierStats(), - offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), - } - return b, b.initOffChainMessages(offchainMessages) -} - -func (b *backend) initOffChainMessages(offchainMessages [][]byte) error { - for i, offchainMsg := range offchainMessages { - unsignedMsg, err := avalancheWarp.ParseUnsignedMessage(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", avalancheWarp.ErrWrongNetworkID, i) - } - - if unsignedMsg.SourceChainID != b.sourceChainID { - return fmt.Errorf("%w at index %d", avalancheWarp.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 *avalancheWarp.UnsignedMessage) 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 *avalancheWarp.UnsignedMessage) ([]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("%w: %w", ErrVerifyWarpMessage, 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 := avalancheWarp.NewUnsignedMessage(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("%w: %w", ErrValidateBlock, 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) (*avalancheWarp.UnsignedMessage, 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 := avalancheWarp.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 *avalancheWarp.UnsignedMessage) ([]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 -} diff --git a/graft/coreth/warp/backend_test.go b/graft/coreth/warp/backend_test.go deleted file mode 100644 index 2549c768a911..000000000000 --- a/graft/coreth/warp/backend_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/graft/coreth/warp/warptest" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -var ( - networkID uint32 = 54321 - sourceChainID = ids.GenerateTestID() - testSourceAddress = utils.RandomBytes(20) - testPayload = []byte("test") - testUnsignedMessage *avalancheWarp.UnsignedMessage -) - -func init() { - testAddressedCallPayload, err := payload.NewAddressedCall(testSourceAddress, testPayload) - if err != nil { - panic(err) - } - testUnsignedMessage, err = avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, testAddressedCallPayload.Bytes()) - if err != nil { - panic(err) - } -} - -func TestAddAndGetValidMessage(t *testing.T) { - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) - require.NoError(t, err) - - // Add testUnsignedMessage to the warp backend - require.NoError(t, backend.AddMessage(testUnsignedMessage)) - - // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(t.Context(), testUnsignedMessage) - require.NoError(t, err) - - expectedSig, err := warpSigner.Sign(testUnsignedMessage) - require.NoError(t, err) - require.Equal(t, expectedSig, signature) -} - -func TestAddAndGetUnknownMessage(t *testing.T) { - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) - require.NoError(t, err) - - // Try getting a signature for a message that was not added. - _, err = backend.GetMessageSignature(t.Context(), testUnsignedMessage) - require.ErrorIs(t, err, ErrVerifyWarpMessage) -} - -func TestGetBlockSignature(t *testing.T) { - require := require.New(t) - - blkID := ids.GenerateTestID() - blockClient := warptest.MakeBlockClient(blkID) - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, db, messageSignatureCache, nil) - require.NoError(err) - - blockHashPayload, err := payload.NewHash(blkID) - require.NoError(err) - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) - require.NoError(err) - expectedSig, err := warpSigner.Sign(unsignedMessage) - require.NoError(err) - - signature, err := backend.GetBlockSignature(t.Context(), blkID) - require.NoError(err) - require.Equal(expectedSig, signature) - - _, err = backend.GetBlockSignature(t.Context(), ids.GenerateTestID()) - require.ErrorIs(err, ErrValidateBlock) -} - -func TestZeroSizedCache(t *testing.T) { - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - - // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. - messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) - require.NoError(t, err) - - // Add testUnsignedMessage to the warp backend - require.NoError(t, backend.AddMessage(testUnsignedMessage)) - - // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(t.Context(), testUnsignedMessage) - require.NoError(t, err) - - expectedSig, err := warpSigner.Sign(testUnsignedMessage) - require.NoError(t, err) - require.Equal(t, expectedSig, signature) -} - -func TestOffChainMessages(t *testing.T) { - type test struct { - offchainMessages [][]byte - check func(require *require.Assertions, b Backend) - err error - } - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - - for name, test := range map[string]test{ - "no offchain messages": {}, - "single off-chain message": { - offchainMessages: [][]byte{ - testUnsignedMessage.Bytes(), - }, - check: func(require *require.Assertions, b Backend) { - msg, err := b.GetMessage(testUnsignedMessage.ID()) - require.NoError(err) - require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - - signature, err := b.GetMessageSignature(t.Context(), testUnsignedMessage) - require.NoError(err) - expectedSignatureBytes, err := warpSigner.Sign(msg) - require.NoError(err) - require.Equal(expectedSignatureBytes, signature) - }, - }, - "unknown message": { - check: func(require *require.Assertions, b Backend) { - _, err := b.GetMessage(testUnsignedMessage.ID()) - require.ErrorIs(err, database.ErrNotFound) - }, - }, - "invalid message": { - offchainMessages: [][]byte{{1, 2, 3}}, - err: errParsingOffChainMessage, - }, - } { - t.Run(name, func(t *testing.T) { - require := require.New(t) - db := memdb.New() - - messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, test.offchainMessages) - require.ErrorIs(err, test.err) - if test.check != nil { - test.check(require, backend) - } - }) - } -} diff --git a/graft/coreth/warp/client.go b/graft/coreth/warp/client.go deleted file mode 100644 index ce13ae08b344..000000000000 --- a/graft/coreth/warp/client.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "fmt" - - "github.com/ava-labs/libevm/common/hexutil" - - "github.com/ava-labs/avalanchego/graft/coreth/rpc" - "github.com/ava-labs/avalanchego/ids" -) - -var _ Client = (*client)(nil) - -type Client interface { - GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) - GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) - GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) - GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) - GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) -} - -// client implementation for interacting with EVM [chain] -type client struct { - client *rpc.Client -} - -// NewClient returns a Client for interacting with EVM [chain] -func NewClient(uri, chain string) (Client, error) { - innerClient, err := rpc.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", uri, chain)) - if err != nil { - return nil, fmt.Errorf("failed to dial client. err: %w", err) - } - return &client{ - client: innerClient, - }, nil -} - -func (c *client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getMessage", messageID); err != nil { - return nil, fmt.Errorf("call to warp_getMessage failed. err: %w", err) - } - return res, nil -} - -func (c *client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getMessageSignature", messageID); err != nil { - return nil, fmt.Errorf("call to warp_getMessageSignature failed. err: %w", err) - } - return res, nil -} - -func (c *client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetIDStr); err != nil { - return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed. err: %w", err) - } - return res, nil -} - -func (c *client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getBlockSignature", blockID); err != nil { - return nil, fmt.Errorf("call to warp_getBlockSignature failed. err: %w", err) - } - return res, nil -} - -func (c *client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetIDStr); err != nil { - return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed. err: %w", err) - } - return res, nil -} diff --git a/graft/coreth/warp/service.go b/graft/coreth/warp/service.go deleted file mode 100644 index 98b967fe6768..000000000000 --- a/graft/coreth/warp/service.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "errors" - "fmt" - - "github.com/ava-labs/libevm/common/hexutil" - "github.com/ava-labs/libevm/log" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - warpprecompile "github.com/ava-labs/avalanchego/graft/coreth/precompile/contracts/warp" -) - -var errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") - -// API introduces snowman specific functionality to the evm -type API struct { - chainContext *snow.Context - backend Backend - signatureAggregator *acp118.SignatureAggregator -} - -func NewAPI(chainCtx *snow.Context, backend Backend, signatureAggregator *acp118.SignatureAggregator) *API { - return &API{ - backend: backend, - chainContext: chainCtx, - signatureAggregator: signatureAggregator, - } -} - -// GetMessage returns the Warp message associated with a messageID. -func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { - message, err := a.backend.GetMessage(messageID) - if err != nil { - return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) - } - return hexutil.Bytes(message.Bytes()), nil -} - -// GetMessageSignature returns the BLS signature associated with a messageID. -func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - unsignedMessage, err := a.backend.GetMessage(messageID) - if err != nil { - return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) - } - signature, err := a.backend.GetMessageSignature(ctx, unsignedMessage) - if err != nil { - return nil, fmt.Errorf("failed to get signature for message %s with error %w", messageID, err) - } - return signature, nil -} - -// GetBlockSignature returns the BLS signature associated with a blockID. -func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { - signature, err := a.backend.GetBlockSignature(ctx, blockID) - if err != nil { - return nil, fmt.Errorf("failed to get signature for block %s with error %w", blockID, err) - } - return signature, nil -} - -// GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] -func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { - unsignedMessage, err := a.backend.GetMessage(messageID) - if err != nil { - return nil, err - } - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) -} - -// GetBlockAggregateSignature fetches the aggregate signature for the requested [blockID] -func (a *API) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { - blockHashPayload, err := payload.NewHash(blockID) - if err != nil { - return nil, err - } - unsignedMessage, err := warp.NewUnsignedMessage(a.chainContext.NetworkID, a.chainContext.ChainID, blockHashPayload.Bytes()) - if err != nil { - return nil, err - } - - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) -} - -func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetIDStr string) (hexutil.Bytes, error) { - subnetID := a.chainContext.SubnetID - if len(subnetIDStr) > 0 { - sid, err := ids.FromString(subnetIDStr) - if err != nil { - return nil, fmt.Errorf("failed to parse subnetID: %q", subnetIDStr) - } - subnetID = sid - } - validatorState := a.chainContext.ValidatorState - pChainHeight, err := validatorState.GetCurrentHeight(ctx) - if err != nil { - return nil, err - } - - validatorSet, err := validatorState.GetWarpValidatorSet(ctx, pChainHeight, subnetID) - if err != nil { - return nil, fmt.Errorf("failed to get validator set: %w", err) - } - if len(validatorSet.Validators) == 0 { - return nil, fmt.Errorf("%w (SubnetID: %s, Height: %d)", errNoValidators, subnetID, pChainHeight) - } - - log.Debug("Fetching signature", - "sourceSubnetID", subnetID, - "height", pChainHeight, - "numValidators", len(validatorSet.Validators), - "totalWeight", validatorSet.TotalWeight, - ) - warpMessage := &warp.Message{ - UnsignedMessage: *unsignedMessage, - Signature: &warp.BitSetSignature{}, - } - signedMessage, _, _, err := a.signatureAggregator.AggregateSignatures( - ctx, - warpMessage, - nil, - validatorSet.Validators, - quorumNum, - warpprecompile.WarpQuorumDenominator, - ) - if err != nil { - return nil, err - } - // TODO: return the signature and total weight as well to the caller for more complete details - // Need to decide on the best UI for this and write up documentation with the potential - // gotchas that could impact signed messages becoming invalid. - return hexutil.Bytes(signedMessage.Bytes()), nil -} diff --git a/graft/coreth/warp/verifier_backend.go b/graft/coreth/warp/verifier_backend.go deleted file mode 100644 index 8738d1639a8d..000000000000 --- a/graft/coreth/warp/verifier_backend.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "fmt" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -const ( - ParseErrCode = iota + 1 - VerifyErrCode -) - -// Verify verifies the signature of the message -// It also implements the acp118.Verifier interface -func (b *backend) Verify(ctx context.Context, unsignedMessage *avalancheWarp.UnsignedMessage, _ []byte) *common.AppError { - messageID := unsignedMessage.ID() - // Known on-chain messages should be signed - if _, err := b.GetMessage(messageID); err == nil { - return nil - } else if err != database.ErrNotFound { - return &common.AppError{ - Code: ParseErrCode, - Message: fmt.Sprintf("failed to get message %s: %s", messageID, err.Error()), - } - } - - parsed, err := payload.Parse(unsignedMessage.Payload) - if err != nil { - b.stats.IncMessageParseFail() - return &common.AppError{ - Code: ParseErrCode, - Message: "failed to parse payload: " + err.Error(), - } - } - - switch p := parsed.(type) { - case *payload.Hash: - return b.verifyBlockMessage(ctx, p) - default: - b.stats.IncMessageParseFail() - return &common.AppError{ - Code: ParseErrCode, - Message: fmt.Sprintf("unknown payload type: %T", p), - } - } -} - -// verifyBlockMessage returns nil if blockHashPayload contains the ID -// of an accepted block indicating it should be signed by the VM. -func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { - blockID := blockHashPayload.Hash - _, err := b.blockClient.GetAcceptedBlock(ctx, blockID) - if err != nil { - b.stats.IncBlockValidationFail() - return &common.AppError{ - Code: VerifyErrCode, - Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), - } - } - - return nil -} diff --git a/graft/coreth/warp/verifier_backend_test.go b/graft/coreth/warp/verifier_backend_test.go deleted file mode 100644 index b4ad804e25fe..000000000000 --- a/graft/coreth/warp/verifier_backend_test.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/ava-labs/avalanchego/cache" - "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/graft/coreth/warp/warptest" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/proto/pb/sdk" - "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/snowtest" - "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -func TestAddressedCallSignatures(t *testing.T) { - metricstest.WithMetrics(t) - - database := memdb.New() - snowCtx := snowtest.Context(t, snowtest.CChainID) - - offChainPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) - require.NoError(t, err) - offchainMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offChainPayload.Bytes()) - require.NoError(t, err) - offchainSignature, err := snowCtx.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) - err *common.AppError - }{ - "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 := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) - require.NoError(t, err) - signature, err := snowCtx.WarpSigner.Sign(msg) - require.NoError(t, err) - require.NoError(t, backend.AddMessage(msg)) - return msg.Bytes(), signature - }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Zero(t, stats.messageParseFail.Snapshot().Count()) - require.Zero(t, 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.Zero(t, stats.messageParseFail.Snapshot().Count()) - require.Zero(t, 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 := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) - require.NoError(t, err) - return unknownMessage.Bytes(), nil - }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Equal(t, int64(1), stats.messageParseFail.Snapshot().Count()) - require.Zero(t, stats.blockValidationFail.Snapshot().Count()) - }, - err: &common.AppError{Code: ParseErrCode}, - }, - } - - for name, test := range tests { - for _, withCache := range []bool{true, false} { - if withCache { - name += "_with_cache" - } else { - name += "_no_cache" - } - t.Run(name, 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(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, warptest.EmptyBlockClient, database, sigCache, [][]byte{offchainMessage.Bytes()}) - require.NoError(t, err) - handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) - - requestBytes, expectedResponse := test.setup(warpBackend) - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, test.err) - 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.Empty(t, responseBytes, "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()) - } - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.NoError(t, err, "error unmarshalling SignatureResponse") - - require.Equal(t, expectedResponse, response.Signature) - }) - } - } -} - -func TestBlockSignatures(t *testing.T) { - metricstest.WithMetrics(t) - - database := memdb.New() - snowCtx := snowtest.Context(t, snowtest.CChainID) - - 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 := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.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) - err *common.AppError - }{ - "known block": { - setup: func() (request []byte, expectedResponse []byte) { - hashPayload, err := payload.NewHash(knownBlkID) - require.NoError(t, err) - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) - require.NoError(t, err) - signature, err := snowCtx.WarpSigner.Sign(unsignedMessage) - require.NoError(t, err) - return toMessageBytes(knownBlkID), signature - }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Zero(t, stats.blockValidationFail.Snapshot().Count()) - require.Zero(t, 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.Equal(t, int64(1), stats.blockValidationFail.Snapshot().Count()) - require.Zero(t, stats.messageParseFail.Snapshot().Count()) - }, - err: &common.AppError{Code: VerifyErrCode}, - }, - } - - for name, test := range tests { - for _, withCache := range []bool{true, false} { - if withCache { - name += "_with_cache" - } else { - name += "_no_cache" - } - t.Run(name, 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( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - blockClient, - database, - sigCache, - nil, - ) - require.NoError(t, err) - handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) - - requestBytes, expectedResponse := test.setup() - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, test.err) - - 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.Empty(t, responseBytes, "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()) - } - var response sdk.SignatureResponse - err = proto.Unmarshal(responseBytes, &response) - require.NoError(t, err, "error unmarshalling SignatureResponse") - require.Equal(t, expectedResponse, response.Signature) - }) - } - } -} diff --git a/graft/coreth/warp/verifier_stats.go b/graft/coreth/warp/verifier_stats.go deleted file mode 100644 index 8f4ed1718bab..000000000000 --- a/graft/coreth/warp/verifier_stats.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import "github.com/ava-labs/libevm/metrics" - -type verifierStats struct { - messageParseFail metrics.Counter - // BlockRequest metrics - blockValidationFail metrics.Counter -} - -func newVerifierStats() *verifierStats { - return &verifierStats{ - messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), - blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), - } -} - -func (h *verifierStats) IncBlockValidationFail() { - h.blockValidationFail.Inc(1) -} - -func (h *verifierStats) IncMessageParseFail() { - h.messageParseFail.Inc(1) -} diff --git a/graft/coreth/warp/warptest/block_client.go b/graft/coreth/warp/warptest/block_client.go deleted file mode 100644 index 7f98d4f79786..000000000000 --- a/graft/coreth/warp/warptest/block_client.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// warptest exposes common functionality for testing the warp package. -package warptest - -import ( - "context" - "slices" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/consensus/snowman/snowmantest" - "github.com/ava-labs/avalanchego/snow/snowtest" -) - -// EmptyBlockClient returns an error if a block is requested -var EmptyBlockClient BlockClient = MakeBlockClient() - -type BlockClient func(ctx context.Context, blockID ids.ID) (snowman.Block, error) - -func (f BlockClient) GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) { - return f(ctx, blockID) -} - -// MakeBlockClient returns a new BlockClient that returns the provided blocks. -// If a block is requested that isn't part of the provided blocks, an error is -// returned. -func MakeBlockClient(blkIDs ...ids.ID) BlockClient { - return func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if !slices.Contains(blkIDs, blkID) { - return nil, database.ErrNotFound - } - - return &snowmantest.Block{ - Decidable: snowtest.Decidable{ - IDV: blkID, - Status: snowtest.Accepted, - }, - }, nil - } -} diff --git a/graft/subnet-evm/examples/sign-uptime-message/main.go b/graft/subnet-evm/examples/sign-uptime-message/main.go index 2cb9fe0313db..7030e462f4e1 100644 --- a/graft/subnet-evm/examples/sign-uptime-message/main.go +++ b/graft/subnet-evm/examples/sign-uptime-message/main.go @@ -13,13 +13,13 @@ import ( "google.golang.org/protobuf/proto" "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/graft/subnet-evm/warp/messages" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/peer" "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/snow/networking/router" "github.com/ava-labs/avalanchego/utils/compression" + "github.com/ava-labs/avalanchego/vms/evm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/avalanchego/wallet/subnet/primary" @@ -44,7 +44,7 @@ func main() { log.Fatalf("failed to fetch network ID: %s\n", err) } - validatorUptime, err := messages.NewValidatorUptime(validationID, reqUptime) + validatorUptime, err := message.NewValidatorUptime(validationID, reqUptime) if err != nil { log.Fatalf("failed to create validatorUptime message: %s\n", err) } diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index c92027de58de..5ba2c9b1d7e5 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -36,6 +36,7 @@ import ( _ "github.com/ava-labs/libevm/eth/tracers/js" _ "github.com/ava-labs/libevm/eth/tracers/native" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/cache/metercacher" "github.com/ava-labs/avalanchego/codec" @@ -63,7 +64,6 @@ import ( "github.com/ava-labs/avalanchego/graft/subnet-evm/sync/client/stats" "github.com/ava-labs/avalanchego/graft/subnet-evm/sync/handlers" "github.com/ava-labs/avalanchego/graft/subnet-evm/triedb/hashdb" - "github.com/ava-labs/avalanchego/graft/subnet-evm/warp" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -88,6 +88,7 @@ import ( avalancheUtils "github.com/ava-labs/avalanchego/utils" avajson "github.com/ava-labs/avalanchego/utils/json" avalanchegoprometheus "github.com/ava-labs/avalanchego/vms/evm/metrics/prometheus" + "github.com/ava-labs/avalanchego/vms/evm/warp" ethparams "github.com/ava-labs/libevm/params" avalancheRPC "github.com/gorilla/rpc/v2" ) @@ -253,9 +254,13 @@ type VM struct { vmsync.Server vmsync.Client - // Avalanche Warp Messaging backend + // Avalanche Warp Messaging components // Used to serve BLS signatures of warp messages over RPC - warpBackend warp.Backend + warpMsgDB *warp.DB + warpVerifier *warp.Verifier + warpSignatureCache cache.Cacher[ids.ID, []byte] + offchainWarpMessages [][]byte + warpAPI *warp.API // Initialize only sets these if nil so they can be overridden in tests ethTxGossipHandler p2p.Handler @@ -460,15 +465,16 @@ func (vm *VM) Initialize( } // Initialize warp backend - offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) + vm.offchainWarpMessages = make([][]byte, len(vm.config.WarpOffChainMessages)) for i, hexMsg := range vm.config.WarpOffChainMessages { - offchainWarpMessages[i] = []byte(hexMsg) + vm.offchainWarpMessages[i] = []byte(hexMsg) } warpSignatureCache := lru.NewCache[ids.ID, []byte](warpSignatureCacheSize) meteredCache, err := metercacher.New("warp_signature_cache", vm.sdkMetrics, warpSignatureCache) if err != nil { return fmt.Errorf("failed to create warp signature cache: %w", err) } + vm.warpSignatureCache = meteredCache // clear warpdb on initialization if config enabled if vm.config.PruneWarpDB { @@ -477,15 +483,18 @@ func (vm *VM) Initialize( } } - vm.warpBackend, err = warp.NewBackend( - vm.ctx.NetworkID, - vm.ctx.ChainID, + vm.warpMsgDB = warp.NewDB(vm.warpDB) + vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, vm.uptimeTracker) + + // Create warp API (signatureAggregator will be nil until createAPIs) + vm.warpAPI, err = warp.NewAPI( + vm.ctx, + vm.warpMsgDB, vm.ctx.WarpSigner, - vm, - vm.uptimeTracker, - vm.warpDB, - meteredCache, - offchainWarpMessages, + vm.warpVerifier, + vm.warpSignatureCache, + nil, // signatureAggregator is set later in createAPIs + vm.offchainWarpMessages, ) if err != nil { return err @@ -497,7 +506,7 @@ func (vm *VM) Initialize( go vm.ctx.Log.RecoverAndPanic(vm.startContinuousProfiler) // Add p2p warp message warpHandler - warpHandler := acp118.NewCachedHandler(meteredCache, vm.warpBackend, vm.ctx.WarpSigner) + warpHandler := warp.NewHandler(vm.warpSignatureCache, vm.warpVerifier, vm.ctx.WarpSigner) if err = vm.Network.AddHandler(p2p.SignatureRequestHandlerID, warpHandler); err != nil { return err } @@ -1100,25 +1109,23 @@ func (vm *VM) getBlock(_ context.Context, id ids.ID) (snowman.Block, error) { return wrapBlock(ethBlock, vm) } -// GetAcceptedBlock attempts to retrieve block [blkID] from the VM. This method -// only returns accepted blocks. -func (vm *VM) GetAcceptedBlock(ctx context.Context, blkID ids.ID) (snowman.Block, error) { +// HasBlock returns nil if the block is accepted, or an error otherwise. +// Implements warp.BlockStore. +func (vm *VM) HasBlock(ctx context.Context, blkID ids.ID) error { blk, err := vm.GetBlock(ctx, blkID) if err != nil { - return nil, err + return err } - height := blk.Height() - acceptedBlkID, err := vm.GetBlockIDAtHeight(ctx, height) + acceptedBlkID, err := vm.GetBlockIDAtHeight(ctx, blk.Height()) if err != nil { - return nil, err + return err } if acceptedBlkID != blkID { - // The provided block is not accepted. - return nil, database.ErrNotFound + return database.ErrNotFound } - return blk, nil + return nil } // SetPreference sets what the current tail of the chain is @@ -1211,7 +1218,19 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.Network.NewClient(p2p.SignatureRequestHandlerID) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - if err := handler.RegisterName("warp", warp.NewAPI(vm.ctx, vm.warpBackend, signatureAggregator)); err != nil { + warpAPI, err := warp.NewAPI( + vm.ctx, + vm.warpMsgDB, + vm.ctx.WarpSigner, + vm.warpVerifier, + vm.warpSignatureCache, + signatureAggregator, + vm.offchainWarpMessages, + ) + if err != nil { + return nil, err + } + if err := handler.RegisterName("warp", warpAPI); err != nil { return nil, err } enabledAPIs = append(enabledAPIs, "warp") diff --git a/graft/subnet-evm/plugin/evm/vm_warp_test.go b/graft/subnet-evm/plugin/evm/vm_warp_test.go index f6a04ebee773..57069b816966 100644 --- a/graft/subnet-evm/plugin/evm/vm_warp_test.go +++ b/graft/subnet-evm/plugin/evm/vm_warp_test.go @@ -29,7 +29,6 @@ import ( "github.com/ava-labs/avalanchego/graft/subnet-evm/plugin/evm/extension" "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contract" "github.com/ava-labs/avalanchego/graft/subnet-evm/utils" - "github.com/ava-labs/avalanchego/graft/subnet-evm/warp" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -46,6 +45,7 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/chain" "github.com/ava-labs/avalanchego/vms/evm/predicate" + "github.com/ava-labs/avalanchego/vms/evm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" warpcontract "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" @@ -157,17 +157,17 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.NoError(err) // Verify the signature cannot be fetched before the block is accepted - _, err = tvm.vm.warpBackend.GetMessageSignature(t.Context(), unsignedMessage) - require.ErrorIs(err, warp.ErrVerifyWarpMessage) - _, err = tvm.vm.warpBackend.GetBlockSignature(t.Context(), blk.ID()) - require.ErrorIs(err, warp.ErrValidateBlock) + _, err = tvm.vm.warpAPI.GetMessageSignature(t.Context(), unsignedMessage.ID()) + require.ErrorIs(err, warp.ErrMessageNotFound) + _, err = tvm.vm.warpAPI.GetBlockSignature(t.Context(), blk.ID()) + require.ErrorIs(err, warp.ErrBlockNotFound) require.NoError(tvm.vm.SetPreference(t.Context(), blk.ID())) require.NoError(blk.Accept(t.Context())) tvm.vm.blockChain.DrainAcceptorQueue() // Verify the message signature after accepting the block. - rawSignatureBytes, err := tvm.vm.warpBackend.GetMessageSignature(t.Context(), unsignedMessage) + rawSignatureBytes, err := tvm.vm.warpAPI.GetMessageSignature(t.Context(), unsignedMessage.ID()) require.NoError(err) blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes) require.NoError(err) @@ -184,7 +184,7 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.True(bls.Verify(tvm.vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) // Verify the blockID will now be signed by the backend and produces a valid signature. - rawSignatureBytes, err = tvm.vm.warpBackend.GetBlockSignature(t.Context(), blk.ID()) + rawSignatureBytes, err = tvm.vm.warpAPI.GetBlockSignature(t.Context(), blk.ID()) require.NoError(err) blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes) require.NoError(err) @@ -847,14 +847,14 @@ func testSignatureRequestsToVM(t *testing.T, scheme string) { require.NoError(t, err) // Add the known message and get its signature to confirm - require.NoError(t, tvm.vm.warpBackend.AddMessage(knownWarpMessage)) - knownMessageSignature, err := tvm.vm.warpBackend.GetMessageSignature(t.Context(), knownWarpMessage) + require.NoError(t, tvm.vm.warpMsgDB.Add(knownWarpMessage)) + knownMessageSignature, err := tvm.vm.warpAPI.GetMessageSignature(t.Context(), knownWarpMessage.ID()) require.NoError(t, err) // Setup known block lastAcceptedID, err := tvm.vm.LastAccepted(t.Context()) require.NoError(t, err) - knownBlockSignature, err := tvm.vm.warpBackend.GetBlockSignature(t.Context(), lastAcceptedID) + knownBlockSignature, err := tvm.vm.warpAPI.GetBlockSignature(t.Context(), lastAcceptedID) require.NoError(t, err) type testCase struct { @@ -957,9 +957,9 @@ func TestClearWarpDB(t *testing.T) { for _, payload := range payloads { unsignedMsg, err := avalancheWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, payload) require.NoError(t, err) - require.NoError(t, vm.warpBackend.AddMessage(unsignedMsg)) + require.NoError(t, vm.warpMsgDB.Add(unsignedMsg)) // ensure that the message was added - _, err = vm.warpBackend.GetMessageSignature(t.Context(), unsignedMsg) + _, err = vm.warpAPI.GetMessageSignature(t.Context(), unsignedMsg.ID()) require.NoError(t, err) messages = append(messages, unsignedMsg) } @@ -974,7 +974,7 @@ func TestClearWarpDB(t *testing.T) { // check messages are still present for _, message := range messages { - bytes, err := vm.warpBackend.GetMessageSignature(t.Context(), message) + bytes, err := vm.warpAPI.GetMessageSignature(t.Context(), message.ID()) require.NoError(t, err) require.NotEmpty(t, bytes) } @@ -993,7 +993,7 @@ func TestClearWarpDB(t *testing.T) { // ensure all messages have been deleted for _, message := range messages { - _, err := vm.warpBackend.GetMessageSignature(t.Context(), message) - require.ErrorIs(t, err, &commonEng.AppError{Code: warp.ParseErrCode}) + _, err := vm.warpAPI.GetMessageSignature(t.Context(), message.ID()) + require.ErrorIs(t, err, warp.ErrMessageNotFound) } } diff --git a/graft/subnet-evm/plugin/evm/wrapped_block.go b/graft/subnet-evm/plugin/evm/wrapped_block.go index d32326f3eda2..0b5ef4afe8d2 100644 --- a/graft/subnet-evm/plugin/evm/wrapped_block.go +++ b/graft/subnet-evm/plugin/evm/wrapped_block.go @@ -130,7 +130,7 @@ func (b *wrappedBlock) handlePrecompileAccept(rules extras.Rules) error { } acceptCtx := &precompileconfig.AcceptContext{ SnowCtx: b.vm.ctx, - Warp: b.vm.warpBackend, + Warp: b.vm.warpMsgDB, } for _, receipt := range receipts { for logIdx, log := range receipt.Logs { diff --git a/graft/subnet-evm/precompile/contracts/warp/config.go b/graft/subnet-evm/precompile/contracts/warp/config.go index 88ac407e0fc3..7a398392abb0 100644 --- a/graft/subnet-evm/precompile/contracts/warp/config.go +++ b/graft/subnet-evm/precompile/contracts/warp/config.go @@ -131,7 +131,7 @@ func (*Config) Accept(acceptCtx *precompileconfig.AcceptContext, blockHash commo "logData", common.Bytes2Hex(logData), "warpMessageID", unsignedMessage.ID(), ) - if err := acceptCtx.Warp.AddMessage(unsignedMessage); err != nil { + if err := acceptCtx.Warp.Add(unsignedMessage); err != nil { return fmt.Errorf("failed to add warp message during accept (TxHash: %s, LogIndex: %d): %w", txHash, logIndex, err) } return nil diff --git a/graft/subnet-evm/precompile/precompileconfig/config.go b/graft/subnet-evm/precompile/precompileconfig/config.go index 02196b127d22..3485622d89f1 100644 --- a/graft/subnet-evm/precompile/precompileconfig/config.go +++ b/graft/subnet-evm/precompile/precompileconfig/config.go @@ -55,7 +55,7 @@ type Predicater interface { } type WarpMessageWriter interface { - AddMessage(unsignedMessage *warp.UnsignedMessage) error + Add(unsignedMessage *warp.UnsignedMessage) error } // AcceptContext defines the context passed in to a precompileconfig's Accepter diff --git a/graft/subnet-evm/tests/warp/warp_test.go b/graft/subnet-evm/tests/warp/warp_test.go index e83fe5ed9299..731a68527440 100644 --- a/graft/subnet-evm/tests/warp/warp_test.go +++ b/graft/subnet-evm/tests/warp/warp_test.go @@ -28,7 +28,7 @@ import ( "github.com/ava-labs/avalanchego/graft/subnet-evm/cmd/simulator/txs" "github.com/ava-labs/avalanchego/graft/subnet-evm/ethclient" "github.com/ava-labs/avalanchego/graft/subnet-evm/params" - "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" + warpContract "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp/warpbindings" "github.com/ava-labs/avalanchego/graft/subnet-evm/tests" "github.com/ava-labs/avalanchego/graft/subnet-evm/tests/utils" @@ -42,7 +42,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/api" warptestbindings "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp/warptest/bindings" - warpBackend "github.com/ava-labs/avalanchego/graft/subnet-evm/warp" + "github.com/ava-labs/avalanchego/vms/evm/warp" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ethereum "github.com/ava-labs/libevm" @@ -333,13 +333,13 @@ func (w *warpTest) sendWarpMessageTx(ctx context.Context, client ethclient.Clien startingNonce, err := client.NonceAt(ctx, w.sendingSubnetFundedAddress, nil) require.NoError(err) - packedInput, err := warp.PackSendWarpMessage(testPayload) + packedInput, err := warpContract.PackSendWarpMessage(testPayload) require.NoError(err) tx := types.NewTx(&types.DynamicFeeTx{ ChainID: w.sendingSubnetChainID, Nonce: startingNonce, - To: &warp.Module.Address, + To: &warpContract.Module.Address, Gas: 200_000, GasFeeCap: big.NewInt(225 * params.GWei), GasTipCap: big.NewInt(params.GWei), @@ -376,7 +376,7 @@ func verifyAndExtractWarpMessage( require := require.New(ginkgo.GinkgoT()) log.Info("Filtering SendWarpMessage events using binding") - warpFilterer, err := warpbindings.NewIWarpMessengerFilterer(warp.Module.Address, client) + warpFilterer, err := warpbindings.NewIWarpMessengerFilterer(warpContract.Module.Address, client) require.NoError(err) iter, err := warpFilterer.FilterSendWarpMessage( @@ -415,9 +415,9 @@ func (w *warpTest) aggregateSignaturesViaAPI() { tc := e2e.NewTestContext() ctx := tc.DefaultContext() - warpAPIs := make(map[ids.NodeID]warpBackend.Client, len(w.sendingSubnetURIs)) + warpAPIs := make(map[ids.NodeID]*warp.Client, len(w.sendingSubnetURIs)) for _, uri := range w.sendingSubnetURIs { - client, err := warpBackend.NewClient(uri, w.sendingSubnet.BlockchainID.String()) + client, err := warp.NewClient(uri, w.sendingSubnet.BlockchainID.String()) require.NoError(err) infoClient := info.NewClient(uri) @@ -446,7 +446,7 @@ func (w *warpTest) aggregateSignaturesViaAPI() { require.NotEmpty(warpValidators) // Verify that the signature aggregation matches the results of manually constructing the warp message - client, err := warpBackend.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) + client, err := warp.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) require.NoError(err) log.Info("Fetching addressed call aggregate signature via p2p API") @@ -455,26 +455,26 @@ func (w *warpTest) aggregateSignaturesViaAPI() { subnetIDStr = w.receivingSubnet.SubnetID.String() } - signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, w.addressedCallUnsignedMessage.ID(), warp.WarpQuorumDenominator, subnetIDStr) + signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, w.addressedCallUnsignedMessage.ID(), warpContract.WarpQuorumDenominator, subnetIDStr) require.NoError(err) parsedWarpMessage, err := avalancheWarp.ParseMessage(signedWarpMessageBytes) require.NoError(err) numSigners, err := parsedWarpMessage.Signature.NumSigners() require.NoError(err) require.Len(warpValidators.Validators, numSigners) - err = parsedWarpMessage.Signature.Verify(&parsedWarpMessage.UnsignedMessage, w.networkID, warpValidators, warp.WarpQuorumDenominator, warp.WarpQuorumDenominator) + err = parsedWarpMessage.Signature.Verify(&parsedWarpMessage.UnsignedMessage, w.networkID, warpValidators, warpContract.WarpQuorumDenominator, warpContract.WarpQuorumDenominator) require.NoError(err) w.addressedCallSignedMessage = parsedWarpMessage log.Info("Fetching block payload aggregate signature via p2p API") - signedWarpBlockBytes, err := client.GetBlockAggregateSignature(ctx, w.blockID, warp.WarpQuorumDenominator, subnetIDStr) + signedWarpBlockBytes, err := client.GetBlockAggregateSignature(ctx, w.blockID, warpContract.WarpQuorumDenominator, subnetIDStr) require.NoError(err) parsedWarpBlockMessage, err := avalancheWarp.ParseMessage(signedWarpBlockBytes) require.NoError(err) numSigners, err = parsedWarpBlockMessage.Signature.NumSigners() require.NoError(err) require.Len(warpValidators.Validators, numSigners) - err = parsedWarpBlockMessage.Signature.Verify(&parsedWarpBlockMessage.UnsignedMessage, w.networkID, warpValidators, warp.WarpQuorumDenominator, warp.WarpQuorumDenominator) + err = parsedWarpBlockMessage.Signature.Verify(&parsedWarpBlockMessage.UnsignedMessage, w.networkID, warpValidators, warpContract.WarpQuorumDenominator, warpContract.WarpQuorumDenominator) require.NoError(err) w.blockPayloadSignedMessage = parsedWarpBlockMessage } @@ -494,12 +494,12 @@ func (w *warpTest) deliverAddressedCallToReceivingSubnet() { nonce, err := client.NonceAt(ctx, w.receivingSubnetFundedAddress, nil) require.NoError(err) - packedInput, err := warp.PackGetVerifiedWarpMessage(0) + packedInput, err := warpContract.PackGetVerifiedWarpMessage(0) require.NoError(err) tx := types.NewTx(&types.DynamicFeeTx{ ChainID: w.receivingSubnetChainID, Nonce: nonce, - To: &warp.Module.Address, + To: &warpContract.Module.Address, Gas: 5_000_000, GasFeeCap: big.NewInt(225 * params.GWei), GasTipCap: big.NewInt(params.GWei), @@ -507,7 +507,7 @@ func (w *warpTest) deliverAddressedCallToReceivingSubnet() { Data: packedInput, AccessList: types.AccessList{ { - Address: warp.ContractAddress, + Address: warpContract.ContractAddress, StorageKeys: predicate.New(w.addressedCallSignedMessage.Bytes()), }, }, @@ -528,7 +528,7 @@ func (w *warpTest) deliverAddressedCallToReceivingSubnet() { log.Info("Fetching relevant warp logs and receipts from new block") logs, err := client.FilterLogs(ctx, ethereum.FilterQuery{ BlockHash: &blockHash, - Addresses: []common.Address{warp.Module.Address}, + Addresses: []common.Address{warpContract.Module.Address}, }) require.NoError(err) require.Empty(logs) @@ -552,12 +552,12 @@ func (w *warpTest) deliverBlockHashPayload() { nonce, err := client.NonceAt(ctx, w.receivingSubnetFundedAddress, nil) require.NoError(err) - packedInput, err := warp.PackGetVerifiedWarpBlockHash(0) + packedInput, err := warpContract.PackGetVerifiedWarpBlockHash(0) require.NoError(err) tx := types.NewTx(&types.DynamicFeeTx{ ChainID: w.receivingSubnetChainID, Nonce: nonce, - To: &warp.Module.Address, + To: &warpContract.Module.Address, Gas: 5_000_000, GasFeeCap: big.NewInt(225 * params.GWei), GasTipCap: big.NewInt(params.GWei), @@ -565,7 +565,7 @@ func (w *warpTest) deliverBlockHashPayload() { Data: packedInput, AccessList: types.AccessList{ { - Address: warp.ContractAddress, + Address: warpContract.ContractAddress, StorageKeys: predicate.New(w.blockPayloadSignedMessage.Bytes()), }, }, @@ -585,7 +585,7 @@ func (w *warpTest) deliverBlockHashPayload() { log.Info("Fetching relevant warp logs and receipts from new block") logs, err := client.FilterLogs(ctx, ethereum.FilterQuery{ BlockHash: &blockHash, - Addresses: []common.Address{warp.Module.Address}, + Addresses: []common.Address{warpContract.Module.Address}, }) require.NoError(err) require.Empty(logs) @@ -606,7 +606,7 @@ func (w *warpTest) warpBindingsTest() { require.NoError(err) auth.Context = ctx - proxyAddr, deployTx, warpTestContract, err := warptestbindings.DeployWarpTest(auth, client, warp.Module.Address) + proxyAddr, deployTx, warpTestContract, err := warptestbindings.DeployWarpTest(auth, client, warpContract.Module.Address) require.NoError(err) log.Info("Waiting for WarpTest deployment", "txHash", deployTx.Hash(), "proxyAddr", proxyAddr) @@ -682,7 +682,7 @@ func (w *warpTest) warpLoad() { log.Info("Subscribing to warp send events on sending subnet") logs := make(chan types.Log, numWorkers*int(txsPerWorker)) sub, err := sendingClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{ - Addresses: []common.Address{warp.Module.Address}, + Addresses: []common.Address{warpContract.Module.Address}, }, logs) require.NoError(err) defer func() { @@ -692,14 +692,14 @@ func (w *warpTest) warpLoad() { log.Info("Generating tx sequence to send warp messages...") warpSendSequences, err := txs.GenerateTxSequences(ctx, func(key *ecdsa.PrivateKey, nonce uint64) (*types.Transaction, error) { - data, err := warp.PackSendWarpMessage([]byte(fmt.Sprintf("Jets %d-%d Dolphins", key.X.Int64(), nonce))) + data, err := warpContract.PackSendWarpMessage([]byte(fmt.Sprintf("Jets %d-%d Dolphins", key.X.Int64(), nonce))) if err != nil { return nil, err } tx := types.NewTx(&types.DynamicFeeTx{ ChainID: w.sendingSubnetChainID, Nonce: nonce, - To: &warp.Module.Address, + To: &warpContract.Module.Address, Gas: 200_000, GasFeeCap: big.NewInt(225 * params.GWei), GasTipCap: big.NewInt(params.GWei), @@ -715,7 +715,7 @@ func (w *warpTest) warpLoad() { require.NoError(warpSendLoader.Execute(ctx)) require.NoError(warpSendLoader.ConfirmReachedTip(ctx)) - warpClient, err := warpBackend.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) + warpClient, err := warp.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) require.NoError(err) subnetIDStr := "" if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID { @@ -727,25 +727,25 @@ func (w *warpTest) warpLoad() { // Wait for the next warp send log warpLog := <-logs - unsignedMessage, err := warp.UnpackSendWarpEventDataToMessage(warpLog.Data) + unsignedMessage, err := warpContract.UnpackSendWarpEventDataToMessage(warpLog.Data) if err != nil { return nil, err } log.Info("Fetching addressed call aggregate signature via p2p API") - signedWarpMessageBytes, err := warpClient.GetMessageAggregateSignature(ctx, unsignedMessage.ID(), warp.WarpDefaultQuorumNumerator, subnetIDStr) + signedWarpMessageBytes, err := warpClient.GetMessageAggregateSignature(ctx, unsignedMessage.ID(), warpContract.WarpDefaultQuorumNumerator, subnetIDStr) if err != nil { return nil, err } - packedInput, err := warp.PackGetVerifiedWarpMessage(0) + packedInput, err := warpContract.PackGetVerifiedWarpMessage(0) if err != nil { return nil, err } tx := types.NewTx(&types.DynamicFeeTx{ ChainID: w.receivingSubnetChainID, Nonce: nonce, - To: &warp.Module.Address, + To: &warpContract.Module.Address, Gas: 5_000_000, GasFeeCap: big.NewInt(225 * params.GWei), GasTipCap: big.NewInt(params.GWei), @@ -753,7 +753,7 @@ func (w *warpTest) warpLoad() { Data: packedInput, AccessList: types.AccessList{ { - Address: warp.ContractAddress, + Address: warpContract.ContractAddress, StorageKeys: predicate.New(signedWarpMessageBytes), }, }, diff --git a/graft/subnet-evm/warp/backend.go b/graft/subnet-evm/warp/backend.go deleted file mode 100644 index b205e7165bf9..000000000000 --- a/graft/subnet-evm/warp/backend.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "errors" - "fmt" - - "github.com/ava-labs/libevm/log" - - "github.com/ava-labs/avalanchego/cache" - "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -var ( - _ Backend = (*backend)(nil) - ErrValidateBlock = errors.New("failed to validate block message") - ErrVerifyWarpMessage = errors.New("failed to verify warp message") - errParsingOffChainMessage = errors.New("failed to parse off-chain message") - - messageCacheSize = 500 -) - -type BlockClient interface { - GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.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 *avalancheWarp.UnsignedMessage) error - - // GetMessageSignature validates the message and returns the signature of the requested message. - GetMessageSignature(ctx context.Context, message *avalancheWarp.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) (*avalancheWarp.UnsignedMessage, error) - - acp118.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 avalancheWarp.Signer - blockClient BlockClient - uptimeTracker *uptimetracker.UptimeTracker - signatureCache cache.Cacher[ids.ID, []byte] - messageCache *lru.Cache[ids.ID, *avalancheWarp.UnsignedMessage] - offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.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 avalancheWarp.Signer, - blockClient BlockClient, - uptimeTracker *uptimetracker.UptimeTracker, - 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, - uptimeTracker: uptimeTracker, - messageCache: lru.NewCache[ids.ID, *avalancheWarp.UnsignedMessage](messageCacheSize), - stats: newVerifierStats(), - offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), - } - return b, b.initOffChainMessages(offchainMessages) -} - -func (b *backend) initOffChainMessages(offchainMessages [][]byte) error { - for i, offchainMsg := range offchainMessages { - unsignedMsg, err := avalancheWarp.ParseUnsignedMessage(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", avalancheWarp.ErrWrongNetworkID, i) - } - - if unsignedMsg.SourceChainID != b.sourceChainID { - return fmt.Errorf("%w at index %d", avalancheWarp.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 *avalancheWarp.UnsignedMessage) 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 *avalancheWarp.UnsignedMessage) ([]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("%w: %w", ErrVerifyWarpMessage, 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 := avalancheWarp.NewUnsignedMessage(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("%w: %w", ErrValidateBlock, 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) (*avalancheWarp.UnsignedMessage, 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 := avalancheWarp.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 *avalancheWarp.UnsignedMessage) ([]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 -} diff --git a/graft/subnet-evm/warp/backend_test.go b/graft/subnet-evm/warp/backend_test.go deleted file mode 100644 index 00343d9d3552..000000000000 --- a/graft/subnet-evm/warp/backend_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/graft/subnet-evm/warp/warptest" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -var ( - networkID uint32 = 54321 - sourceChainID = ids.GenerateTestID() - testSourceAddress = utils.RandomBytes(20) - testPayload = []byte("test") - testUnsignedMessage *avalancheWarp.UnsignedMessage -) - -func init() { - testAddressedCallPayload, err := payload.NewAddressedCall(testSourceAddress, testPayload) - if err != nil { - panic(err) - } - testUnsignedMessage, err = avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, testAddressedCallPayload.Bytes()) - if err != nil { - panic(err) - } -} - -func TestAddAndGetValidMessage(t *testing.T) { - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) - require.NoError(t, err) - - // Add testUnsignedMessage to the warp backend - require.NoError(t, backend.AddMessage(testUnsignedMessage)) - - // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(t.Context(), testUnsignedMessage) - require.NoError(t, err) - - expectedSig, err := warpSigner.Sign(testUnsignedMessage) - require.NoError(t, err) - require.Equal(t, expectedSig, signature) -} - -func TestAddAndGetUnknownMessage(t *testing.T) { - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) - require.NoError(t, err) - - // Try getting a signature for a message that was not added. - _, err = backend.GetMessageSignature(t.Context(), testUnsignedMessage) - require.ErrorIs(t, err, ErrVerifyWarpMessage) -} - -func TestGetBlockSignature(t *testing.T) { - require := require.New(t) - - blkID := ids.GenerateTestID() - blockClient := warptest.MakeBlockClient(blkID) - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - messageSignatureCache := lru.NewCache[ids.ID, []byte](500) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, nil, db, messageSignatureCache, nil) - require.NoError(err) - - blockHashPayload, err := payload.NewHash(blkID) - require.NoError(err) - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) - require.NoError(err) - expectedSig, err := warpSigner.Sign(unsignedMessage) - require.NoError(err) - - signature, err := backend.GetBlockSignature(t.Context(), blkID) - require.NoError(err) - require.Equal(expectedSig, signature) - - _, err = backend.GetBlockSignature(t.Context(), ids.GenerateTestID()) - require.ErrorIs(err, ErrValidateBlock) -} - -func TestZeroSizedCache(t *testing.T) { - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - - // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. - messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, nil) - require.NoError(t, err) - - // Add testUnsignedMessage to the warp backend - require.NoError(t, backend.AddMessage(testUnsignedMessage)) - - // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(t.Context(), testUnsignedMessage) - require.NoError(t, err) - - expectedSig, err := warpSigner.Sign(testUnsignedMessage) - require.NoError(t, err) - require.Equal(t, expectedSig, signature) -} - -func TestOffChainMessages(t *testing.T) { - type test struct { - offchainMessages [][]byte - check func(require *require.Assertions, b Backend) - err error - } - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - - for name, test := range map[string]test{ - "no offchain messages": {}, - "single off-chain message": { - offchainMessages: [][]byte{ - testUnsignedMessage.Bytes(), - }, - check: func(require *require.Assertions, b Backend) { - msg, err := b.GetMessage(testUnsignedMessage.ID()) - require.NoError(err) - require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - - signature, err := b.GetMessageSignature(t.Context(), testUnsignedMessage) - require.NoError(err) - expectedSignatureBytes, err := warpSigner.Sign(msg) - require.NoError(err) - require.Equal(expectedSignatureBytes, signature) - }, - }, - "unknown message": { - check: func(require *require.Assertions, b Backend) { - _, err := b.GetMessage(testUnsignedMessage.ID()) - require.ErrorIs(err, database.ErrNotFound) - }, - }, - "invalid message": { - offchainMessages: [][]byte{{1, 2, 3}}, - err: errParsingOffChainMessage, - }, - } { - t.Run(name, func(t *testing.T) { - require := require.New(t) - db := memdb.New() - - messageSignatureCache := lru.NewCache[ids.ID, []byte](0) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, nil, db, messageSignatureCache, test.offchainMessages) - require.ErrorIs(err, test.err) - if test.check != nil { - test.check(require, backend) - } - }) - } -} diff --git a/graft/subnet-evm/warp/client.go b/graft/subnet-evm/warp/client.go deleted file mode 100644 index c1e49e34b1ac..000000000000 --- a/graft/subnet-evm/warp/client.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "fmt" - - "github.com/ava-labs/libevm/common/hexutil" - - "github.com/ava-labs/avalanchego/graft/subnet-evm/rpc" - "github.com/ava-labs/avalanchego/ids" -) - -var _ Client = (*client)(nil) - -type Client interface { - GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) - GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) - GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) - GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) - GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) -} - -// client implementation for interacting with EVM [chain] -type client struct { - client *rpc.Client -} - -// NewClient returns a Client for interacting with EVM [chain] -func NewClient(uri, chain string) (Client, error) { - innerClient, err := rpc.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", uri, chain)) - if err != nil { - return nil, fmt.Errorf("failed to dial client. err: %w", err) - } - return &client{ - client: innerClient, - }, nil -} - -func (c *client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getMessage", messageID); err != nil { - return nil, fmt.Errorf("call to warp_getMessage failed. err: %w", err) - } - return res, nil -} - -func (c *client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getMessageSignature", messageID); err != nil { - return nil, fmt.Errorf("call to warp_getMessageSignature failed. err: %w", err) - } - return res, nil -} - -func (c *client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetIDStr); err != nil { - return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed. err: %w", err) - } - return res, nil -} - -func (c *client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getBlockSignature", blockID); err != nil { - return nil, fmt.Errorf("call to warp_getBlockSignature failed. err: %w", err) - } - return res, nil -} - -func (c *client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { - var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetIDStr); err != nil { - return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed. err: %w", err) - } - return res, nil -} diff --git a/graft/subnet-evm/warp/messages/codec.go b/graft/subnet-evm/warp/messages/codec.go deleted file mode 100644 index 968601af39fb..000000000000 --- a/graft/subnet-evm/warp/messages/codec.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package messages - -import ( - "errors" - - "github.com/ava-labs/avalanchego/codec" - "github.com/ava-labs/avalanchego/codec/linearcodec" - "github.com/ava-labs/avalanchego/utils/units" -) - -const ( - CodecVersion = 0 - - MaxMessageSize = 24 * units.KiB -) - -var Codec codec.Manager - -func init() { - Codec = codec.NewManager(MaxMessageSize) - lc := linearcodec.NewDefault() - - err := errors.Join( - lc.RegisterType(&ValidatorUptime{}), - Codec.RegisterCodec(CodecVersion, lc), - ) - if err != nil { - panic(err) - } -} diff --git a/graft/subnet-evm/warp/messages/payload.go b/graft/subnet-evm/warp/messages/payload.go deleted file mode 100644 index 843c7bbf79be..000000000000 --- a/graft/subnet-evm/warp/messages/payload.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package messages - -import ( - "errors" - "fmt" -) - -var errWrongType = errors.New("wrong payload type") - -// Payload provides a common interface for all payloads implemented by this -// package. -type Payload interface { - // Bytes returns the binary representation of this payload. - Bytes() []byte - - // initialize the payload with the provided binary representation. - initialize(b []byte) -} - -func Parse(bytes []byte) (Payload, error) { - var payload Payload - if _, err := Codec.Unmarshal(bytes, &payload); err != nil { - return nil, err - } - payload.initialize(bytes) - return payload, nil -} - -func initialize(p Payload) error { - bytes, err := Codec.Marshal(CodecVersion, &p) - if err != nil { - return fmt.Errorf("couldn't marshal %T payload: %w", p, err) - } - p.initialize(bytes) - return nil -} diff --git a/graft/subnet-evm/warp/messages/validator_uptime.go b/graft/subnet-evm/warp/messages/validator_uptime.go deleted file mode 100644 index 941029868401..000000000000 --- a/graft/subnet-evm/warp/messages/validator_uptime.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package messages - -import ( - "fmt" - - "github.com/ava-labs/avalanchego/ids" -) - -// ValidatorUptime is signed when the ValidationID is known and the validator -// has been up for TotalUptime seconds. -type ValidatorUptime struct { - ValidationID ids.ID `serialize:"true"` - TotalUptime uint64 `serialize:"true"` // in seconds - - bytes []byte -} - -// NewValidatorUptime creates a new *ValidatorUptime and initializes it. -func NewValidatorUptime(validationID ids.ID, totalUptime uint64) (*ValidatorUptime, error) { - bhp := &ValidatorUptime{ - ValidationID: validationID, - TotalUptime: totalUptime, - } - return bhp, initialize(bhp) -} - -// ParseValidatorUptime converts a slice of bytes into an initialized ValidatorUptime. -func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { - payloadIntf, err := Parse(b) - if err != nil { - return nil, err - } - payload, ok := payloadIntf.(*ValidatorUptime) - if !ok { - return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf) - } - return payload, nil -} - -// Bytes returns the binary representation of this payload. It assumes that the -// payload is initialized from either NewValidatorUptime or Parse. -func (b *ValidatorUptime) Bytes() []byte { - return b.bytes -} - -func (b *ValidatorUptime) initialize(bytes []byte) { - b.bytes = bytes -} diff --git a/graft/subnet-evm/warp/service.go b/graft/subnet-evm/warp/service.go deleted file mode 100644 index 07a444f208c4..000000000000 --- a/graft/subnet-evm/warp/service.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "errors" - "fmt" - - "github.com/ava-labs/libevm/common/hexutil" - "github.com/ava-labs/libevm/log" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - warpprecompile "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" -) - -var errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") - -// API introduces snowman specific functionality to the evm -type API struct { - chainContext *snow.Context - backend Backend - signatureAggregator *acp118.SignatureAggregator -} - -func NewAPI(chainCtx *snow.Context, backend Backend, signatureAggregator *acp118.SignatureAggregator) *API { - return &API{ - backend: backend, - chainContext: chainCtx, - signatureAggregator: signatureAggregator, - } -} - -// GetMessage returns the Warp message associated with a messageID. -func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { - message, err := a.backend.GetMessage(messageID) - if err != nil { - return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) - } - return hexutil.Bytes(message.Bytes()), nil -} - -// GetMessageSignature returns the BLS signature associated with a messageID. -func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - unsignedMessage, err := a.backend.GetMessage(messageID) - if err != nil { - return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) - } - signature, err := a.backend.GetMessageSignature(ctx, unsignedMessage) - if err != nil { - return nil, fmt.Errorf("failed to get signature for message %s with error %w", messageID, err) - } - return signature, nil -} - -// GetBlockSignature returns the BLS signature associated with a blockID. -func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { - signature, err := a.backend.GetBlockSignature(ctx, blockID) - if err != nil { - return nil, fmt.Errorf("failed to get signature for block %s with error %w", blockID, err) - } - return signature, nil -} - -// GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] -func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { - unsignedMessage, err := a.backend.GetMessage(messageID) - if err != nil { - return nil, err - } - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) -} - -// GetBlockAggregateSignature fetches the aggregate signature for the requested [blockID] -func (a *API) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { - blockHashPayload, err := payload.NewHash(blockID) - if err != nil { - return nil, err - } - unsignedMessage, err := warp.NewUnsignedMessage(a.chainContext.NetworkID, a.chainContext.ChainID, blockHashPayload.Bytes()) - if err != nil { - return nil, err - } - - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) -} - -func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetIDStr string) (hexutil.Bytes, error) { - subnetID := a.chainContext.SubnetID - if len(subnetIDStr) > 0 { - sid, err := ids.FromString(subnetIDStr) - if err != nil { - return nil, fmt.Errorf("failed to parse subnetID: %q", subnetIDStr) - } - subnetID = sid - } - validatorState := a.chainContext.ValidatorState - pChainHeight, err := validatorState.GetCurrentHeight(ctx) - if err != nil { - return nil, err - } - - validatorSet, err := validatorState.GetWarpValidatorSet(ctx, pChainHeight, subnetID) - if err != nil { - return nil, fmt.Errorf("failed to get validator set: %w", err) - } - if len(validatorSet.Validators) == 0 { - return nil, fmt.Errorf("%w (SubnetID: %s, Height: %d)", errNoValidators, subnetID, pChainHeight) - } - - log.Debug("Fetching signature", - "sourceSubnetID", subnetID, - "height", pChainHeight, - "numValidators", len(validatorSet.Validators), - "totalWeight", validatorSet.TotalWeight, - ) - warpMessage := &warp.Message{ - UnsignedMessage: *unsignedMessage, - Signature: &warp.BitSetSignature{}, - } - signedMessage, _, _, err := a.signatureAggregator.AggregateSignatures( - ctx, - warpMessage, - nil, - validatorSet.Validators, - quorumNum, - warpprecompile.WarpQuorumDenominator, - ) - if err != nil { - return nil, err - } - // TODO: return the signature and total weight as well to the caller for more complete details - // Need to decide on the best UI for this and write up documentation with the potential - // gotchas that could impact signed messages becoming invalid. - return hexutil.Bytes(signedMessage.Bytes()), nil -} diff --git a/graft/subnet-evm/warp/verifier_backend.go b/graft/subnet-evm/warp/verifier_backend.go deleted file mode 100644 index 481597e9988a..000000000000 --- a/graft/subnet-evm/warp/verifier_backend.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "fmt" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/graft/subnet-evm/warp/messages" - "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -const ( - ParseErrCode = iota + 1 - VerifyErrCode -) - -// Verify verifies the signature of the message -// It also implements the acp118.Verifier interface -func (b *backend) Verify(ctx context.Context, unsignedMessage *avalancheWarp.UnsignedMessage, _ []byte) *common.AppError { - messageID := unsignedMessage.ID() - // Known on-chain messages should be signed - if _, err := b.GetMessage(messageID); err == nil { - return nil - } else if err != database.ErrNotFound { - return &common.AppError{ - Code: ParseErrCode, - Message: fmt.Sprintf("failed to get message %s: %s", messageID, err.Error()), - } - } - - parsed, err := payload.Parse(unsignedMessage.Payload) - if err != nil { - b.stats.IncMessageParseFail() - return &common.AppError{ - Code: ParseErrCode, - Message: "failed to parse payload: " + err.Error(), - } - } - - switch p := parsed.(type) { - case *payload.AddressedCall: - return b.verifyOffchainAddressedCall(p) - case *payload.Hash: - return b.verifyBlockMessage(ctx, p) - default: - b.stats.IncMessageParseFail() - return &common.AppError{ - Code: ParseErrCode, - Message: fmt.Sprintf("unknown payload type: %T", p), - } - } -} - -// verifyBlockMessage returns nil if blockHashPayload contains the ID -// of an accepted block indicating it should be signed by the VM. -func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { - blockID := blockHashPayload.Hash - _, err := b.blockClient.GetAcceptedBlock(ctx, blockID) - if err != nil { - b.stats.IncBlockValidationFail() - return &common.AppError{ - Code: VerifyErrCode, - Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), - } - } - - return nil -} - -// verifyOffchainAddressedCall verifies the addressed call message -func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { - // Further, parse the payload to see if it is a known type. - parsed, err := messages.Parse(addressedCall.Payload) - if err != nil { - b.stats.IncMessageParseFail() - return &common.AppError{ - Code: ParseErrCode, - Message: "failed to parse addressed call message: " + err.Error(), - } - } - - if len(addressedCall.SourceAddress) != 0 { - return &common.AppError{ - Code: VerifyErrCode, - Message: "source address should be empty for offchain addressed messages", - } - } - - switch p := parsed.(type) { - case *messages.ValidatorUptime: - if err := b.verifyUptimeMessage(p); err != nil { - b.stats.IncUptimeValidationFail() - return err - } - default: - b.stats.IncMessageParseFail() - return &common.AppError{ - Code: ParseErrCode, - Message: fmt.Sprintf("unknown message type: %T", p), - } - } - - return nil -} - -func (b *backend) verifyUptimeMessage(uptimeMsg *messages.ValidatorUptime) *common.AppError { - currentUptime, _, err := b.uptimeTracker.GetUptime(uptimeMsg.ValidationID) - if err != nil { - return &common.AppError{ - Code: VerifyErrCode, - Message: "failed to get uptime: " + err.Error(), - } - } - - currentUptimeSeconds := uint64(currentUptime.Seconds()) - // verify the current uptime against the total uptime in the message - if currentUptimeSeconds < uptimeMsg.TotalUptime { - return &common.AppError{ - Code: VerifyErrCode, - Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for validationID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, uptimeMsg.ValidationID), - } - } - - return nil -} diff --git a/graft/subnet-evm/warp/verifier_backend_test.go b/graft/subnet-evm/warp/verifier_backend_test.go deleted file mode 100644 index a157e64a3bbc..000000000000 --- a/graft/subnet-evm/warp/verifier_backend_test.go +++ /dev/null @@ -1,366 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/ava-labs/avalanchego/cache" - "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/graft/subnet-evm/utils/utilstest" - "github.com/ava-labs/avalanchego/graft/subnet-evm/warp/messages" - "github.com/ava-labs/avalanchego/graft/subnet-evm/warp/warptest" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/proto/pb/sdk" - "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/validators" - "github.com/ava-labs/avalanchego/snow/validators/validatorstest" - "github.com/ava-labs/avalanchego/utils/timer/mockable" - "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" - "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -func TestAddressedCallSignatures(t *testing.T) { - metricstest.WithMetrics(t) - - database := memdb.New() - snowCtx := utilstest.NewTestSnowContext(t) - - offChainPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) - require.NoError(t, err) - offchainMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offChainPayload.Bytes()) - require.NoError(t, err) - offchainSignature, err := snowCtx.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) - err *common.AppError - }{ - "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 := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) - require.NoError(t, err) - signature, err := snowCtx.WarpSigner.Sign(msg) - require.NoError(t, err) - require.NoError(t, backend.AddMessage(msg)) - return msg.Bytes(), signature - }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Zero(t, stats.messageParseFail.Snapshot().Count()) - require.Zero(t, 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.Zero(t, stats.messageParseFail.Snapshot().Count()) - require.Zero(t, 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 := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) - require.NoError(t, err) - return unknownMessage.Bytes(), nil - }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Equal(t, int64(1), stats.messageParseFail.Snapshot().Count()) - require.Zero(t, stats.blockValidationFail.Snapshot().Count()) - }, - err: &common.AppError{Code: ParseErrCode}, - }, - } - - for name, test := range tests { - for _, withCache := range []bool{true, false} { - if withCache { - name += "_with_cache" - } else { - name += "_no_cache" - } - t.Run(name, 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( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - warptest.EmptyBlockClient, - nil, - database, - sigCache, - [][]byte{offchainMessage.Bytes()}, - ) - require.NoError(t, err) - handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) - - requestBytes, expectedResponse := test.setup(warpBackend) - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, test.err) - 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.Empty(t, responseBytes, "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()) - } - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.NoError(t, err, "error unmarshalling SignatureResponse") - - require.Equal(t, expectedResponse, response.Signature) - }) - } - } -} - -func TestBlockSignatures(t *testing.T) { - metricstest.WithMetrics(t) - - database := memdb.New() - snowCtx := utilstest.NewTestSnowContext(t) - - knownBlkID := ids.GenerateTestID() - blockClient := warptest.MakeBlockClient(knownBlkID) - - toMessageBytes := func(id ids.ID) []byte { - idPayload, err := payload.NewHash(id) - require.NoError(t, err) - msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, idPayload.Bytes()) - require.NoError(t, err) - return msg.Bytes() - } - - tests := map[string]struct { - setup func() (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, stats *verifierStats) - err *common.AppError - }{ - "known block": { - setup: func() (request []byte, expectedResponse []byte) { - hashPayload, err := payload.NewHash(knownBlkID) - require.NoError(t, err) - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) - require.NoError(t, err) - signature, err := snowCtx.WarpSigner.Sign(unsignedMessage) - require.NoError(t, err) - return toMessageBytes(knownBlkID), signature - }, - verifyStats: func(t *testing.T, stats *verifierStats) { - require.Zero(t, stats.blockValidationFail.Snapshot().Count()) - require.Zero(t, 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.Equal(t, int64(1), stats.blockValidationFail.Snapshot().Count()) - require.Zero(t, stats.messageParseFail.Snapshot().Count()) - }, - err: &common.AppError{Code: VerifyErrCode}, - }, - } - - for name, test := range tests { - for _, withCache := range []bool{true, false} { - if withCache { - name += "_with_cache" - } else { - name += "_no_cache" - } - t.Run(name, 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( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - blockClient, - nil, - database, - sigCache, - nil, - ) - require.NoError(t, err) - handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) - - requestBytes, expectedResponse := test.setup() - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, test.err) - - 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.Empty(t, responseBytes, "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()) - } - var response sdk.SignatureResponse - err = proto.Unmarshal(responseBytes, &response) - require.NoError(t, err, "error unmarshalling SignatureResponse") - require.Equal(t, expectedResponse, response.Signature) - }) - } - } -} - -func TestUptimeSignatures(t *testing.T) { - database := memdb.New() - snowCtx := utilstest.NewTestSnowContext(t) - - validationID := ids.GenerateTestID() - nodeID := ids.GenerateTestNodeID() - startTime := uint64(time.Now().Unix()) - - getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID) ([]byte, *avalancheWarp.UnsignedMessage) { - uptimePayload, err := messages.NewValidatorUptime(vID, 80) - require.NoError(t, err) - addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) - require.NoError(t, err) - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) - require.NoError(t, err) - - protoMsg := &sdk.SignatureRequest{Message: unsignedMessage.Bytes()} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - return protoBytes, unsignedMessage - } - - for _, withCache := range []bool{true, false} { - var sigCache cache.Cacher[ids.ID, []byte] - if withCache { - sigCache = lru.NewCache[ids.ID, []byte](100) - } else { - sigCache = &cache.Empty[ids.ID, []byte]{} - } - - // Create a validator state that includes our test validator - // TODO(JonathanOppenheimer): see func NewTestValidatorState() -- this should be examined - // when we address the issue of that function. - validatorState := &validatorstest.State{ - GetCurrentValidatorSetF: func(context.Context, ids.ID) (map[ids.ID]*validators.GetCurrentValidatorOutput, uint64, error) { - return map[ids.ID]*validators.GetCurrentValidatorOutput{ - validationID: { - ValidationID: validationID, - NodeID: nodeID, - Weight: 1, - StartTime: startTime, - IsActive: true, - IsL1Validator: true, - }, - }, 0, nil - }, - } - - clk := &mockable.Clock{} - uptimeTracker, err := uptimetracker.New( - validatorState, - snowCtx.SubnetID, - memdb.New(), - clk, - ) - require.NoError(t, err) - - require.NoError(t, uptimeTracker.Sync(t.Context())) - - warpBackend, err := NewBackend( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - warptest.EmptyBlockClient, - uptimeTracker, - database, - sigCache, - nil, - ) - require.NoError(t, err) - handler := acp118.NewCachedHandler(sigCache, warpBackend, snowCtx.WarpSigner) - - // sourceAddress nonZero - protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) - _, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, "2: source address should be empty for offchain addressed messages", appErr.Error()) - - // not existing validationID - vID := ids.GenerateTestID() - protoBytes, _ = getUptimeMessageBytes([]byte{}, vID) - _, appErr = handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: failed to get uptime: validationID not found: %s", vID), appErr.Error()) - - // uptime is less than requested (not connected) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: current uptime 0 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - - // uptime is less than requested (not enough time) - require.NoError(t, uptimeTracker.Connect(nodeID)) - clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: current uptime 40 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - - // valid uptime (enough time has passed) - clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) - responseBytes, appErr := handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.Nil(t, appErr) - expectedSignature, err := snowCtx.WarpSigner.Sign(msg) - require.NoError(t, err) - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.Equal(t, expectedSignature, response.Signature) - } -} diff --git a/graft/subnet-evm/warp/verifier_stats.go b/graft/subnet-evm/warp/verifier_stats.go deleted file mode 100644 index 6c870ce848fa..000000000000 --- a/graft/subnet-evm/warp/verifier_stats.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package warp - -import "github.com/ava-labs/libevm/metrics" - -type verifierStats struct { - messageParseFail metrics.Counter - // AddressedCall metrics - addressedCallValidationFail metrics.Counter - // BlockRequest metrics - blockValidationFail metrics.Counter - // Uptime metrics - uptimeValidationFail metrics.Counter -} - -func newVerifierStats() *verifierStats { - return &verifierStats{ - messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), - addressedCallValidationFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_validation_fail", nil), - blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), - uptimeValidationFail: metrics.NewRegisteredCounter("warp_backend_uptime_validation_fail", nil), - } -} - -func (h *verifierStats) IncAddressedCallValidationFail() { - h.addressedCallValidationFail.Inc(1) -} - -func (h *verifierStats) IncBlockValidationFail() { - h.blockValidationFail.Inc(1) -} - -func (h *verifierStats) IncMessageParseFail() { - h.messageParseFail.Inc(1) -} - -func (h *verifierStats) IncUptimeValidationFail() { - h.uptimeValidationFail.Inc(1) -} diff --git a/graft/subnet-evm/warp/warptest/block_client.go b/graft/subnet-evm/warp/warptest/block_client.go deleted file mode 100644 index 7f98d4f79786..000000000000 --- a/graft/subnet-evm/warp/warptest/block_client.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// warptest exposes common functionality for testing the warp package. -package warptest - -import ( - "context" - "slices" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/consensus/snowman/snowmantest" - "github.com/ava-labs/avalanchego/snow/snowtest" -) - -// EmptyBlockClient returns an error if a block is requested -var EmptyBlockClient BlockClient = MakeBlockClient() - -type BlockClient func(ctx context.Context, blockID ids.ID) (snowman.Block, error) - -func (f BlockClient) GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) { - return f(ctx, blockID) -} - -// MakeBlockClient returns a new BlockClient that returns the provided blocks. -// If a block is requested that isn't part of the provided blocks, an error is -// returned. -func MakeBlockClient(blkIDs ...ids.ID) BlockClient { - return func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if !slices.Contains(blkIDs, blkID) { - return nil, database.ErrNotFound - } - - return &snowmantest.Block{ - Decidable: snowtest.Decidable{ - IDV: blkID, - Status: snowtest.Accepted, - }, - }, nil - } -} diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index eff08fd1aaea..e0ae1f18e027 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -29,7 +29,7 @@ var _ acp118.Verifier = (*acp118Adapter)(nil) // BlockStore provides access to accepted blocks. type BlockStore interface { - GetBlock(ctx context.Context, blockID ids.ID) error + HasBlock(ctx context.Context, blockID ids.ID) error } // DB stores and retrieves warp messages from the underlying database. @@ -143,7 +143,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes // of an accepted block indicating it should be signed by the VM. func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { blockID := blockHashPayload.Hash - if err := v.blockClient.GetBlock(ctx, blockID); err != nil { + if err := v.blockClient.HasBlock(ctx, blockID); err != nil { v.blockValidationFail.Inc(1) return &common.AppError{ Code: VerifyErrCode, diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/backend_test.go index 763b53c6ca62..77d700448f6e 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/backend_test.go @@ -60,7 +60,7 @@ func init() { // testBlockStore implements BlockStore for testing type testBlockStore func(ctx context.Context, blockID ids.ID) error -func (t testBlockStore) GetBlock(ctx context.Context, blockID ids.ID) error { +func (t testBlockStore) HasBlock(ctx context.Context, blockID ids.ID) error { return t(ctx, blockID) } diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 807b64063971..a31b500ad9f9 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -21,7 +21,11 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) -var errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") +var ( + errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") + ErrMessageNotFound = errors.New("message not found") + ErrBlockNotFound = errors.New("block not found") +) // API introduces snowman specific functionality to the evm. // It provides caching and orchestration over the core warp primitives. @@ -113,7 +117,7 @@ func (a *API) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { unsignedMessage, err := a.getMessage(messageID) if err != nil { - return nil, fmt.Errorf("failed to get message %s: %w", messageID, err) + return nil, fmt.Errorf("%w %s: %w", ErrMessageNotFound, messageID, err) } return a.signMessage(ctx, unsignedMessage) } @@ -221,6 +225,9 @@ func (a *API) signMessage(ctx context.Context, unsignedMessage *warp.UnsignedMes } if err := a.verifier.Verify(ctx, unsignedMessage); err != nil { + if err.Code == VerifyErrCode { + return nil, fmt.Errorf("%w: %w", ErrBlockNotFound, err) + } return nil, fmt.Errorf("failed to verify message %s: %w", msgID, err) } From a13d9f554aa98ab9fa296f0e6ba1702d68c89e51 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 17 Dec 2025 17:04:18 -0500 Subject: [PATCH 31/53] chore: delete example --- .../examples/sign-uptime-message/main.go | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 graft/subnet-evm/examples/sign-uptime-message/main.go diff --git a/graft/subnet-evm/examples/sign-uptime-message/main.go b/graft/subnet-evm/examples/sign-uptime-message/main.go deleted file mode 100644 index 7030e462f4e1..000000000000 --- a/graft/subnet-evm/examples/sign-uptime-message/main.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package main - -import ( - "context" - "log" - "net/netip" - "time" - - "github.com/prometheus/client_golang/prometheus" - "google.golang.org/protobuf/proto" - - "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p" - "github.com/ava-labs/avalanchego/network/peer" - "github.com/ava-labs/avalanchego/proto/pb/sdk" - "github.com/ava-labs/avalanchego/snow/networking/router" - "github.com/ava-labs/avalanchego/utils/compression" - "github.com/ava-labs/avalanchego/vms/evm/warp/message" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/ava-labs/avalanchego/wallet/subnet/primary" - - p2pmessage "github.com/ava-labs/avalanchego/message" -) - -// An example application demonstrating how to request a signature for -// an uptime message from a node running locally. -func main() { - uri := primary.LocalAPIURI - // The following IDs are placeholders and should be replaced with real values - // before running the code. - // The validationID is for the validation period that the uptime message is signed for. - validationID := ids.FromStringOrPanic("p3NUAY4PbcAnyCyvUTjGVjezNEQCdnVdfAbJcZScvKpxP5tJr") - // The sourceChainID is the ID of the chain. - sourceChainID := ids.FromStringOrPanic("2UZWB4xjNadRcHSpXarQoCryiVdcGWoT5w1dUztNfMKkAd2hJX") - reqUptime := uint64(3486) - infoClient := info.NewClient(uri) - networkID, err := infoClient.GetNetworkID(context.Background()) - if err != nil { - log.Fatalf("failed to fetch network ID: %s\n", err) - } - - validatorUptime, err := message.NewValidatorUptime(validationID, reqUptime) - if err != nil { - log.Fatalf("failed to create validatorUptime message: %s\n", err) - } - - addressedCall, err := payload.NewAddressedCall( - nil, - validatorUptime.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create AddressedCall message: %s\n", err) - } - - unsignedWarp, err := warp.NewUnsignedMessage( - networkID, - sourceChainID, - addressedCall.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create unsigned Warp message: %s\n", err) - } - - p, err := peer.StartTestPeer( - context.Background(), - netip.AddrPortFrom( - netip.AddrFrom4([4]byte{127, 0, 0, 1}), - 9651, - ), - networkID, - router.InboundHandlerFunc(func(_ context.Context, msg p2pmessage.InboundMessage) { - log.Printf("received %s: %s", msg.Op(), msg.Message()) - }), - ) - if err != nil { - log.Fatalf("failed to start peer: %s\n", err) - } - - messageBuilder, err := p2pmessage.NewCreator( - prometheus.NewRegistry(), - compression.TypeZstd, - time.Hour, - ) - if err != nil { - log.Fatalf("failed to create message builder: %s\n", err) - } - - appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ - Message: unsignedWarp.Bytes(), - }) - if err != nil { - log.Fatalf("failed to marshal SignatureRequest: %s\n", err) - } - - appRequest, err := messageBuilder.AppRequest( - sourceChainID, - 0, - time.Hour, - p2p.PrefixMessage( - p2p.ProtocolPrefix(p2p.SignatureRequestHandlerID), - appRequestPayload, - ), - ) - if err != nil { - log.Fatalf("failed to create AppRequest: %s\n", err) - } - - p.Send(context.Background(), appRequest) - - time.Sleep(5 * time.Second) - - p.StartClose() - err = p.AwaitClosed(context.Background()) - if err != nil { - log.Fatalf("failed to close peer: %s\n", err) - } -} From 4378fac7307739741a0d8c62cad0fcf2f749a5be Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 17 Dec 2025 18:00:23 -0500 Subject: [PATCH 32/53] chore: lint --- graft/subnet-evm/plugin/evm/vm.go | 2 +- graft/subnet-evm/tests/warp/warp_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index 5ba2c9b1d7e5..5ebc18cf9cfb 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -78,6 +78,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/chain" "github.com/ava-labs/avalanchego/vms/evm/acp226" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" + "github.com/ava-labs/avalanchego/vms/evm/warp" subnetevmlog "github.com/ava-labs/avalanchego/graft/subnet-evm/plugin/evm/log" vmsync "github.com/ava-labs/avalanchego/graft/subnet-evm/plugin/evm/sync" @@ -88,7 +89,6 @@ import ( avalancheUtils "github.com/ava-labs/avalanchego/utils" avajson "github.com/ava-labs/avalanchego/utils/json" avalanchegoprometheus "github.com/ava-labs/avalanchego/vms/evm/metrics/prometheus" - "github.com/ava-labs/avalanchego/vms/evm/warp" ethparams "github.com/ava-labs/libevm/params" avalancheRPC "github.com/gorilla/rpc/v2" ) diff --git a/graft/subnet-evm/tests/warp/warp_test.go b/graft/subnet-evm/tests/warp/warp_test.go index 731a68527440..96729916f088 100644 --- a/graft/subnet-evm/tests/warp/warp_test.go +++ b/graft/subnet-evm/tests/warp/warp_test.go @@ -28,7 +28,6 @@ import ( "github.com/ava-labs/avalanchego/graft/subnet-evm/cmd/simulator/txs" "github.com/ava-labs/avalanchego/graft/subnet-evm/ethclient" "github.com/ava-labs/avalanchego/graft/subnet-evm/params" - warpContract "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp/warpbindings" "github.com/ava-labs/avalanchego/graft/subnet-evm/tests" "github.com/ava-labs/avalanchego/graft/subnet-evm/tests/utils" @@ -38,11 +37,12 @@ import ( "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/vms/evm/predicate" + "github.com/ava-labs/avalanchego/vms/evm/warp" "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/platformvm/api" + warpContract "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" warptestbindings "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp/warptest/bindings" - "github.com/ava-labs/avalanchego/vms/evm/warp" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ethereum "github.com/ava-labs/libevm" From 9a954096e6d4d34268dbb6cb150b740130d13ac5 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 11:02:48 -0500 Subject: [PATCH 33/53] fix: first round pass at Josh's comments --- graft/coreth/plugin/evm/vm.go | 6 +++--- graft/subnet-evm/plugin/evm/vm.go | 6 +++--- vms/evm/warp/backend.go | 26 +++++++++----------------- vms/evm/warp/client.go | 16 ++++++++-------- vms/evm/warp/message/codec_test.go | 5 ++--- vms/evm/warp/service.go | 26 +++++++++++++------------- 6 files changed, 38 insertions(+), 47 deletions(-) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index 8a45b27032cb..7f5e11d8922e 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -253,7 +253,7 @@ type VM struct { warpVerifier *warp.Verifier warpSignatureCache cache.Cacher[ids.ID, []byte] offchainWarpMessages [][]byte - warpAPI *warp.API + warpAPI *warp.Service ethTxPushGossiper avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*GossipEthTx]] @@ -456,7 +456,7 @@ func (vm *VM) Initialize( vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, nil) // Create warp API (signatureAggregator will be nil until createAPIs) - vm.warpAPI, err = warp.NewAPI( + vm.warpAPI, err = warp.NewService( vm.ctx, vm.warpMsgDB, vm.ctx.WarpSigner, @@ -1092,7 +1092,7 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.Network.NewClient(p2p.SignatureRequestHandlerID) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - warpAPI, err := warp.NewAPI( + warpAPI, err := warp.NewService( vm.ctx, vm.warpMsgDB, vm.ctx.WarpSigner, diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index 7c0eedf69573..2fb8c188e937 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -260,7 +260,7 @@ type VM struct { warpVerifier *warp.Verifier warpSignatureCache cache.Cacher[ids.ID, []byte] offchainWarpMessages [][]byte - warpAPI *warp.API + warpAPI *warp.Service // Initialize only sets these if nil so they can be overridden in tests ethTxGossipHandler p2p.Handler @@ -487,7 +487,7 @@ func (vm *VM) Initialize( vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, vm.uptimeTracker) // Create warp API (signatureAggregator will be nil until createAPIs) - vm.warpAPI, err = warp.NewAPI( + vm.warpAPI, err = warp.NewService( vm.ctx, vm.warpMsgDB, vm.ctx.WarpSigner, @@ -1218,7 +1218,7 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.Network.NewClient(p2p.SignatureRequestHandlerID) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - warpAPI, err := warp.NewAPI( + warpAPI, err := warp.NewService( vm.ctx, vm.warpMsgDB, vm.ctx.WarpSigner, diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/backend.go index e0ae1f18e027..fc72dd1e8df4 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/backend.go @@ -5,6 +5,7 @@ package warp import ( "context" + "errors" "fmt" "github.com/ava-labs/libevm/metrics" @@ -25,7 +26,7 @@ const ( VerifyErrCode ) -var _ acp118.Verifier = (*acp118Adapter)(nil) +var _ acp118.Verifier = (*acp118Handler)(nil) // BlockStore provides access to accepted blocks. type BlockStore interface { @@ -48,9 +49,6 @@ func NewDB(db database.Database) *DB { func (d *DB) Add(unsignedMsg *warp.UnsignedMessage) error { msgID := unsignedMsg.ID() - // 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 := d.db.Put(msgID[:], unsignedMsg.Bytes()); err != nil { return fmt.Errorf("failed to put warp message in db: %w", err) } @@ -73,13 +71,12 @@ func (d *DB) Get(msgID ids.ID) (*warp.UnsignedMessage, error) { return unsignedMessage, nil } -// Verifier implements acp118.Verifier and validates whether a warp message should be signed. +// Verifier validates whether a warp message should be signed. type Verifier struct { db *DB blockClient BlockStore uptimeTracker *uptimetracker.UptimeTracker - // Metrics messageParseFail metrics.Counter addressedCallValidationFail metrics.Counter blockValidationFail metrics.Counter @@ -109,7 +106,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes // Known on-chain messages should be signed if _, err := v.db.Get(messageID); err == nil { return nil - } else if err != database.ErrNotFound { + } else if !errors.Is(err, database.ErrNotFound) { return &common.AppError{ Code: ParseErrCode, Message: fmt.Sprintf("failed to get message %s: %s", messageID, err), @@ -202,21 +199,16 @@ func (v *Verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *comm return nil } -// acp118Adapter adapts the EVM warp Verifier to the acp118.Verifier interface. -// This adapter ignores the justification parameter since the EVM verifier doesn't use it. -type acp118Adapter struct { +// acp118Handler supports signing warp messages requested by peers. +type acp118Handler struct { verifier *Verifier } -// Verify implements acp118.Verifier by delegating to the wrapped Verifier. -// The justification parameter is ignored as it's not used by the EVM warp verifier. -func (a *acp118Adapter) Verify(ctx context.Context, message *warp.UnsignedMessage, _ []byte) *common.AppError { +func (a *acp118Handler) Verify(ctx context.Context, message *warp.UnsignedMessage, _ []byte) *common.AppError { return a.verifier.Verify(ctx, message) } -// NewHandler creates a new acp118.Handler for signing warp messages. -// This is a convenience function that wraps the verifier in an acp118Adapter -// and creates a cached handler. +// NewHandler returns a handler for signing warp messages requested by peers. func NewHandler( signatureCache cache.Cacher[ids.ID, []byte], verifier *Verifier, @@ -224,7 +216,7 @@ func NewHandler( ) *acp118.Handler { return acp118.NewCachedHandler( signatureCache, - &acp118Adapter{verifier: verifier}, + &acp118Handler{verifier: verifier}, signer, ) } diff --git a/vms/evm/warp/client.go b/vms/evm/warp/client.go index 79075312d95d..a49cb6a8844a 100644 --- a/vms/evm/warp/client.go +++ b/vms/evm/warp/client.go @@ -19,19 +19,19 @@ type Client struct { // NewClient returns a Client for interacting with EVM chain func NewClient(uri, chain string) (*Client, error) { - innerClient, err := rpc.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", uri, chain)) + c, err := rpc.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", uri, chain)) if err != nil { - return nil, fmt.Errorf("failed to dial client. err: %w", err) + return nil, fmt.Errorf("failed to dial client: %w", err) } return &Client{ - client: innerClient, + client: c, }, nil } func (c *Client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessage", messageID); err != nil { - return nil, fmt.Errorf("call to warp_getMessage failed. err: %w", err) + return nil, fmt.Errorf("call to warp_getMessage failed: %w", err) } return res, nil } @@ -39,7 +39,7 @@ func (c *Client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, erro func (c *Client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessageSignature", messageID); err != nil { - return nil, fmt.Errorf("call to warp_getMessageSignature failed. err: %w", err) + return nil, fmt.Errorf("call to warp_getMessageSignature failed: %w", err) } return res, nil } @@ -47,7 +47,7 @@ func (c *Client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]b func (c *Client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetIDStr); err != nil { - return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed. err: %w", err) + return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed: %w", err) } return res, nil } @@ -55,7 +55,7 @@ func (c *Client) GetMessageAggregateSignature(ctx context.Context, messageID ids func (c *Client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getBlockSignature", blockID); err != nil { - return nil, fmt.Errorf("call to warp_getBlockSignature failed. err: %w", err) + return nil, fmt.Errorf("call to warp_getBlockSignature failed: %w", err) } return res, nil } @@ -63,7 +63,7 @@ func (c *Client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, func (c *Client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetIDStr); err != nil { - return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed. err: %w", err) + return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed: %w", err) } return res, nil } diff --git a/vms/evm/warp/message/codec_test.go b/vms/evm/warp/message/codec_test.go index 1bab8cdcce4e..32b6deb225e5 100644 --- a/vms/evm/warp/message/codec_test.go +++ b/vms/evm/warp/message/codec_test.go @@ -11,8 +11,7 @@ import ( "github.com/ava-labs/avalanchego/ids" ) -// TestCodecSerialization tests the registration order changes in codec.go, -// does not change, preventing unintended serialization format changes. +// TestCodecSerialization ensures the serialization format remains stable. func TestCodecSerialization(t *testing.T) { tests := []struct { name string @@ -69,7 +68,7 @@ func TestCodecSerialization(t *testing.T) { // Test marshaling produces expected bytes gotBytes, err := Codec.Marshal(CodecVersion, tt.msg) require.NoError(err) - require.Equal(tt.wantBytes, gotBytes, "marshaled bytes do not match expected - codec registration order may have changed") + require.Equal(tt.wantBytes, gotBytes) // Test unmarshaling the expected bytes produces the original message var gotMsg ValidatorUptime diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 8e46146efa3d..510d3a55a1b4 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -27,9 +27,9 @@ var ( ErrBlockNotFound = errors.New("block not found") ) -// API introduces snowman specific functionality to the evm. +// Service introduces snowman specific functionality to the evm. // It provides caching and orchestration over the core warp primitives. -type API struct { +type Service struct { chainContext *snow.Context db *DB signer warp.Signer @@ -42,7 +42,7 @@ type API struct { offchainMessages map[ids.ID]*warp.UnsignedMessage } -func NewAPI( +func NewService( chainCtx *snow.Context, db *DB, signer warp.Signer, @@ -50,7 +50,7 @@ func NewAPI( signatureCache cache.Cacher[ids.ID, []byte], signatureAggregator *acp118.SignatureAggregator, offchainMessages [][]byte, -) (*API, error) { +) (*Service, error) { offchainMsgs := make(map[ids.ID]*warp.UnsignedMessage) for i, offchainMsg := range offchainMessages { unsignedMsg, err := warp.ParseUnsignedMessage(offchainMsg) @@ -73,7 +73,7 @@ func NewAPI( offchainMsgs[unsignedMsg.ID()] = unsignedMsg } - return &API{ + return &Service{ db: db, signer: signer, verifier: verifier, @@ -86,7 +86,7 @@ func NewAPI( } // GetMessage returns the Warp message associated with a messageID. -func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { +func (a *Service) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { message, err := a.getMessage(messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s: %w", messageID, err) @@ -95,7 +95,7 @@ func (a *API) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, er } // getMessage retrieves a message from cache, offchain messages, or database. -func (a *API) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { +func (a *Service) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { if msg, ok := a.messageCache.Get(messageID); ok { return msg, nil } @@ -114,7 +114,7 @@ func (a *API) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { } // GetMessageSignature returns the BLS signature associated with a messageID. -func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { +func (a *Service) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { unsignedMessage, err := a.getMessage(messageID) if err != nil { return nil, fmt.Errorf("%w %s: %w", ErrMessageNotFound, messageID, err) @@ -125,7 +125,7 @@ func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexuti // GetBlockSignature returns the BLS signature associated with a blockID. // It constructs a warp message with a Hash payload containing the blockID, // then returns the signature for that message. -func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { +func (a *Service) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { blockHashPayload, err := payload.NewHash(blockID) if err != nil { return nil, fmt.Errorf("failed to create block hash payload: %w", err) @@ -144,7 +144,7 @@ func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.By } // GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] -func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { +func (a *Service) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { unsignedMessage, err := a.getMessage(messageID) if err != nil { return nil, err @@ -153,7 +153,7 @@ func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID } // GetBlockAggregateSignature fetches the aggregate signature for the requested [blockID] -func (a *API) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { +func (a *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { blockHashPayload, err := payload.NewHash(blockID) if err != nil { return nil, err @@ -166,7 +166,7 @@ func (a *API) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, qu return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) } -func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetIDStr string) (hexutil.Bytes, error) { +func (a *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetIDStr string) (hexutil.Bytes, error) { subnetID := a.chainContext.SubnetID if len(subnetIDStr) > 0 { sid, err := ids.FromString(subnetIDStr) @@ -217,7 +217,7 @@ func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.Uns } // signMessage verifies, signs, and caches a signature for the given unsigned message. -func (a *API) signMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) (hexutil.Bytes, error) { +func (a *Service) signMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) (hexutil.Bytes, error) { msgID := unsignedMessage.ID() if sig, ok := a.signatureCache.Get(msgID); ok { From 99b310d171800c493a0bb7b7419a6a67446f75f8 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 11:42:32 -0500 Subject: [PATCH 34/53] fix: Josh round 2 comments + organize tests --- vms/evm/warp/db.go | 50 +++ vms/evm/warp/db_test.go | 33 ++ vms/evm/warp/helpers_test.go | 47 +++ vms/evm/warp/message/codec.go | 33 -- vms/evm/warp/message/codec_test.go | 127 -------- vms/evm/warp/message/validator_uptime.go | 43 --- vms/evm/warp/service.go | 45 +-- vms/evm/warp/{backend.go => verifier.go} | 78 ++--- .../{backend_test.go => verifier_test.go} | 290 ++++++++++-------- 9 files changed, 366 insertions(+), 380 deletions(-) create mode 100644 vms/evm/warp/db.go create mode 100644 vms/evm/warp/db_test.go create mode 100644 vms/evm/warp/helpers_test.go delete mode 100644 vms/evm/warp/message/codec.go delete mode 100644 vms/evm/warp/message/codec_test.go delete mode 100644 vms/evm/warp/message/validator_uptime.go rename vms/evm/warp/{backend.go => verifier.go} (80%) rename vms/evm/warp/{backend_test.go => verifier_test.go} (69%) diff --git a/vms/evm/warp/db.go b/vms/evm/warp/db.go new file mode 100644 index 000000000000..0ee9c3c21c70 --- /dev/null +++ b/vms/evm/warp/db.go @@ -0,0 +1,50 @@ +// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +// DB stores and retrieves warp messages from the underlying database. +type DB struct { + db database.Database +} + +// NewDB creates a new warp message database. +func NewDB(db database.Database) *DB { + return &DB{ + db: db, + } +} + +// Add stores a warp message in the database and cache. +func (d *DB) Add(unsignedMsg *warp.UnsignedMessage) error { + msgID := unsignedMsg.ID() + + if err := d.db.Put(msgID[:], unsignedMsg.Bytes()); err != nil { + return fmt.Errorf("failed to put warp message in db: %w", err) + } + + return nil +} + +// Get retrieves a warp message from the database. +func (d *DB) Get(msgID ids.ID) (*warp.UnsignedMessage, error) { + unsignedMessageBytes, err := d.db.Get(msgID[:]) + 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", msgID.String(), err) + } + + return unsignedMessage, nil +} diff --git a/vms/evm/warp/db_test.go b/vms/evm/warp/db_test.go new file mode 100644 index 000000000000..fe08ac92448d --- /dev/null +++ b/vms/evm/warp/db_test.go @@ -0,0 +1,33 @@ +// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +func TestDBAddAndSign(t *testing.T) { + db := memdb.New() + + sk, err := localsigner.New() + require.NoError(t, err) + warpSigner := warp.NewSigner(sk, networkID, sourceChainID) + + messageDB := NewDB(db) + + require.NoError(t, messageDB.Add(testUnsignedMessage)) + + signature, err := warpSigner.Sign(testUnsignedMessage) + require.NoError(t, err) + + wantSig, err := warpSigner.Sign(testUnsignedMessage) + require.NoError(t, err) + require.Equal(t, wantSig, signature) +} diff --git a/vms/evm/warp/helpers_test.go b/vms/evm/warp/helpers_test.go new file mode 100644 index 000000000000..adc7c7803561 --- /dev/null +++ b/vms/evm/warp/helpers_test.go @@ -0,0 +1,47 @@ +// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +var ( + networkID uint32 = 54321 + sourceChainID = ids.GenerateTestID() + testSourceAddress = utils.RandomBytes(20) + testPayload = []byte("test") + testUnsignedMessage *warp.UnsignedMessage + + emptyBlockStore BlockStore = testBlockStore{} +) + +func init() { + testAddressedCallPayload, err := payload.NewAddressedCall(testSourceAddress, testPayload) + if err != nil { + panic(err) + } + testUnsignedMessage, err = warp.NewUnsignedMessage(networkID, sourceChainID, testAddressedCallPayload.Bytes()) + if err != nil { + panic(err) + } +} + +// testBlockStore implements BlockStore for testing +type testBlockStore set.Set[ids.ID] + +func (t testBlockStore) HasBlock(_ context.Context, blockID ids.ID) error { + s := set.Set[ids.ID](t) + if !s.Contains(blockID) { + return database.ErrNotFound + } + return nil +} diff --git a/vms/evm/warp/message/codec.go b/vms/evm/warp/message/codec.go deleted file mode 100644 index 022ec421c69d..000000000000 --- a/vms/evm/warp/message/codec.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import ( - "errors" - - "github.com/ava-labs/avalanchego/codec" - "github.com/ava-labs/avalanchego/codec/linearcodec" - "github.com/ava-labs/avalanchego/utils/units" -) - -const ( - CodecVersion = 0 - - MaxMessageSize = 24 * units.KiB -) - -var Codec codec.Manager - -func init() { - Codec = codec.NewManager(MaxMessageSize) - lc := linearcodec.NewDefault() - - err := errors.Join( - lc.RegisterType(&ValidatorUptime{}), - Codec.RegisterCodec(CodecVersion, lc), - ) - if err != nil { - panic(err) - } -} diff --git a/vms/evm/warp/message/codec_test.go b/vms/evm/warp/message/codec_test.go deleted file mode 100644 index 32b6deb225e5..000000000000 --- a/vms/evm/warp/message/codec_test.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/ids" -) - -// TestCodecSerialization ensures the serialization format remains stable. -func TestCodecSerialization(t *testing.T) { - tests := []struct { - name string - msg *ValidatorUptime - wantBytes []byte - }{ - { - name: "zero values", - msg: &ValidatorUptime{ - ValidationID: ids.Empty, - TotalUptime: 0, - }, - wantBytes: []byte{ - // Codec version (0) - 0x00, 0x00, - // ValidationID (32 bytes of zeros) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // TotalUptime (8 bytes, uint64 = 0) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - }, - { - name: "non-zero values", - msg: &ValidatorUptime{ - ValidationID: ids.ID{ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, - 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, - }, - TotalUptime: 12345, - }, - wantBytes: []byte{ - // Codec version (0) - 0x00, 0x00, - // ValidationID (32 bytes) - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, - 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, - // TotalUptime (8 bytes, uint64 = 12345 in big-endian) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x39, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require := require.New(t) - - // Test marshaling produces expected bytes - gotBytes, err := Codec.Marshal(CodecVersion, tt.msg) - require.NoError(err) - require.Equal(tt.wantBytes, gotBytes) - - // Test unmarshaling the expected bytes produces the original message - var gotMsg ValidatorUptime - version, err := Codec.Unmarshal(tt.wantBytes, &gotMsg) - require.NoError(err) - require.Equal(uint16(CodecVersion), version) - require.Equal(tt.msg.ValidationID, gotMsg.ValidationID) - require.Equal(tt.msg.TotalUptime, gotMsg.TotalUptime) - }) - } -} - -// TestCodecRoundTrip verifies that messages can be marshaled and unmarshaled -// without data loss. -func TestCodecRoundTrip(t *testing.T) { - tests := []struct { - name string - msg *ValidatorUptime - }{ - { - name: "zero values", - msg: &ValidatorUptime{ - ValidationID: ids.Empty, - TotalUptime: 0, - }, - }, - { - name: "max uptime", - msg: &ValidatorUptime{ - ValidationID: ids.GenerateTestID(), - TotalUptime: ^uint64(0), // max uint64 - }, - }, - { - name: "random values", - msg: &ValidatorUptime{ - ValidationID: ids.GenerateTestID(), - TotalUptime: 987654321, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bytes, err := Codec.Marshal(CodecVersion, tt.msg) - require.NoError(t, err) - - var gotMsg ValidatorUptime - version, err := Codec.Unmarshal(bytes, &gotMsg) - require.NoError(t, err) - require.Equal(t, uint16(CodecVersion), version) - require.Equal(t, tt.msg.ValidationID, gotMsg.ValidationID) - require.Equal(t, tt.msg.TotalUptime, gotMsg.TotalUptime) - }) - } -} diff --git a/vms/evm/warp/message/validator_uptime.go b/vms/evm/warp/message/validator_uptime.go deleted file mode 100644 index 06cac66e5b27..000000000000 --- a/vms/evm/warp/message/validator_uptime.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import "github.com/ava-labs/avalanchego/ids" - -// ValidatorUptime is signed when the ValidationID is known and the validator -// has been up for TotalUptime seconds. -type ValidatorUptime struct { - ValidationID ids.ID `serialize:"true"` - TotalUptime uint64 `serialize:"true"` // in seconds - - bytes []byte -} - -func NewValidatorUptime(validationID ids.ID, totalUptime uint64) (*ValidatorUptime, error) { - msg := &ValidatorUptime{ - ValidationID: validationID, - TotalUptime: totalUptime, - } - bytes, err := Codec.Marshal(CodecVersion, msg) - if err != nil { - return nil, err - } - msg.bytes = bytes - return msg, nil -} - -// ParseValidatorUptime converts a slice of bytes into an initialized ValidatorUptime. -func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { - var msg ValidatorUptime - if _, err := Codec.Unmarshal(b, &msg); err != nil { - return nil, err - } - msg.bytes = b - return &msg, nil -} - -// Bytes returns the binary representation of this payload. -func (v *ValidatorUptime) Bytes() []byte { - return v.bytes -} diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 510d3a55a1b4..65717c3c6b65 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -15,7 +15,7 @@ import ( "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" - "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" @@ -30,39 +30,45 @@ var ( // Service introduces snowman specific functionality to the evm. // It provides caching and orchestration over the core warp primitives. type Service struct { - chainContext *snow.Context + networkID uint32 + chainID ids.ID + subnetID ids.ID + validatorState validators.State + db *DB signer warp.Signer verifier *Verifier signatureAggregator *acp118.SignatureAggregator - // Caching messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] signatureCache cache.Cacher[ids.ID, []byte] - offchainMessages map[ids.ID]*warp.UnsignedMessage + offChainMessages map[ids.ID]*warp.UnsignedMessage } func NewService( - chainCtx *snow.Context, + networkID uint32, + chainID ids.ID, + subnetID ids.ID, + validatorState validators.State, db *DB, signer warp.Signer, verifier *Verifier, signatureCache cache.Cacher[ids.ID, []byte], signatureAggregator *acp118.SignatureAggregator, - offchainMessages [][]byte, + offChainMessages [][]byte, ) (*Service, error) { offchainMsgs := make(map[ids.ID]*warp.UnsignedMessage) - for i, offchainMsg := range offchainMessages { + for i, offchainMsg := range offChainMessages { unsignedMsg, err := warp.ParseUnsignedMessage(offchainMsg) if err != nil { return nil, fmt.Errorf("failed to parse off-chain message at index %d: %w", i, err) } - if unsignedMsg.NetworkID != chainCtx.NetworkID { + if unsignedMsg.NetworkID != networkID { return nil, fmt.Errorf("wrong network ID at index %d", i) } - if unsignedMsg.SourceChainID != chainCtx.ChainID { + if unsignedMsg.SourceChainID != chainID { return nil, fmt.Errorf("wrong source chain ID at index %d", i) } @@ -74,19 +80,22 @@ func NewService( } return &Service{ + networkID: networkID, + chainID: chainID, + subnetID: subnetID, + validatorState: validatorState, db: db, signer: signer, verifier: verifier, - chainContext: chainCtx, signatureAggregator: signatureAggregator, messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](500), signatureCache: signatureCache, - offchainMessages: offchainMsgs, + offChainMessages: offchainMsgs, }, nil } // GetMessage returns the Warp message associated with a messageID. -func (a *Service) GetMessage(_ context.Context, messageID ids.ID) (hexutil.Bytes, error) { +func (a *Service) GetMessage(messageID ids.ID) (hexutil.Bytes, error) { message, err := a.getMessage(messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s: %w", messageID, err) @@ -100,7 +109,7 @@ func (a *Service) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { return msg, nil } - if msg, ok := a.offchainMessages[messageID]; ok { + if msg, ok := a.offChainMessages[messageID]; ok { return msg, nil } @@ -132,8 +141,8 @@ func (a *Service) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexuti } unsignedMessage, err := warp.NewUnsignedMessage( - a.chainContext.NetworkID, - a.chainContext.ChainID, + a.networkID, + a.chainID, blockHashPayload.Bytes(), ) if err != nil { @@ -158,7 +167,7 @@ func (a *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID if err != nil { return nil, err } - unsignedMessage, err := warp.NewUnsignedMessage(a.chainContext.NetworkID, a.chainContext.ChainID, blockHashPayload.Bytes()) + unsignedMessage, err := warp.NewUnsignedMessage(a.networkID, a.chainID, blockHashPayload.Bytes()) if err != nil { return nil, err } @@ -167,7 +176,7 @@ func (a *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID } func (a *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetIDStr string) (hexutil.Bytes, error) { - subnetID := a.chainContext.SubnetID + subnetID := a.subnetID if len(subnetIDStr) > 0 { sid, err := ids.FromString(subnetIDStr) if err != nil { @@ -175,7 +184,7 @@ func (a *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp } subnetID = sid } - validatorState := a.chainContext.ValidatorState + validatorState := a.validatorState pChainHeight, err := validatorState.GetCurrentHeight(ctx) if err != nil { return nil, err diff --git a/vms/evm/warp/backend.go b/vms/evm/warp/verifier.go similarity index 80% rename from vms/evm/warp/backend.go rename to vms/evm/warp/verifier.go index fc72dd1e8df4..cfb044848392 100644 --- a/vms/evm/warp/backend.go +++ b/vms/evm/warp/verifier.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package warp @@ -8,67 +8,71 @@ import ( "errors" "fmt" - "github.com/ava-labs/libevm/metrics" - "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" - "github.com/ava-labs/avalanchego/vms/evm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/libevm/metrics" ) const ( ParseErrCode = iota + 1 VerifyErrCode + + CodecVersion = 0 + MaxMessageSize = 24 * units.KiB ) -var _ acp118.Verifier = (*acp118Handler)(nil) +var ( + _ acp118.Verifier = (*acp118Handler)(nil) -// BlockStore provides access to accepted blocks. -type BlockStore interface { - HasBlock(ctx context.Context, blockID ids.ID) error -} + Codec codec.Manager +) -// DB stores and retrieves warp messages from the underlying database. -type DB struct { - db database.Database -} +func init() { + Codec = codec.NewManager(MaxMessageSize) + lc := linearcodec.NewDefault() -// NewDB creates a new warp message database. -func NewDB(db database.Database) *DB { - return &DB{ - db: db, + err := errors.Join( + lc.RegisterType(&ValidatorUptime{}), + Codec.RegisterCodec(CodecVersion, lc), + ) + if err != nil { + panic(err) } } -// Add stores a warp message in the database and cache. -func (d *DB) Add(unsignedMsg *warp.UnsignedMessage) error { - msgID := unsignedMsg.ID() - - if err := d.db.Put(msgID[:], unsignedMsg.Bytes()); err != nil { - return fmt.Errorf("failed to put warp message in db: %w", err) - } - - return nil +// ValidatorUptime is signed when the ValidationID is known and the validator +// has been up for TotalUptime seconds. +type ValidatorUptime struct { + ValidationID ids.ID `serialize:"true"` + TotalUptime uint64 `serialize:"true"` // in seconds } -// Get retrieves a warp message from the database. -func (d *DB) Get(msgID ids.ID) (*warp.UnsignedMessage, error) { - unsignedMessageBytes, err := d.db.Get(msgID[:]) - if err != nil { +// ParseValidatorUptime converts a slice of bytes into a ValidatorUptime. +func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { + var msg ValidatorUptime + if _, err := Codec.Unmarshal(b, &msg); err != nil { return nil, err } + return &msg, nil +} - unsignedMessage, err := warp.ParseUnsignedMessage(unsignedMessageBytes) - if err != nil { - return nil, fmt.Errorf("failed to parse unsigned message %s: %w", msgID.String(), err) - } +// Bytes returns the binary representation of this payload. +func (v *ValidatorUptime) Bytes() ([]byte, error) { + return Codec.Marshal(CodecVersion, v) +} - return unsignedMessage, nil +// BlockStore provides access to accepted blocks. +type BlockStore interface { + HasBlock(ctx context.Context, blockID ids.ID) error } // Verifier validates whether a warp message should be signed. @@ -161,7 +165,7 @@ func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC } } - uptimeMsg, err := message.ParseValidatorUptime(addressedCall.Payload) + uptimeMsg, err := ParseValidatorUptime(addressedCall.Payload) if err != nil { v.messageParseFail.Inc(1) return &common.AppError{ @@ -178,7 +182,7 @@ func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC return nil } -func (v *Verifier) verifyUptimeMessage(uptimeMsg *message.ValidatorUptime) *common.AppError { +func (v *Verifier) verifyUptimeMessage(uptimeMsg *ValidatorUptime) *common.AppError { currentUptime, _, err := v.uptimeTracker.GetUptime(uptimeMsg.ValidationID) if err != nil { return &common.AppError{ diff --git a/vms/evm/warp/backend_test.go b/vms/evm/warp/verifier_test.go similarity index 69% rename from vms/evm/warp/backend_test.go rename to vms/evm/warp/verifier_test.go index a0ada3ca6bfc..af7077200c55 100644 --- a/vms/evm/warp/backend_test.go +++ b/vms/evm/warp/verifier_test.go @@ -6,7 +6,6 @@ package warp import ( "context" "fmt" - "slices" "testing" "time" @@ -15,7 +14,6 @@ import ( "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" - "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/proto/pb/sdk" @@ -23,60 +21,129 @@ import ( "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/snow/validators/validatorstest" - "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/evm/metrics/metricstest" "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" - "github.com/ava-labs/avalanchego/vms/evm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) -var ( - networkID uint32 = 54321 - sourceChainID = ids.GenerateTestID() - testSourceAddress = utils.RandomBytes(20) - testPayload = []byte("test") - testUnsignedMessage *warp.UnsignedMessage - - // emptyBlockStore returns an error if a block is requested - emptyBlockStore BlockStore = testBlockStore(func(_ context.Context, _ ids.ID) error { - return database.ErrNotFound - }) -) - -func init() { - testAddressedCallPayload, err := payload.NewAddressedCall(testSourceAddress, testPayload) - if err != nil { - panic(err) +// TestCodecSerialization ensures the serialization format remains stable. +func TestCodecSerialization(t *testing.T) { + tests := []struct { + name string + msg *ValidatorUptime + wantBytes []byte + }{ + { + name: "zero values", + msg: &ValidatorUptime{ + ValidationID: ids.Empty, + TotalUptime: 0, + }, + wantBytes: []byte{ + // Codec version (0) + 0x00, 0x00, + // ValidationID (32 bytes of zeros) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // TotalUptime (8 bytes, uint64 = 0) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, + { + name: "non-zero values", + msg: &ValidatorUptime{ + ValidationID: ids.ID{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + }, + TotalUptime: 12345, + }, + wantBytes: []byte{ + // Codec version (0) + 0x00, 0x00, + // ValidationID (32 bytes) + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + // TotalUptime (8 bytes, uint64 = 12345 in big-endian) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x39, + }, + }, } - testUnsignedMessage, err = warp.NewUnsignedMessage(networkID, sourceChainID, testAddressedCallPayload.Bytes()) - if err != nil { - panic(err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + gotBytes, err := Codec.Marshal(CodecVersion, tt.msg) + require.NoError(err) + require.Equal(tt.wantBytes, gotBytes) + + var gotMsg ValidatorUptime + version, err := Codec.Unmarshal(tt.wantBytes, &gotMsg) + require.NoError(err) + require.Equal(uint16(CodecVersion), version) + require.Equal(tt.msg.ValidationID, gotMsg.ValidationID) + require.Equal(tt.msg.TotalUptime, gotMsg.TotalUptime) + }) } } -// testBlockStore implements BlockStore for testing -type testBlockStore func(ctx context.Context, blockID ids.ID) error - -func (t testBlockStore) HasBlock(ctx context.Context, blockID ids.ID) error { - return t(ctx, blockID) -} +// TestCodecRoundTrip verifies that messages can be marshaled and unmarshaled +// without data loss. +func TestCodecRoundTrip(t *testing.T) { + tests := []struct { + name string + msg *ValidatorUptime + }{ + { + name: "zero values", + msg: &ValidatorUptime{ + ValidationID: ids.Empty, + TotalUptime: 0, + }, + }, + { + name: "max uptime", + msg: &ValidatorUptime{ + ValidationID: ids.GenerateTestID(), + TotalUptime: ^uint64(0), // max uint64 + }, + }, + { + name: "random values", + msg: &ValidatorUptime{ + ValidationID: ids.GenerateTestID(), + TotalUptime: 987654321, + }, + }, + } -// makeBlockStore returns a new BlockStore that returns the provided blocks. -// If a block is requested that isn't part of the provided blocks, an error is -// returned. -func makeBlockStore(blkIDs ...ids.ID) BlockStore { - return testBlockStore(func(_ context.Context, blkID ids.ID) error { - if !slices.Contains(blkIDs, blkID) { - return database.ErrNotFound - } - return nil - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bytes, err := Codec.Marshal(CodecVersion, tt.msg) + require.NoError(t, err) + + var gotMsg ValidatorUptime + version, err := Codec.Unmarshal(bytes, &gotMsg) + require.NoError(t, err) + require.Equal(t, uint16(CodecVersion), version) + require.Equal(t, tt.msg.ValidationID, gotMsg.ValidationID) + require.Equal(t, tt.msg.TotalUptime, gotMsg.TotalUptime) + }) + } } -func TestAddAndGetValidMessage(t *testing.T) { +func TestVerifierKnownMessage(t *testing.T) { db := memdb.New() sk, err := localsigner.New() @@ -84,20 +151,23 @@ func TestAddAndGetValidMessage(t *testing.T) { warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageDB := NewDB(db) + verifier := NewVerifier(messageDB, nil, nil) - // Add testUnsignedMessage to the warp backend require.NoError(t, messageDB.Add(testUnsignedMessage)) - // Verify that a signature is returned successfully, and compare to expected signature. + // Known messages in the DB should pass verification + appErr := verifier.Verify(t.Context(), testUnsignedMessage) + require.Nil(t, appErr) + signature, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) - expectedSig, err := warpSigner.Sign(testUnsignedMessage) + wantSig, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) - require.Equal(t, expectedSig, signature) + require.Equal(t, wantSig, signature) } -func TestAddAndGetUnknownMessage(t *testing.T) { +func TestVerifierUnknownMessage(t *testing.T) { db := memdb.New() messageDB := NewDB(db) @@ -109,14 +179,13 @@ func TestAddAndGetUnknownMessage(t *testing.T) { unknownMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownPayload.Bytes()) require.NoError(t, err) - // Try to verify an unknown message - should fail with parse error appErr := verifier.Verify(t.Context(), unknownMessage) require.ErrorIs(t, appErr, &common.AppError{Code: ParseErrCode}) } -func TestGetBlockSignature(t *testing.T) { +func TestVerifierBlockMessage(t *testing.T) { blkID := ids.GenerateTestID() - blockStore := makeBlockStore(blkID) + blockStore := testBlockStore(set.Of(blkID)) db := memdb.New() sk, err := localsigner.New() @@ -130,19 +199,18 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(t, err) unsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) require.NoError(t, err) - expectedSig, err := warpSigner.Sign(unsignedMessage) + wantSig, err := warpSigner.Sign(unsignedMessage) require.NoError(t, err) - // Verify the block message + // Known block should pass verification appErr := verifier.Verify(t.Context(), unsignedMessage) require.Nil(t, appErr) - // Then sign it signature, err := warpSigner.Sign(unsignedMessage) require.NoError(t, err) - require.Equal(t, expectedSig, signature) + require.Equal(t, wantSig, signature) - // Test that an unknown block fails verification + // Unknown block should fail verification unknownBlockHashPayload, err := payload.NewHash(ids.GenerateTestID()) require.NoError(t, err) unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) @@ -151,45 +219,21 @@ func TestGetBlockSignature(t *testing.T) { require.ErrorIs(t, unknownAppErr, &common.AppError{Code: VerifyErrCode}) } -func TestVerifierKnownMessage(t *testing.T) { - db := memdb.New() - - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := warp.NewSigner(sk, networkID, sourceChainID) - - messageDB := NewDB(db) - verifier := NewVerifier(messageDB, nil, nil) - - // Add testUnsignedMessage to the database - require.NoError(t, messageDB.Add(testUnsignedMessage)) - - // Known messages in the DB should pass verification - appErr := verifier.Verify(t.Context(), testUnsignedMessage) - require.Nil(t, appErr) - - // And can be signed - signature, err := warpSigner.Sign(testUnsignedMessage) - require.NoError(t, err) - - expectedSig, err := warpSigner.Sign(testUnsignedMessage) - require.NoError(t, err) - require.Equal(t, expectedSig, signature) -} - -func TestKnownMessageSignature(t *testing.T) { +func TestHandlerMessageSignature(t *testing.T) { metricstest.WithMetrics(t) database := memdb.New() snowCtx := snowtest.Context(t, snowtest.CChainID) - tests := map[string]struct { - setup func(db *DB) (request []byte, expectedResponse []byte) + tests := []struct { + name string + setup func(db *DB) (request []byte, wantResponse []byte) verifyStats func(t *testing.T, v *Verifier) err *common.AppError }{ - "known message": { - setup: func(db *DB) (request []byte, expectedResponse []byte) { + { + name: "known message", + setup: func(db *DB) (request []byte, wantResponse []byte) { knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) @@ -204,8 +248,9 @@ func TestKnownMessageSignature(t *testing.T) { require.Zero(t, v.blockValidationFail.Snapshot().Count()) }, }, - "unknown message": { - setup: func(_ *DB) (request []byte, expectedResponse []byte) { + { + name: "unknown message", + setup: func(_ *DB) (request []byte, wantResponse []byte) { unknownPayload, err := payload.NewAddressedCall([]byte{}, []byte("unknown message")) require.NoError(t, err) unknownMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) @@ -220,8 +265,9 @@ func TestKnownMessageSignature(t *testing.T) { }, } - for name, test := range tests { + for _, tt := range tests { for _, withCache := range []bool{true, false} { + name := tt.name if withCache { name += "_with_cache" } else { @@ -238,20 +284,19 @@ func TestKnownMessageSignature(t *testing.T) { v := NewVerifier(db, emptyBlockStore, nil) handler := NewHandler(sigCache, v, snowCtx.WarpSigner) - requestBytes, expectedResponse := test.setup(db) + requestBytes, wantResponse := tt.setup(db) protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, test.err) - test.verifyStats(t, v) + require.ErrorIs(t, appErr, tt.err) + tt.verifyStats(t, v) - // If the expected response is empty, assert that the handler returns an empty response and return early. - if len(expectedResponse) == 0 { - require.Empty(t, responseBytes, "expected response to be empty") + if len(wantResponse) == 0 { + require.Empty(t, responseBytes) return } - // check cache is populated (handler's cache) + if withCache { require.NotZero(t, sigCache.Len()) } else { @@ -259,22 +304,20 @@ func TestKnownMessageSignature(t *testing.T) { } response := &sdk.SignatureResponse{} require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.NoError(t, err, "error unmarshalling SignatureResponse") - - require.Equal(t, expectedResponse, response.Signature) + require.Equal(t, wantResponse, response.Signature) }) } } } -func TestBlockSignatures(t *testing.T) { +func TestHandlerBlockSignature(t *testing.T) { metricstest.WithMetrics(t) database := memdb.New() snowCtx := snowtest.Context(t, snowtest.CChainID) knownBlkID := ids.GenerateTestID() - blockStore := makeBlockStore(knownBlkID) + blockStore := testBlockStore(set.Of(knownBlkID)) toMessageBytes := func(id ids.ID) []byte { idPayload, err := payload.NewHash(id) @@ -290,13 +333,15 @@ func TestBlockSignatures(t *testing.T) { return msg.Bytes() } - tests := map[string]struct { - setup func() (request []byte, expectedResponse []byte) + tests := []struct { + name string + setup func() (request []byte, wantResponse []byte) verifyStats func(t *testing.T, v *Verifier) err *common.AppError }{ - "known block": { - setup: func() (request []byte, expectedResponse []byte) { + { + name: "known block", + setup: func() (request []byte, wantResponse []byte) { hashPayload, err := payload.NewHash(knownBlkID) require.NoError(t, err) unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) @@ -310,8 +355,9 @@ func TestBlockSignatures(t *testing.T) { require.Zero(t, v.messageParseFail.Snapshot().Count()) }, }, - "unknown block": { - setup: func() (request []byte, expectedResponse []byte) { + { + name: "unknown block", + setup: func() (request []byte, wantResponse []byte) { unknownBlockID := ids.GenerateTestID() return toMessageBytes(unknownBlockID), nil }, @@ -323,8 +369,9 @@ func TestBlockSignatures(t *testing.T) { }, } - for name, test := range tests { + for _, tt := range tests { for _, withCache := range []bool{true, false} { + name := tt.name if withCache { name += "_with_cache" } else { @@ -341,21 +388,20 @@ func TestBlockSignatures(t *testing.T) { v := NewVerifier(db, blockStore, nil) handler := NewHandler(sigCache, v, snowCtx.WarpSigner) - requestBytes, expectedResponse := test.setup() + requestBytes, wantResponse := tt.setup() protoMsg := &sdk.SignatureRequest{Message: requestBytes} protoBytes, err := proto.Marshal(protoMsg) require.NoError(t, err) responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, test.err) + require.ErrorIs(t, appErr, tt.err) - test.verifyStats(t, v) + tt.verifyStats(t, v) - // If the expected response is empty, require that the handler returns an empty response and return early. - if len(expectedResponse) == 0 { - require.Empty(t, responseBytes, "expected response to be empty") + if len(wantResponse) == 0 { + require.Empty(t, responseBytes) return } - // check cache is populated (handler's cache) + if withCache { require.NotZero(t, sigCache.Len()) } else { @@ -363,14 +409,14 @@ func TestBlockSignatures(t *testing.T) { } var response sdk.SignatureResponse err = proto.Unmarshal(responseBytes, &response) - require.NoError(t, err, "error unmarshalling SignatureResponse") - require.Equal(t, expectedResponse, response.Signature) + require.NoError(t, err) + require.Equal(t, wantResponse, response.Signature) }) } } } -func TestUptimeSignatures(t *testing.T) { +func TestHandlerUptimeSignature(t *testing.T) { database := memdb.New() snowCtx := snowtest.Context(t, snowtest.CChainID) @@ -379,9 +425,10 @@ func TestUptimeSignatures(t *testing.T) { startTime := uint64(time.Now().Unix()) getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID) ([]byte, *warp.UnsignedMessage) { - uptimePayload, err := message.NewValidatorUptime(vID, 80) + uptimePayload := &ValidatorUptime{ValidationID: vID, TotalUptime: 80} + uptimeBytes, err := uptimePayload.Bytes() require.NoError(t, err) - addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimePayload.Bytes()) + addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimeBytes) require.NoError(t, err) unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) require.NoError(t, err) @@ -400,7 +447,6 @@ func TestUptimeSignatures(t *testing.T) { sigCache = &cache.Empty[ids.ID, []byte]{} } - // Create a validator state that includes our test validator // TODO(JonathanOppenheimer): see func NewTestValidatorState() -- this should be examined // when we address the issue of that function. validatorState := &validatorstest.State{ @@ -465,10 +511,10 @@ func TestUptimeSignatures(t *testing.T) { protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) responseBytes, appErr := handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) require.Nil(t, appErr) - expectedSignature, err := snowCtx.WarpSigner.Sign(msg) + wantSignature, err := snowCtx.WarpSigner.Sign(msg) require.NoError(t, err) response := &sdk.SignatureResponse{} require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.Equal(t, expectedSignature, response.Signature) + require.Equal(t, wantSignature, response.Signature) } } From ee060e352dd0527c37cb0d782db10c9ffe468c39 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 11:52:02 -0500 Subject: [PATCH 35/53] refactor: remove no cache testing --- vms/evm/warp/verifier_test.go | 274 ++++++++++++++-------------------- 1 file changed, 115 insertions(+), 159 deletions(-) diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index af7077200c55..238a0ceb5bcd 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" - "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" @@ -266,47 +265,29 @@ func TestHandlerMessageSignature(t *testing.T) { } for _, tt := range tests { - for _, withCache := range []bool{true, false} { - name := tt.name - if withCache { - name += "_with_cache" - } else { - name += "_no_cache" + t.Run(tt.name, func(t *testing.T) { + sigCache := lru.NewCache[ids.ID, []byte](100) + db := NewDB(database) + v := NewVerifier(db, emptyBlockStore, nil) + handler := NewHandler(sigCache, v, snowCtx.WarpSigner) + + requestBytes, wantResponse := tt.setup(db) + protoMsg := &sdk.SignatureRequest{Message: requestBytes} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, tt.err) + tt.verifyStats(t, v) + + if len(wantResponse) == 0 { + require.Empty(t, responseBytes) + return } - t.Run(name, 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]{} - } - db := NewDB(database) - v := NewVerifier(db, emptyBlockStore, nil) - handler := NewHandler(sigCache, v, snowCtx.WarpSigner) - - requestBytes, wantResponse := tt.setup(db) - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, tt.err) - tt.verifyStats(t, v) - - if len(wantResponse) == 0 { - require.Empty(t, responseBytes) - return - } - - if withCache { - require.NotZero(t, sigCache.Len()) - } else { - require.Zero(t, sigCache.Len()) - } - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.Equal(t, wantResponse, response.Signature) - }) - } + + response := &sdk.SignatureResponse{} + require.NoError(t, proto.Unmarshal(responseBytes, response)) + require.Equal(t, wantResponse, response.Signature) + }) } } @@ -370,49 +351,31 @@ func TestHandlerBlockSignature(t *testing.T) { } for _, tt := range tests { - for _, withCache := range []bool{true, false} { - name := tt.name - if withCache { - name += "_with_cache" - } else { - name += "_no_cache" + t.Run(tt.name, func(t *testing.T) { + sigCache := lru.NewCache[ids.ID, []byte](100) + db := NewDB(database) + v := NewVerifier(db, blockStore, nil) + handler := NewHandler(sigCache, v, snowCtx.WarpSigner) + + requestBytes, wantResponse := tt.setup() + protoMsg := &sdk.SignatureRequest{Message: requestBytes} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, tt.err) + + tt.verifyStats(t, v) + + if len(wantResponse) == 0 { + require.Empty(t, responseBytes) + return } - t.Run(name, 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]{} - } - db := NewDB(database) - v := NewVerifier(db, blockStore, nil) - handler := NewHandler(sigCache, v, snowCtx.WarpSigner) - - requestBytes, wantResponse := tt.setup() - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, tt.err) - - tt.verifyStats(t, v) - - if len(wantResponse) == 0 { - require.Empty(t, responseBytes) - return - } - - if withCache { - require.NotZero(t, sigCache.Len()) - } else { - require.Zero(t, sigCache.Len()) - } - var response sdk.SignatureResponse - err = proto.Unmarshal(responseBytes, &response) - require.NoError(t, err) - require.Equal(t, wantResponse, response.Signature) - }) - } + + var response sdk.SignatureResponse + err = proto.Unmarshal(responseBytes, &response) + require.NoError(t, err) + require.Equal(t, wantResponse, response.Signature) + }) } } @@ -439,82 +402,75 @@ func TestHandlerUptimeSignature(t *testing.T) { return protoBytes, unsignedMessage } - for _, withCache := range []bool{true, false} { - var sigCache cache.Cacher[ids.ID, []byte] - if withCache { - sigCache = lru.NewCache[ids.ID, []byte](100) - } else { - sigCache = &cache.Empty[ids.ID, []byte]{} - } - - // TODO(JonathanOppenheimer): see func NewTestValidatorState() -- this should be examined - // when we address the issue of that function. - validatorState := &validatorstest.State{ - GetCurrentValidatorSetF: func(context.Context, ids.ID) (map[ids.ID]*validators.GetCurrentValidatorOutput, uint64, error) { - return map[ids.ID]*validators.GetCurrentValidatorOutput{ - validationID: { - ValidationID: validationID, - NodeID: nodeID, - Weight: 1, - StartTime: startTime, - IsActive: true, - IsL1Validator: true, - }, - }, 0, nil - }, - } + sigCache := lru.NewCache[ids.ID, []byte](100) + + // TODO(JonathanOppenheimer): see func NewTestValidatorState() -- this should be examined + // when we address the issue of that function. + validatorState := &validatorstest.State{ + GetCurrentValidatorSetF: func(context.Context, ids.ID) (map[ids.ID]*validators.GetCurrentValidatorOutput, uint64, error) { + return map[ids.ID]*validators.GetCurrentValidatorOutput{ + validationID: { + ValidationID: validationID, + NodeID: nodeID, + Weight: 1, + StartTime: startTime, + IsActive: true, + IsL1Validator: true, + }, + }, 0, nil + }, + } - clk := &mockable.Clock{} - uptimeTracker, err := uptimetracker.New( - validatorState, - snowCtx.SubnetID, - memdb.New(), - clk, - ) - require.NoError(t, err) + clk := &mockable.Clock{} + uptimeTracker, err := uptimetracker.New( + validatorState, + snowCtx.SubnetID, + memdb.New(), + clk, + ) + require.NoError(t, err) - require.NoError(t, uptimeTracker.Sync(t.Context())) - - db := NewDB(database) - verifier := NewVerifier(db, emptyBlockStore, uptimeTracker) - handler := NewHandler(sigCache, verifier, snowCtx.WarpSigner) - - // sourceAddress nonZero - protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) - _, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, "2: source address should be empty for offchain addressed messages", appErr.Error()) - - // not existing validationID - vID := ids.GenerateTestID() - protoBytes, _ = getUptimeMessageBytes([]byte{}, vID) - _, appErr = handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: failed to get uptime: validationID not found: %s", vID), appErr.Error()) - - // uptime is less than requested (not connected) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: current uptime 0 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - - // uptime is less than requested (not enough time) - require.NoError(t, uptimeTracker.Connect(nodeID)) - clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: current uptime 40 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - - // valid uptime (enough time has passed) - clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) - responseBytes, appErr := handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.Nil(t, appErr) - wantSignature, err := snowCtx.WarpSigner.Sign(msg) - require.NoError(t, err) - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.Equal(t, wantSignature, response.Signature) - } + require.NoError(t, uptimeTracker.Sync(t.Context())) + + db := NewDB(database) + verifier := NewVerifier(db, emptyBlockStore, uptimeTracker) + handler := NewHandler(sigCache, verifier, snowCtx.WarpSigner) + + // sourceAddress nonZero + protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) + _, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Equal(t, "2: source address should be empty for offchain addressed messages", appErr.Error()) + + // not existing validationID + vID := ids.GenerateTestID() + protoBytes, _ = getUptimeMessageBytes([]byte{}, vID) + _, appErr = handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Equal(t, fmt.Sprintf("2: failed to get uptime: validationID not found: %s", vID), appErr.Error()) + + // uptime is less than requested (not connected) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) + _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Equal(t, fmt.Sprintf("2: current uptime 0 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) + + // uptime is less than requested (not enough time) + require.NoError(t, uptimeTracker.Connect(nodeID)) + clk.Set(clk.Time().Add(40 * time.Second)) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) + _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Equal(t, fmt.Sprintf("2: current uptime 40 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) + + // valid uptime (enough time has passed) + clk.Set(clk.Time().Add(40 * time.Second)) + protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) + responseBytes, appErr := handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) + require.Nil(t, appErr) + wantSignature, err := snowCtx.WarpSigner.Sign(msg) + require.NoError(t, err) + response := &sdk.SignatureResponse{} + require.NoError(t, proto.Unmarshal(responseBytes, response)) + require.Equal(t, wantSignature, response.Signature) } From 19e89ab4a17c90a4ac4b59035aadf5c407650836 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 11:56:37 -0500 Subject: [PATCH 36/53] test: remove duplicative test --- vms/evm/warp/verifier_test.go | 68 ++++++++++++----------------------- 1 file changed, 23 insertions(+), 45 deletions(-) diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index 238a0ceb5bcd..cbd9e8c8bb94 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -77,6 +77,29 @@ func TestCodecSerialization(t *testing.T) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x39, }, }, + { + name: "max uptime", + msg: &ValidatorUptime{ + ValidationID: ids.ID{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }, + TotalUptime: ^uint64(0), + }, + wantBytes: []byte{ + // Codec version (0) + 0x00, 0x00, + // ValidationID (32 bytes of 0xff) + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // TotalUptime (8 bytes, max uint64 in big-endian) + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }, + }, } for _, tt := range tests { @@ -97,51 +120,6 @@ func TestCodecSerialization(t *testing.T) { } } -// TestCodecRoundTrip verifies that messages can be marshaled and unmarshaled -// without data loss. -func TestCodecRoundTrip(t *testing.T) { - tests := []struct { - name string - msg *ValidatorUptime - }{ - { - name: "zero values", - msg: &ValidatorUptime{ - ValidationID: ids.Empty, - TotalUptime: 0, - }, - }, - { - name: "max uptime", - msg: &ValidatorUptime{ - ValidationID: ids.GenerateTestID(), - TotalUptime: ^uint64(0), // max uint64 - }, - }, - { - name: "random values", - msg: &ValidatorUptime{ - ValidationID: ids.GenerateTestID(), - TotalUptime: 987654321, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bytes, err := Codec.Marshal(CodecVersion, tt.msg) - require.NoError(t, err) - - var gotMsg ValidatorUptime - version, err := Codec.Unmarshal(bytes, &gotMsg) - require.NoError(t, err) - require.Equal(t, uint16(CodecVersion), version) - require.Equal(t, tt.msg.ValidationID, gotMsg.ValidationID) - require.Equal(t, tt.msg.TotalUptime, gotMsg.TotalUptime) - }) - } -} - func TestVerifierKnownMessage(t *testing.T) { db := memdb.New() From ed438fc1e8a553c55deaf613ec4849395a374469 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 11:57:37 -0500 Subject: [PATCH 37/53] fix: correct NewService argument calls --- graft/coreth/plugin/evm/vm.go | 10 ++++++++-- graft/subnet-evm/plugin/evm/vm.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index 7f5e11d8922e..a49f4b45db88 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -457,7 +457,10 @@ func (vm *VM) Initialize( // Create warp API (signatureAggregator will be nil until createAPIs) vm.warpAPI, err = warp.NewService( - vm.ctx, + vm.ctx.NetworkID, + vm.ctx.ChainID, + vm.ctx.SubnetID, + vm.ctx.ValidatorState, vm.warpMsgDB, vm.ctx.WarpSigner, vm.warpVerifier, @@ -1093,7 +1096,10 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) warpAPI, err := warp.NewService( - vm.ctx, + vm.ctx.NetworkID, + vm.ctx.ChainID, + vm.ctx.SubnetID, + vm.ctx.ValidatorState, vm.warpMsgDB, vm.ctx.WarpSigner, vm.warpVerifier, diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index 2fb8c188e937..aa8dff2060de 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -488,7 +488,10 @@ func (vm *VM) Initialize( // Create warp API (signatureAggregator will be nil until createAPIs) vm.warpAPI, err = warp.NewService( - vm.ctx, + vm.ctx.NetworkID, + vm.ctx.ChainID, + vm.ctx.SubnetID, + vm.ctx.ValidatorState, vm.warpMsgDB, vm.ctx.WarpSigner, vm.warpVerifier, @@ -1219,7 +1222,10 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) warpAPI, err := warp.NewService( - vm.ctx, + vm.ctx.NetworkID, + vm.ctx.ChainID, + vm.ctx.SubnetID, + vm.ctx.ValidatorState, vm.warpMsgDB, vm.ctx.WarpSigner, vm.warpVerifier, From 7ae577ab2b8de34c5554af3f0e83bfd4320334fa Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 12:03:15 -0500 Subject: [PATCH 38/53] refactor: unexport codec --- vms/evm/warp/verifier.go | 14 +++++++------- vms/evm/warp/verifier_test.go | 6 ++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/vms/evm/warp/verifier.go b/vms/evm/warp/verifier.go index cfb044848392..1e86c1491afa 100644 --- a/vms/evm/warp/verifier.go +++ b/vms/evm/warp/verifier.go @@ -26,23 +26,23 @@ const ( ParseErrCode = iota + 1 VerifyErrCode - CodecVersion = 0 - MaxMessageSize = 24 * units.KiB + codecVersion = 0 + maxMessageSize = 24 * units.KiB ) var ( _ acp118.Verifier = (*acp118Handler)(nil) - Codec codec.Manager + c codec.Manager ) func init() { - Codec = codec.NewManager(MaxMessageSize) + c = codec.NewManager(maxMessageSize) lc := linearcodec.NewDefault() err := errors.Join( lc.RegisterType(&ValidatorUptime{}), - Codec.RegisterCodec(CodecVersion, lc), + c.RegisterCodec(codecVersion, lc), ) if err != nil { panic(err) @@ -59,7 +59,7 @@ type ValidatorUptime struct { // ParseValidatorUptime converts a slice of bytes into a ValidatorUptime. func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { var msg ValidatorUptime - if _, err := Codec.Unmarshal(b, &msg); err != nil { + if _, err := c.Unmarshal(b, &msg); err != nil { return nil, err } return &msg, nil @@ -67,7 +67,7 @@ func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { // Bytes returns the binary representation of this payload. func (v *ValidatorUptime) Bytes() ([]byte, error) { - return Codec.Marshal(CodecVersion, v) + return c.Marshal(codecVersion, v) } // BlockStore provides access to accepted blocks. diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index cbd9e8c8bb94..e7ab15e085af 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -106,14 +106,12 @@ func TestCodecSerialization(t *testing.T) { t.Run(tt.name, func(t *testing.T) { require := require.New(t) - gotBytes, err := Codec.Marshal(CodecVersion, tt.msg) + gotBytes, err := tt.msg.Bytes() require.NoError(err) require.Equal(tt.wantBytes, gotBytes) - var gotMsg ValidatorUptime - version, err := Codec.Unmarshal(tt.wantBytes, &gotMsg) + gotMsg, err := ParseValidatorUptime(tt.wantBytes) require.NoError(err) - require.Equal(uint16(CodecVersion), version) require.Equal(tt.msg.ValidationID, gotMsg.ValidationID) require.Equal(tt.msg.TotalUptime, gotMsg.TotalUptime) }) From a1c447c65065459f0eb3b6ac4b0e1849893b0982 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 12:09:33 -0500 Subject: [PATCH 39/53] chore: update metric names --- vms/evm/warp/verifier.go | 28 ++++++++++++++-------------- vms/evm/warp/verifier_test.go | 8 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/vms/evm/warp/verifier.go b/vms/evm/warp/verifier.go index 1e86c1491afa..53b350955fc9 100644 --- a/vms/evm/warp/verifier.go +++ b/vms/evm/warp/verifier.go @@ -81,10 +81,10 @@ type Verifier struct { blockClient BlockStore uptimeTracker *uptimetracker.UptimeTracker - messageParseFail metrics.Counter - addressedCallValidationFail metrics.Counter - blockValidationFail metrics.Counter - uptimeValidationFail metrics.Counter + messageParseFail metrics.Counter + addressedCallVerifyFail metrics.Counter + blockVerifyFail metrics.Counter + uptimeVerifyFail metrics.Counter } // NewVerifier creates a new warp message verifier. @@ -94,13 +94,13 @@ func NewVerifier( uptimeTracker *uptimetracker.UptimeTracker, ) *Verifier { return &Verifier{ - db: db, - blockClient: blockClient, - uptimeTracker: uptimeTracker, - messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), - addressedCallValidationFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_validation_fail", nil), - blockValidationFail: metrics.NewRegisteredCounter("warp_backend_block_validation_fail", nil), - uptimeValidationFail: metrics.NewRegisteredCounter("warp_backend_uptime_validation_fail", nil), + db: db, + blockClient: blockClient, + uptimeTracker: uptimeTracker, + messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), + addressedCallVerifyFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_verify_fail", nil), + blockVerifyFail: metrics.NewRegisteredCounter("warp_backend_block_verify_fail", nil), + uptimeVerifyFail: metrics.NewRegisteredCounter("warp_backend_uptime_verify_fail", nil), } } @@ -145,7 +145,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { blockID := blockHashPayload.Hash if err := v.blockClient.HasBlock(ctx, blockID); err != nil { - v.blockValidationFail.Inc(1) + v.blockVerifyFail.Inc(1) return &common.AppError{ Code: VerifyErrCode, Message: fmt.Sprintf("failed to get block %s: %s", blockID, err), @@ -158,7 +158,7 @@ func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *pay // verifyOffchainAddressedCall verifies the addressed call message func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { if len(addressedCall.SourceAddress) != 0 { - v.addressedCallValidationFail.Inc(1) + v.addressedCallVerifyFail.Inc(1) return &common.AppError{ Code: VerifyErrCode, Message: "source address should be empty for offchain addressed messages", @@ -175,7 +175,7 @@ func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC } if err := v.verifyUptimeMessage(uptimeMsg); err != nil { - v.uptimeValidationFail.Inc(1) + v.uptimeVerifyFail.Inc(1) return err } diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index e7ab15e085af..407c4c1a82e7 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -220,7 +220,7 @@ func TestHandlerMessageSignature(t *testing.T) { }, verifyStats: func(t *testing.T, v *Verifier) { require.Zero(t, v.messageParseFail.Snapshot().Count()) - require.Zero(t, v.blockValidationFail.Snapshot().Count()) + require.Zero(t, v.blockVerifyFail.Snapshot().Count()) }, }, { @@ -234,7 +234,7 @@ func TestHandlerMessageSignature(t *testing.T) { }, verifyStats: func(t *testing.T, v *Verifier) { require.Equal(t, int64(1), v.messageParseFail.Snapshot().Count()) - require.Zero(t, v.blockValidationFail.Snapshot().Count()) + require.Zero(t, v.blockVerifyFail.Snapshot().Count()) }, err: &common.AppError{Code: ParseErrCode}, }, @@ -308,7 +308,7 @@ func TestHandlerBlockSignature(t *testing.T) { return toMessageBytes(knownBlkID), signature }, verifyStats: func(t *testing.T, v *Verifier) { - require.Zero(t, v.blockValidationFail.Snapshot().Count()) + require.Zero(t, v.blockVerifyFail.Snapshot().Count()) require.Zero(t, v.messageParseFail.Snapshot().Count()) }, }, @@ -319,7 +319,7 @@ func TestHandlerBlockSignature(t *testing.T) { return toMessageBytes(unknownBlockID), nil }, verifyStats: func(t *testing.T, v *Verifier) { - require.Equal(t, int64(1), v.blockValidationFail.Snapshot().Count()) + require.Equal(t, int64(1), v.blockVerifyFail.Snapshot().Count()) require.Zero(t, v.messageParseFail.Snapshot().Count()) }, err: &common.AppError{Code: VerifyErrCode}, From 47cb95f72669f409f028fb19966a49d315b34634 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 12:19:16 -0500 Subject: [PATCH 40/53] refactor: subnetIDStr to subnetID --- graft/coreth/tests/warp/warp_test.go | 14 +++++++------- graft/subnet-evm/tests/warp/warp_test.go | 14 +++++++------- vms/evm/warp/client.go | 8 ++++---- vms/evm/warp/service.go | 18 +++++------------- 4 files changed, 23 insertions(+), 31 deletions(-) diff --git a/graft/coreth/tests/warp/warp_test.go b/graft/coreth/tests/warp/warp_test.go index 8bab79bdd887..61b04cceddb6 100644 --- a/graft/coreth/tests/warp/warp_test.go +++ b/graft/coreth/tests/warp/warp_test.go @@ -353,12 +353,12 @@ func (w *warpTest) aggregateSignaturesViaAPI() { require.NoError(err) ginkgo.GinkgoLogr.Info("Fetching addressed call aggregate signature via p2p API") - subnetIDStr := "" + subnetID := ids.Empty if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID { - subnetIDStr = w.receivingSubnet.SubnetID.String() + subnetID = w.receivingSubnet.SubnetID } - signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, w.addressedCallUnsignedMessage.ID(), warp.WarpQuorumDenominator, subnetIDStr) + signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, w.addressedCallUnsignedMessage.ID(), warp.WarpQuorumDenominator, subnetID) require.NoError(err) parsedWarpMessage, err := avalancheWarp.ParseMessage(signedWarpMessageBytes) require.NoError(err) @@ -370,7 +370,7 @@ func (w *warpTest) aggregateSignaturesViaAPI() { w.addressedCallSignedMessage = parsedWarpMessage ginkgo.GinkgoLogr.Info("Fetching block payload aggregate signature via p2p API") - signedWarpBlockBytes, err := client.GetBlockAggregateSignature(ctx, w.blockID, warp.WarpQuorumDenominator, subnetIDStr) + signedWarpBlockBytes, err := client.GetBlockAggregateSignature(ctx, w.blockID, warp.WarpQuorumDenominator, subnetID) require.NoError(err) parsedWarpBlockMessage, err := avalancheWarp.ParseMessage(signedWarpBlockBytes) require.NoError(err) @@ -560,9 +560,9 @@ func (w *warpTest) warpLoad() { warpClient, err := warpBackend.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) require.NoError(err) - subnetIDStr := "" + subnetID := ids.Empty if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID { - subnetIDStr = w.receivingSubnet.SubnetID.String() + subnetID = w.receivingSubnet.SubnetID } ginkgo.GinkgoLogr.Info("Executing warp delivery sequences...") @@ -576,7 +576,7 @@ func (w *warpTest) warpLoad() { } ginkgo.GinkgoLogr.Info("Fetching addressed call aggregate signature via p2p API") - signedWarpMessageBytes, err := warpClient.GetMessageAggregateSignature(ctx, unsignedMessage.ID(), warp.WarpDefaultQuorumNumerator, subnetIDStr) + signedWarpMessageBytes, err := warpClient.GetMessageAggregateSignature(ctx, unsignedMessage.ID(), warp.WarpDefaultQuorumNumerator, subnetID) if err != nil { return nil, err } diff --git a/graft/subnet-evm/tests/warp/warp_test.go b/graft/subnet-evm/tests/warp/warp_test.go index c364f753e41d..5376fbcc1e3e 100644 --- a/graft/subnet-evm/tests/warp/warp_test.go +++ b/graft/subnet-evm/tests/warp/warp_test.go @@ -448,12 +448,12 @@ func (w *warpTest) aggregateSignaturesViaAPI() { require.NoError(err) log.Info("Fetching addressed call aggregate signature via p2p API") - subnetIDStr := "" + subnetID := ids.Empty if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID { - subnetIDStr = w.receivingSubnet.SubnetID.String() + subnetID = w.receivingSubnet.SubnetID } - signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, w.addressedCallUnsignedMessage.ID(), warpContract.WarpQuorumDenominator, subnetIDStr) + signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, w.addressedCallUnsignedMessage.ID(), warpContract.WarpQuorumDenominator, subnetID) require.NoError(err) parsedWarpMessage, err := avalancheWarp.ParseMessage(signedWarpMessageBytes) require.NoError(err) @@ -465,7 +465,7 @@ func (w *warpTest) aggregateSignaturesViaAPI() { w.addressedCallSignedMessage = parsedWarpMessage log.Info("Fetching block payload aggregate signature via p2p API") - signedWarpBlockBytes, err := client.GetBlockAggregateSignature(ctx, w.blockID, warpContract.WarpQuorumDenominator, subnetIDStr) + signedWarpBlockBytes, err := client.GetBlockAggregateSignature(ctx, w.blockID, warpContract.WarpQuorumDenominator, subnetID) require.NoError(err) parsedWarpBlockMessage, err := avalancheWarp.ParseMessage(signedWarpBlockBytes) require.NoError(err) @@ -715,9 +715,9 @@ func (w *warpTest) warpLoad() { warpClient, err := warp.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) require.NoError(err) - subnetIDStr := "" + subnetID := ids.Empty if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID { - subnetIDStr = w.receivingSubnet.SubnetID.String() + subnetID = w.receivingSubnet.SubnetID } log.Info("Executing warp delivery sequences...") @@ -731,7 +731,7 @@ func (w *warpTest) warpLoad() { } log.Info("Fetching addressed call aggregate signature via p2p API") - signedWarpMessageBytes, err := warpClient.GetMessageAggregateSignature(ctx, unsignedMessage.ID(), warpContract.WarpDefaultQuorumNumerator, subnetIDStr) + signedWarpMessageBytes, err := warpClient.GetMessageAggregateSignature(ctx, unsignedMessage.ID(), warpContract.WarpDefaultQuorumNumerator, subnetID) if err != nil { return nil, err } diff --git a/vms/evm/warp/client.go b/vms/evm/warp/client.go index a49cb6a8844a..2b68050eae26 100644 --- a/vms/evm/warp/client.go +++ b/vms/evm/warp/client.go @@ -44,9 +44,9 @@ func (c *Client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]b return res, nil } -func (c *Client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { +func (c *Client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetID ids.ID) ([]byte, error) { var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetIDStr); err != nil { + if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetID); err != nil { return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed: %w", err) } return res, nil @@ -60,9 +60,9 @@ func (c *Client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, return res, nil } -func (c *Client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { +func (c *Client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetID ids.ID) ([]byte, error) { var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetIDStr); err != nil { + if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetID); err != nil { return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed: %w", err) } return res, nil diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 65717c3c6b65..516073ad3e6c 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -153,16 +153,16 @@ func (a *Service) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexuti } // GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] -func (a *Service) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { +func (a *Service) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { unsignedMessage, err := a.getMessage(messageID) if err != nil { return nil, err } - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) + return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) } // GetBlockAggregateSignature fetches the aggregate signature for the requested [blockID] -func (a *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { +func (a *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { blockHashPayload, err := payload.NewHash(blockID) if err != nil { return nil, err @@ -172,18 +172,10 @@ func (a *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID return nil, err } - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) + return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) } -func (a *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetIDStr string) (hexutil.Bytes, error) { - subnetID := a.subnetID - if len(subnetIDStr) > 0 { - sid, err := ids.FromString(subnetIDStr) - if err != nil { - return nil, fmt.Errorf("failed to parse subnetID: %q", subnetIDStr) - } - subnetID = sid - } +func (a *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetID ids.ID) (hexutil.Bytes, error) { validatorState := a.validatorState pChainHeight, err := validatorState.GetCurrentHeight(ctx) if err != nil { From 1f7d078ffe532abc235b0c623b5bd667d63f7084 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 12:24:57 -0500 Subject: [PATCH 41/53] chore: lint --- graft/coreth/plugin/evm/vm.go | 2 -- graft/subnet-evm/plugin/evm/vm.go | 2 -- vms/evm/warp/service.go | 3 --- vms/evm/warp/verifier.go | 3 ++- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index a49f4b45db88..aa0aa6cbb4f9 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -459,7 +459,6 @@ func (vm *VM) Initialize( vm.warpAPI, err = warp.NewService( vm.ctx.NetworkID, vm.ctx.ChainID, - vm.ctx.SubnetID, vm.ctx.ValidatorState, vm.warpMsgDB, vm.ctx.WarpSigner, @@ -1098,7 +1097,6 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpAPI, err := warp.NewService( vm.ctx.NetworkID, vm.ctx.ChainID, - vm.ctx.SubnetID, vm.ctx.ValidatorState, vm.warpMsgDB, vm.ctx.WarpSigner, diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index aa8dff2060de..cac4555b9060 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -490,7 +490,6 @@ func (vm *VM) Initialize( vm.warpAPI, err = warp.NewService( vm.ctx.NetworkID, vm.ctx.ChainID, - vm.ctx.SubnetID, vm.ctx.ValidatorState, vm.warpMsgDB, vm.ctx.WarpSigner, @@ -1224,7 +1223,6 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpAPI, err := warp.NewService( vm.ctx.NetworkID, vm.ctx.ChainID, - vm.ctx.SubnetID, vm.ctx.ValidatorState, vm.warpMsgDB, vm.ctx.WarpSigner, diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 516073ad3e6c..a1c705b3772c 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -32,7 +32,6 @@ var ( type Service struct { networkID uint32 chainID ids.ID - subnetID ids.ID validatorState validators.State db *DB @@ -48,7 +47,6 @@ type Service struct { func NewService( networkID uint32, chainID ids.ID, - subnetID ids.ID, validatorState validators.State, db *DB, signer warp.Signer, @@ -82,7 +80,6 @@ func NewService( return &Service{ networkID: networkID, chainID: chainID, - subnetID: subnetID, validatorState: validatorState, db: db, signer: signer, diff --git a/vms/evm/warp/verifier.go b/vms/evm/warp/verifier.go index 53b350955fc9..8608c870ac8f 100644 --- a/vms/evm/warp/verifier.go +++ b/vms/evm/warp/verifier.go @@ -8,6 +8,8 @@ import ( "errors" "fmt" + "github.com/ava-labs/libevm/metrics" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/codec/linearcodec" @@ -19,7 +21,6 @@ import ( "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/ava-labs/libevm/metrics" ) const ( From 03672f23964a52720f93113897414c5a4eb493f3 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 12:34:00 -0500 Subject: [PATCH 42/53] chore: lint --- vms/evm/warp/verifier_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index 407c4c1a82e7..169236f68f96 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -348,8 +348,7 @@ func TestHandlerBlockSignature(t *testing.T) { } var response sdk.SignatureResponse - err = proto.Unmarshal(responseBytes, &response) - require.NoError(t, err) + require.NoError(t, proto.Unmarshal(responseBytes, &response)) require.Equal(t, wantResponse, response.Signature) }) } From 2fea5455a6ed0d587d891040b721794ca946aad9 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 12:53:16 -0500 Subject: [PATCH 43/53] fix: double initilization --- graft/coreth/plugin/evm/vm.go | 23 +++--------- graft/subnet-evm/plugin/evm/vm.go | 24 ++++-------- vms/evm/warp/service.go | 62 +++++++++++++++++-------------- 3 files changed, 47 insertions(+), 62 deletions(-) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index aa0aa6cbb4f9..0061926edf32 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -455,7 +455,8 @@ func (vm *VM) Initialize( vm.warpMsgDB = warp.NewDB(vm.warpDB) vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, nil) - // Create warp API (signatureAggregator will be nil until createAPIs) + // Create warp API. The signatureAggregator will be set later in CreateHandlers + // when the p2p network client becomes available. vm.warpAPI, err = warp.NewService( vm.ctx.NetworkID, vm.ctx.ChainID, @@ -464,7 +465,7 @@ func (vm *VM) Initialize( vm.ctx.WarpSigner, vm.warpVerifier, vm.warpSignatureCache, - nil, // signatureAggregator is set later in createAPIs + nil, // signatureAggregator is set in CreateHandlers via SetSignatureAggregator vm.offchainWarpMessages, ) if err != nil { @@ -1094,21 +1095,9 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.Network.NewClient(p2p.SignatureRequestHandlerID) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - warpAPI, err := warp.NewService( - vm.ctx.NetworkID, - vm.ctx.ChainID, - vm.ctx.ValidatorState, - vm.warpMsgDB, - vm.ctx.WarpSigner, - vm.warpVerifier, - vm.warpSignatureCache, - signatureAggregator, - vm.offchainWarpMessages, - ) - if err != nil { - return nil, err - } - if err := handler.RegisterName("warp", warpAPI); err != nil { + vm.warpAPI.SetSignatureAggregator(signatureAggregator) + + if err := handler.RegisterName("warp", vm.warpAPI); err != nil { return nil, err } enabledAPIs = append(enabledAPIs, "warp") diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index cac4555b9060..3ac97d40fae3 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -486,7 +486,8 @@ func (vm *VM) Initialize( vm.warpMsgDB = warp.NewDB(vm.warpDB) vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, vm.uptimeTracker) - // Create warp API (signatureAggregator will be nil until createAPIs) + // Create warp API. The signatureAggregator will be set later in CreateHandlers + // when the p2p network client becomes available. vm.warpAPI, err = warp.NewService( vm.ctx.NetworkID, vm.ctx.ChainID, @@ -495,7 +496,7 @@ func (vm *VM) Initialize( vm.ctx.WarpSigner, vm.warpVerifier, vm.warpSignatureCache, - nil, // signatureAggregator is set later in createAPIs + nil, // signatureAggregator is set in CreateHandlers via SetSignatureAggregator vm.offchainWarpMessages, ) if err != nil { @@ -1220,21 +1221,10 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.Network.NewClient(p2p.SignatureRequestHandlerID) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - warpAPI, err := warp.NewService( - vm.ctx.NetworkID, - vm.ctx.ChainID, - vm.ctx.ValidatorState, - vm.warpMsgDB, - vm.ctx.WarpSigner, - vm.warpVerifier, - vm.warpSignatureCache, - signatureAggregator, - vm.offchainWarpMessages, - ) - if err != nil { - return nil, err - } - if err := handler.RegisterName("warp", warpAPI); err != nil { + // Set the signature aggregator on the existing warpAPI instance + vm.warpAPI.SetSignatureAggregator(signatureAggregator) + + if err := handler.RegisterName("warp", vm.warpAPI); err != nil { return nil, err } enabledAPIs = append(enabledAPIs, "warp") diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index a1c705b3772c..9dede3a823fb 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -92,8 +92,8 @@ func NewService( } // GetMessage returns the Warp message associated with a messageID. -func (a *Service) GetMessage(messageID ids.ID) (hexutil.Bytes, error) { - message, err := a.getMessage(messageID) +func (s *Service) GetMessage(messageID ids.ID) (hexutil.Bytes, error) { + message, err := s.getMessage(messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s: %w", messageID, err) } @@ -101,79 +101,85 @@ func (a *Service) GetMessage(messageID ids.ID) (hexutil.Bytes, error) { } // getMessage retrieves a message from cache, offchain messages, or database. -func (a *Service) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { - if msg, ok := a.messageCache.Get(messageID); ok { +func (s *Service) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { + if msg, ok := s.messageCache.Get(messageID); ok { return msg, nil } - if msg, ok := a.offChainMessages[messageID]; ok { + if msg, ok := s.offChainMessages[messageID]; ok { return msg, nil } - msg, err := a.db.Get(messageID) + msg, err := s.db.Get(messageID) if err != nil { return nil, err } - a.messageCache.Put(messageID, msg) + s.messageCache.Put(messageID, msg) return msg, nil } // GetMessageSignature returns the BLS signature associated with a messageID. -func (a *Service) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - unsignedMessage, err := a.getMessage(messageID) +func (s *Service) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { + unsignedMessage, err := s.getMessage(messageID) if err != nil { return nil, fmt.Errorf("%w %s: %w", ErrMessageNotFound, messageID, err) } - return a.signMessage(ctx, unsignedMessage) + return s.signMessage(ctx, unsignedMessage) } // GetBlockSignature returns the BLS signature associated with a blockID. // It constructs a warp message with a Hash payload containing the blockID, // then returns the signature for that message. -func (a *Service) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { +func (s *Service) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { blockHashPayload, err := payload.NewHash(blockID) if err != nil { return nil, fmt.Errorf("failed to create block hash payload: %w", err) } unsignedMessage, err := warp.NewUnsignedMessage( - a.networkID, - a.chainID, + s.networkID, + s.chainID, blockHashPayload.Bytes(), ) if err != nil { return nil, fmt.Errorf("failed to create unsigned warp message: %w", err) } - return a.signMessage(ctx, unsignedMessage) + return s.signMessage(ctx, unsignedMessage) +} + +// SetSignatureAggregator sets the signature aggregator for the service. +// This must be called before any aggregate signature methods are used. +func (s *Service) SetSignatureAggregator(signatureAggregator *acp118.SignatureAggregator) { + s.signatureAggregator = signatureAggregator } // GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] -func (a *Service) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { - unsignedMessage, err := a.getMessage(messageID) +func (s *Service) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { + unsignedMessage, err := s.getMessage(messageID) if err != nil { return nil, err } - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) + return s.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) } // GetBlockAggregateSignature fetches the aggregate signature for the requested [blockID] -func (a *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { +func (s *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { blockHashPayload, err := payload.NewHash(blockID) if err != nil { return nil, err } - unsignedMessage, err := warp.NewUnsignedMessage(a.networkID, a.chainID, blockHashPayload.Bytes()) + unsignedMessage, err := warp.NewUnsignedMessage(s.networkID, s.chainID, blockHashPayload.Bytes()) if err != nil { return nil, err } - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) + return s.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) } -func (a *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetID ids.ID) (hexutil.Bytes, error) { - validatorState := a.validatorState +func (s *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetID ids.ID) (hexutil.Bytes, error) { + validatorState := s.validatorState pChainHeight, err := validatorState.GetCurrentHeight(ctx) if err != nil { return nil, err @@ -197,7 +203,7 @@ func (a *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp UnsignedMessage: *unsignedMessage, Signature: &warp.BitSetSignature{}, } - signedMessage, _, _, err := a.signatureAggregator.AggregateSignatures( + signedMessage, _, _, err := s.signatureAggregator.AggregateSignatures( ctx, warpMessage, nil, @@ -215,25 +221,25 @@ func (a *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp } // signMessage verifies, signs, and caches a signature for the given unsigned message. -func (a *Service) signMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) (hexutil.Bytes, error) { +func (s *Service) signMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) (hexutil.Bytes, error) { msgID := unsignedMessage.ID() - if sig, ok := a.signatureCache.Get(msgID); ok { + if sig, ok := s.signatureCache.Get(msgID); ok { return sig, nil } - if err := a.verifier.Verify(ctx, unsignedMessage); err != nil { + if err := s.verifier.Verify(ctx, unsignedMessage); err != nil { if err.Code == VerifyErrCode { return nil, fmt.Errorf("%w: %w", ErrBlockNotFound, err) } return nil, fmt.Errorf("failed to verify message %s: %w", msgID, err) } - signature, err := a.signer.Sign(unsignedMessage) + signature, err := s.signer.Sign(unsignedMessage) if err != nil { return nil, fmt.Errorf("failed to sign message %s: %w", msgID, err) } - a.signatureCache.Put(msgID, signature) + s.signatureCache.Put(msgID, signature) return signature, nil } From 1a6239157b4107d97f8c5116ccd1aaf40e93ebbb Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 13:03:30 -0500 Subject: [PATCH 44/53] docs: improve function doc names --- vms/evm/warp/client.go | 5 +++++ vms/evm/warp/db.go | 2 +- vms/evm/warp/service.go | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/vms/evm/warp/client.go b/vms/evm/warp/client.go index 2b68050eae26..df84de8ce1d7 100644 --- a/vms/evm/warp/client.go +++ b/vms/evm/warp/client.go @@ -28,6 +28,7 @@ func NewClient(uri, chain string) (*Client, error) { }, nil } +// GetMessage returns the Warp message associated with the given messageID. func (c *Client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessage", messageID); err != nil { @@ -36,6 +37,7 @@ func (c *Client) GetMessage(ctx context.Context, messageID ids.ID) ([]byte, erro return res, nil } +// GetMessageSignature returns the BLS signature associated with the given messageID. func (c *Client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessageSignature", messageID); err != nil { @@ -44,6 +46,7 @@ func (c *Client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]b return res, nil } +// GetMessageAggregateSignature fetches the aggregate signature for the given messageID. func (c *Client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetID); err != nil { @@ -52,6 +55,7 @@ func (c *Client) GetMessageAggregateSignature(ctx context.Context, messageID ids return res, nil } +// GetBlockSignature returns the BLS signature associated with the given blockID. func (c *Client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getBlockSignature", blockID); err != nil { @@ -60,6 +64,7 @@ func (c *Client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, return res, nil } +// GetBlockAggregateSignature fetches the aggregate signature for the given blockID. func (c *Client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetID ids.ID) ([]byte, error) { var res hexutil.Bytes if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetID); err != nil { diff --git a/vms/evm/warp/db.go b/vms/evm/warp/db.go index 0ee9c3c21c70..17e755d1e27e 100644 --- a/vms/evm/warp/db.go +++ b/vms/evm/warp/db.go @@ -34,7 +34,7 @@ func (d *DB) Add(unsignedMsg *warp.UnsignedMessage) error { return nil } -// Get retrieves a warp message from the database. +// Get retrieves a warp message for the given msgID from the database. func (d *DB) Get(msgID ids.ID) (*warp.UnsignedMessage, error) { unsignedMessageBytes, err := d.db.Get(msgID[:]) if err != nil { diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 9dede3a823fb..776ff6567417 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -91,7 +91,7 @@ func NewService( }, nil } -// GetMessage returns the Warp message associated with a messageID. +// GetMessage returns the Warp message associated with the given messageID. func (s *Service) GetMessage(messageID ids.ID) (hexutil.Bytes, error) { message, err := s.getMessage(messageID) if err != nil { @@ -119,7 +119,7 @@ func (s *Service) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { return msg, nil } -// GetMessageSignature returns the BLS signature associated with a messageID. +// GetMessageSignature returns the BLS signature associated with the given messageID. func (s *Service) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { unsignedMessage, err := s.getMessage(messageID) if err != nil { @@ -128,7 +128,7 @@ func (s *Service) GetMessageSignature(ctx context.Context, messageID ids.ID) (he return s.signMessage(ctx, unsignedMessage) } -// GetBlockSignature returns the BLS signature associated with a blockID. +// GetBlockSignature returns the BLS signature associated with the given blockID. // It constructs a warp message with a Hash payload containing the blockID, // then returns the signature for that message. func (s *Service) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { @@ -155,7 +155,7 @@ func (s *Service) SetSignatureAggregator(signatureAggregator *acp118.SignatureAg s.signatureAggregator = signatureAggregator } -// GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] +// GetMessageAggregateSignature fetches the aggregate signature for the requested messageID. func (s *Service) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { unsignedMessage, err := s.getMessage(messageID) if err != nil { @@ -164,7 +164,7 @@ func (s *Service) GetMessageAggregateSignature(ctx context.Context, messageID id return s.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) } -// GetBlockAggregateSignature fetches the aggregate signature for the requested [blockID] +// GetBlockAggregateSignature fetches the aggregate signature for the requested blockID. func (s *Service) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { blockHashPayload, err := payload.NewHash(blockID) if err != nil { From 134abe131f6bd09bfa20a11c45a56c4e9494408b Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 14:16:35 -0500 Subject: [PATCH 45/53] refactor: rewrite tests --- vms/evm/warp/db_test.go | 47 ++++ vms/evm/warp/helpers_test.go | 117 +++++++++ vms/evm/warp/verifier.go | 69 ++++-- vms/evm/warp/verifier_test.go | 430 +++++++++++++++++++--------------- 4 files changed, 454 insertions(+), 209 deletions(-) diff --git a/vms/evm/warp/db_test.go b/vms/evm/warp/db_test.go index fe08ac92448d..0f14836bee40 100644 --- a/vms/evm/warp/db_test.go +++ b/vms/evm/warp/db_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" "github.com/ava-labs/avalanchego/vms/platformvm/warp" @@ -31,3 +32,49 @@ func TestDBAddAndSign(t *testing.T) { require.NoError(t, err) require.Equal(t, wantSig, signature) } + +func TestDBGet(t *testing.T) { + db := memdb.New() + messageDB := NewDB(db) + + require.NoError(t, messageDB.Add(testUnsignedMessage)) + + gotMsg, err := messageDB.Get(testUnsignedMessage.ID()) + require.NoError(t, err) + require.Equal(t, testUnsignedMessage.Bytes(), gotMsg.Bytes()) + require.Equal(t, testUnsignedMessage.ID(), gotMsg.ID()) +} + +func TestDBGetNotFound(t *testing.T) { + db := memdb.New() + messageDB := NewDB(db) + + unknownID := testUnsignedMessage.ID() + _, err := messageDB.Get(unknownID) + require.ErrorIs(t, err, database.ErrNotFound) +} + +func TestDBGetCorruptedData(t *testing.T) { + db := memdb.New() + messageDB := NewDB(db) + + corruptedBytes := []byte{0xFF, 0xFF, 0xFF} + msgID := testUnsignedMessage.ID() + require.NoError(t, db.Put(msgID[:], corruptedBytes)) + + _, err := messageDB.Get(msgID) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse unsigned message") +} + +func TestDBAddDuplicate(t *testing.T) { + db := memdb.New() + messageDB := NewDB(db) + + require.NoError(t, messageDB.Add(testUnsignedMessage)) + require.NoError(t, messageDB.Add(testUnsignedMessage)) + + gotMsg, err := messageDB.Get(testUnsignedMessage.ID()) + require.NoError(t, err) + require.Equal(t, testUnsignedMessage.Bytes(), gotMsg.Bytes()) +} diff --git a/vms/evm/warp/helpers_test.go b/vms/evm/warp/helpers_test.go index adc7c7803561..5b4ddbd6db3e 100644 --- a/vms/evm/warp/helpers_test.go +++ b/vms/evm/warp/helpers_test.go @@ -5,11 +5,24 @@ package warp import ( "context" + "testing" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/p2p/p2ptest" + "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/evm/uptimetracker" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) @@ -45,3 +58,107 @@ func (t testBlockStore) HasBlock(_ context.Context, blockID ids.ID) error { } return nil } + +// metricExpectations specifies expected metric values for test assertions +type metricExpectations struct { + messageParseFail int64 + addressedCallVerifyFail int64 + blockVerifyFail int64 + uptimeVerifyFail int64 +} + +// requireMetrics verifies all metrics in the Verifier match expectations +func requireMetrics(t *testing.T, v *Verifier, expected metricExpectations) { + t.Helper() + require.Equal(t, float64(expected.messageParseFail), testutil.ToFloat64(v.messageParseFail)) + require.Equal(t, float64(expected.addressedCallVerifyFail), testutil.ToFloat64(v.addressedCallVerifyFail)) + require.Equal(t, float64(expected.blockVerifyFail), testutil.ToFloat64(v.blockVerifyFail)) + require.Equal(t, float64(expected.uptimeVerifyFail), testutil.ToFloat64(v.uptimeVerifyFail)) +} + +// handlerTestSetup contains common test fixtures for handler tests +type handlerTestSetup struct { + client *p2p.Client + verifier *Verifier + db *DB + pk *bls.PublicKey + sigCache *lru.Cache[ids.ID, []byte] + serverNodeID ids.NodeID +} + +// setupHandler creates a handler with p2p client for testing +func setupHandler( + t *testing.T, + ctx context.Context, + database database.Database, + blockStore BlockStore, + uptimeTracker *uptimetracker.UptimeTracker, + networkID uint32, + chainID ids.ID, +) *handlerTestSetup { + sk, err := localsigner.New() + require.NoError(t, err) + pk := sk.PublicKey() + signer := warp.NewSigner(sk, networkID, chainID) + + sigCache := lru.NewCache[ids.ID, []byte](100) + db := NewDB(database) + v := NewVerifier(db, blockStore, uptimeTracker, prometheus.NewRegistry()) + handler := NewHandler(sigCache, v, signer) + + clientNodeID := ids.GenerateTestNodeID() + serverNodeID := ids.GenerateTestNodeID() + client := p2ptest.NewClient(t, ctx, clientNodeID, p2p.NoOpHandler{}, serverNodeID, handler) + + return &handlerTestSetup{ + client: client, + verifier: v, + db: db, + pk: pk, + sigCache: sigCache, + serverNodeID: serverNodeID, + } +} + +// sendSignatureRequest sends a signature request and returns the signature bytes +func sendSignatureRequest( + t *testing.T, + ctx context.Context, + setup *handlerTestSetup, + message *warp.UnsignedMessage, + wantError bool, +) []byte { + t.Helper() + + request := &sdk.SignatureRequest{ + Message: message.Bytes(), + } + requestBytes, err := proto.Marshal(request) + require.NoError(t, err) + + var signature []byte + responseChan := make(chan struct{}) + onResponse := func(_ context.Context, _ ids.NodeID, responseBytes []byte, appErr error) { + defer close(responseChan) + + if wantError { + require.Error(t, appErr) + return + } + + require.NoError(t, appErr) + + var response sdk.SignatureResponse + require.NoError(t, proto.Unmarshal(responseBytes, &response)) + signature = response.Signature + + sig, err := bls.SignatureFromBytes(response.Signature) + require.NoError(t, err) + require.True(t, bls.Verify(setup.pk, sig, request.Message)) + } + + require.NoError(t, setup.client.AppRequest(ctx, set.Of(setup.serverNodeID), requestBytes, onResponse)) + <-responseChan + + return signature +} diff --git a/vms/evm/warp/verifier.go b/vms/evm/warp/verifier.go index 8608c870ac8f..bf8d0a56e7b6 100644 --- a/vms/evm/warp/verifier.go +++ b/vms/evm/warp/verifier.go @@ -8,7 +8,7 @@ import ( "errors" "fmt" - "github.com/ava-labs/libevm/metrics" + "github.com/prometheus/client_golang/prometheus" "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/codec" @@ -76,33 +76,60 @@ type BlockStore interface { HasBlock(ctx context.Context, blockID ids.ID) error } +// MessageDB provides access to warp messages. +type MessageDB interface { + Get(ids.ID) (*warp.UnsignedMessage, error) +} + // Verifier validates whether a warp message should be signed. type Verifier struct { - db *DB + db MessageDB blockClient BlockStore uptimeTracker *uptimetracker.UptimeTracker - messageParseFail metrics.Counter - addressedCallVerifyFail metrics.Counter - blockVerifyFail metrics.Counter - uptimeVerifyFail metrics.Counter + messageParseFail prometheus.Counter + addressedCallVerifyFail prometheus.Counter + blockVerifyFail prometheus.Counter + uptimeVerifyFail prometheus.Counter } // NewVerifier creates a new warp message verifier. func NewVerifier( - db *DB, + db MessageDB, blockClient BlockStore, uptimeTracker *uptimetracker.UptimeTracker, + reg prometheus.Registerer, ) *Verifier { - return &Verifier{ - db: db, - blockClient: blockClient, - uptimeTracker: uptimeTracker, - messageParseFail: metrics.NewRegisteredCounter("warp_backend_message_parse_fail", nil), - addressedCallVerifyFail: metrics.NewRegisteredCounter("warp_backend_addressed_call_verify_fail", nil), - blockVerifyFail: metrics.NewRegisteredCounter("warp_backend_block_verify_fail", nil), - uptimeVerifyFail: metrics.NewRegisteredCounter("warp_backend_uptime_verify_fail", nil), + v := &Verifier{ + db: db, + blockClient: blockClient, + uptimeTracker: uptimeTracker, + messageParseFail: prometheus.NewCounter(prometheus.CounterOpts{ + Name: "warp_backend_message_parse_fail", + Help: "Number of warp message parse failures", + }), + addressedCallVerifyFail: prometheus.NewCounter(prometheus.CounterOpts{ + Name: "warp_backend_addressed_call_verify_fail", + Help: "Number of addressed call verification failures", + }), + blockVerifyFail: prometheus.NewCounter(prometheus.CounterOpts{ + Name: "warp_backend_block_verify_fail", + Help: "Number of block verification failures", + }), + uptimeVerifyFail: prometheus.NewCounter(prometheus.CounterOpts{ + Name: "warp_backend_uptime_verify_fail", + Help: "Number of uptime verification failures", + }), } + + if reg != nil { + reg.MustRegister(v.messageParseFail) + reg.MustRegister(v.addressedCallVerifyFail) + reg.MustRegister(v.blockVerifyFail) + reg.MustRegister(v.uptimeVerifyFail) + } + + return v } // Verify validates whether a warp message should be signed. @@ -120,7 +147,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes parsed, err := payload.Parse(unsignedMessage.Payload) if err != nil { - v.messageParseFail.Inc(1) + v.messageParseFail.Inc() return &common.AppError{ Code: ParseErrCode, Message: fmt.Sprintf("failed to parse payload: %s", err), @@ -133,7 +160,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes case *payload.Hash: return v.verifyBlockMessage(ctx, p) default: - v.messageParseFail.Inc(1) + v.messageParseFail.Inc() return &common.AppError{ Code: ParseErrCode, Message: fmt.Sprintf("unknown payload type: %T", p), @@ -146,7 +173,7 @@ func (v *Verifier) Verify(ctx context.Context, unsignedMessage *warp.UnsignedMes func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { blockID := blockHashPayload.Hash if err := v.blockClient.HasBlock(ctx, blockID); err != nil { - v.blockVerifyFail.Inc(1) + v.blockVerifyFail.Inc() return &common.AppError{ Code: VerifyErrCode, Message: fmt.Sprintf("failed to get block %s: %s", blockID, err), @@ -159,7 +186,7 @@ func (v *Verifier) verifyBlockMessage(ctx context.Context, blockHashPayload *pay // verifyOffchainAddressedCall verifies the addressed call message func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { if len(addressedCall.SourceAddress) != 0 { - v.addressedCallVerifyFail.Inc(1) + v.addressedCallVerifyFail.Inc() return &common.AppError{ Code: VerifyErrCode, Message: "source address should be empty for offchain addressed messages", @@ -168,7 +195,7 @@ func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC uptimeMsg, err := ParseValidatorUptime(addressedCall.Payload) if err != nil { - v.messageParseFail.Inc(1) + v.messageParseFail.Inc() return &common.AppError{ Code: ParseErrCode, Message: fmt.Sprintf("failed to parse addressed call message: %s", err), @@ -176,7 +203,7 @@ func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedC } if err := v.verifyUptimeMessage(uptimeMsg); err != nil { - v.uptimeVerifyFail.Inc(1) + v.uptimeVerifyFail.Inc() return err } diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index 169236f68f96..0344b7eacc85 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -5,17 +5,15 @@ package warp import ( "context" - "fmt" "testing" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/snow/validators" @@ -126,11 +124,10 @@ func TestVerifierKnownMessage(t *testing.T) { warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageDB := NewDB(db) - verifier := NewVerifier(messageDB, nil, nil) + verifier := NewVerifier(messageDB, nil, nil, prometheus.NewRegistry()) require.NoError(t, messageDB.Add(testUnsignedMessage)) - // Known messages in the DB should pass verification appErr := verifier.Verify(t.Context(), testUnsignedMessage) require.Nil(t, appErr) @@ -140,15 +137,21 @@ func TestVerifierKnownMessage(t *testing.T) { wantSig, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) require.Equal(t, wantSig, signature) + + requireMetrics(t, verifier, metricExpectations{ + messageParseFail: 0, + addressedCallVerifyFail: 0, + blockVerifyFail: 0, + uptimeVerifyFail: 0, + }) } func TestVerifierUnknownMessage(t *testing.T) { db := memdb.New() messageDB := NewDB(db) - verifier := NewVerifier(messageDB, nil, nil) + verifier := NewVerifier(messageDB, nil, nil, prometheus.NewRegistry()) - // Create an unknown message with empty source address to test parse failure unknownPayload, err := payload.NewAddressedCall([]byte{}, []byte("unknown message")) require.NoError(t, err) unknownMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownPayload.Bytes()) @@ -156,6 +159,13 @@ func TestVerifierUnknownMessage(t *testing.T) { appErr := verifier.Verify(t.Context(), unknownMessage) require.ErrorIs(t, appErr, &common.AppError{Code: ParseErrCode}) + + requireMetrics(t, verifier, metricExpectations{ + messageParseFail: 1, + addressedCallVerifyFail: 0, + blockVerifyFail: 0, + uptimeVerifyFail: 0, + }) } func TestVerifierBlockMessage(t *testing.T) { @@ -168,7 +178,7 @@ func TestVerifierBlockMessage(t *testing.T) { warpSigner := warp.NewSigner(sk, networkID, sourceChainID) messageDB := NewDB(db) - verifier := NewVerifier(messageDB, blockStore, nil) + verifier := NewVerifier(messageDB, blockStore, nil, prometheus.NewRegistry()) blockHashPayload, err := payload.NewHash(blkID) require.NoError(t, err) @@ -177,7 +187,6 @@ func TestVerifierBlockMessage(t *testing.T) { wantSig, err := warpSigner.Sign(unsignedMessage) require.NoError(t, err) - // Known block should pass verification appErr := verifier.Verify(t.Context(), unsignedMessage) require.Nil(t, appErr) @@ -185,84 +194,118 @@ func TestVerifierBlockMessage(t *testing.T) { require.NoError(t, err) require.Equal(t, wantSig, signature) - // Unknown block should fail verification + requireMetrics(t, verifier, metricExpectations{ + messageParseFail: 0, + addressedCallVerifyFail: 0, + blockVerifyFail: 0, + uptimeVerifyFail: 0, + }) + unknownBlockHashPayload, err := payload.NewHash(ids.GenerateTestID()) require.NoError(t, err) unknownUnsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownBlockHashPayload.Bytes()) require.NoError(t, err) unknownAppErr := verifier.Verify(t.Context(), unknownUnsignedMessage) require.ErrorIs(t, unknownAppErr, &common.AppError{Code: VerifyErrCode}) + + requireMetrics(t, verifier, metricExpectations{ + messageParseFail: 0, + addressedCallVerifyFail: 0, + blockVerifyFail: 1, + uptimeVerifyFail: 0, + }) +} + +func TestVerifierMalformedPayload(t *testing.T) { + db := memdb.New() + messageDB := NewDB(db) + v := NewVerifier(messageDB, emptyBlockStore, nil, prometheus.NewRegistry()) + + invalidPayload := []byte{0xFF, 0xFF, 0xFF, 0xFF} + msg, err := warp.NewUnsignedMessage(networkID, sourceChainID, invalidPayload) + require.NoError(t, err) + + appErr := v.Verify(context.Background(), msg) + require.NotNil(t, appErr) + require.Equal(t, int32(ParseErrCode), appErr.Code) + + require.Equal(t, float64(1), testutil.ToFloat64(v.messageParseFail)) + require.Equal(t, float64(0), testutil.ToFloat64(v.blockVerifyFail)) +} + +func TestVerifierMalformedUptimePayload(t *testing.T) { + db := memdb.New() + messageDB := NewDB(db) + v := NewVerifier(messageDB, emptyBlockStore, nil, prometheus.NewRegistry()) + + invalidUptimeBytes := []byte{0xFF, 0xFF, 0xFF} + addressedCall, err := payload.NewAddressedCall([]byte{}, invalidUptimeBytes) + require.NoError(t, err) + + msg, err := warp.NewUnsignedMessage(networkID, sourceChainID, addressedCall.Bytes()) + require.NoError(t, err) + + appErr := v.Verify(context.Background(), msg) + require.NotNil(t, appErr) + require.Equal(t, int32(ParseErrCode), appErr.Code) + + require.Equal(t, float64(1), testutil.ToFloat64(v.messageParseFail)) } func TestHandlerMessageSignature(t *testing.T) { metricstest.WithMetrics(t) - database := memdb.New() + ctx := context.Background() snowCtx := snowtest.Context(t, snowtest.CChainID) tests := []struct { - name string - setup func(db *DB) (request []byte, wantResponse []byte) - verifyStats func(t *testing.T, v *Verifier) - err *common.AppError + name string + setupMessage func(db *DB) *warp.UnsignedMessage + wantError bool + wantSignature bool + wantMetrics metricExpectations }{ { name: "known message", - setup: func(db *DB) (request []byte, wantResponse []byte) { + setupMessage: func(db *DB) *warp.UnsignedMessage { knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) require.NoError(t, err) - signature, err := snowCtx.WarpSigner.Sign(msg) - require.NoError(t, err) require.NoError(t, db.Add(msg)) - return msg.Bytes(), signature + return msg }, - verifyStats: func(t *testing.T, v *Verifier) { - require.Zero(t, v.messageParseFail.Snapshot().Count()) - require.Zero(t, v.blockVerifyFail.Snapshot().Count()) + wantSignature: true, + wantMetrics: metricExpectations{ + messageParseFail: 0, + blockVerifyFail: 0, }, }, { name: "unknown message", - setup: func(_ *DB) (request []byte, wantResponse []byte) { + setupMessage: func(_ *DB) *warp.UnsignedMessage { unknownPayload, err := payload.NewAddressedCall([]byte{}, []byte("unknown message")) require.NoError(t, err) - unknownMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) + msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) require.NoError(t, err) - return unknownMessage.Bytes(), nil + return msg }, - verifyStats: func(t *testing.T, v *Verifier) { - require.Equal(t, int64(1), v.messageParseFail.Snapshot().Count()) - require.Zero(t, v.blockVerifyFail.Snapshot().Count()) + wantError: true, + wantMetrics: metricExpectations{ + messageParseFail: 1, + blockVerifyFail: 0, }, - err: &common.AppError{Code: ParseErrCode}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sigCache := lru.NewCache[ids.ID, []byte](100) - db := NewDB(database) - v := NewVerifier(db, emptyBlockStore, nil) - handler := NewHandler(sigCache, v, snowCtx.WarpSigner) - - requestBytes, wantResponse := tt.setup(db) - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, tt.err) - tt.verifyStats(t, v) + setup := setupHandler(t, ctx, memdb.New(), emptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) - if len(wantResponse) == 0 { - require.Empty(t, responseBytes) - return - } + message := tt.setupMessage(setup.db) + sendSignatureRequest(t, ctx, setup, message, tt.wantError) - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.Equal(t, wantResponse, response.Signature) + requireMetrics(t, setup.verifier, tt.wantMetrics) }) } } @@ -270,182 +313,193 @@ func TestHandlerMessageSignature(t *testing.T) { func TestHandlerBlockSignature(t *testing.T) { metricstest.WithMetrics(t) - database := memdb.New() + ctx := context.Background() snowCtx := snowtest.Context(t, snowtest.CChainID) knownBlkID := ids.GenerateTestID() blockStore := testBlockStore(set.Of(knownBlkID)) - toMessageBytes := func(id ids.ID) []byte { - idPayload, err := payload.NewHash(id) - if err != nil { - panic(err) - } - - msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, idPayload.Bytes()) - if err != nil { - panic(err) - } - - return msg.Bytes() - } - tests := []struct { name string - setup func() (request []byte, wantResponse []byte) - verifyStats func(t *testing.T, v *Verifier) - err *common.AppError + blockID ids.ID + wantError bool + wantMetrics metricExpectations }{ { - name: "known block", - setup: func() (request []byte, wantResponse []byte) { - hashPayload, err := payload.NewHash(knownBlkID) - require.NoError(t, err) - unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) - require.NoError(t, err) - signature, err := snowCtx.WarpSigner.Sign(unsignedMessage) - require.NoError(t, err) - return toMessageBytes(knownBlkID), signature - }, - verifyStats: func(t *testing.T, v *Verifier) { - require.Zero(t, v.blockVerifyFail.Snapshot().Count()) - require.Zero(t, v.messageParseFail.Snapshot().Count()) + name: "known block", + blockID: knownBlkID, + wantMetrics: metricExpectations{ + blockVerifyFail: 0, + messageParseFail: 0, }, }, { - name: "unknown block", - setup: func() (request []byte, wantResponse []byte) { - unknownBlockID := ids.GenerateTestID() - return toMessageBytes(unknownBlockID), nil - }, - verifyStats: func(t *testing.T, v *Verifier) { - require.Equal(t, int64(1), v.blockVerifyFail.Snapshot().Count()) - require.Zero(t, v.messageParseFail.Snapshot().Count()) + name: "unknown block", + blockID: ids.GenerateTestID(), + wantError: true, + wantMetrics: metricExpectations{ + blockVerifyFail: 1, + messageParseFail: 0, }, - err: &common.AppError{Code: VerifyErrCode}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sigCache := lru.NewCache[ids.ID, []byte](100) - db := NewDB(database) - v := NewVerifier(db, blockStore, nil) - handler := NewHandler(sigCache, v, snowCtx.WarpSigner) - - requestBytes, wantResponse := tt.setup() - protoMsg := &sdk.SignatureRequest{Message: requestBytes} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, tt.err) + setup := setupHandler(t, ctx, memdb.New(), blockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) - tt.verifyStats(t, v) + hashPayload, err := payload.NewHash(tt.blockID) + require.NoError(t, err) + message, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) + require.NoError(t, err) - if len(wantResponse) == 0 { - require.Empty(t, responseBytes) - return - } + sendSignatureRequest(t, ctx, setup, message, tt.wantError) - var response sdk.SignatureResponse - require.NoError(t, proto.Unmarshal(responseBytes, &response)) - require.Equal(t, wantResponse, response.Signature) + requireMetrics(t, setup.verifier, tt.wantMetrics) }) } } func TestHandlerUptimeSignature(t *testing.T) { - database := memdb.New() + metricstest.WithMetrics(t) + + ctx := context.Background() snowCtx := snowtest.Context(t, snowtest.CChainID) validationID := ids.GenerateTestID() nodeID := ids.GenerateTestNodeID() startTime := uint64(time.Now().Unix()) + requestedUptime := uint64(80) - getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID) ([]byte, *warp.UnsignedMessage) { - uptimePayload := &ValidatorUptime{ValidationID: vID, TotalUptime: 80} - uptimeBytes, err := uptimePayload.Bytes() - require.NoError(t, err) - addressedCall, err := payload.NewAddressedCall(sourceAddress, uptimeBytes) - require.NoError(t, err) - unsignedMessage, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) - require.NoError(t, err) - - protoMsg := &sdk.SignatureRequest{Message: unsignedMessage.Bytes()} - protoBytes, err := proto.Marshal(protoMsg) - require.NoError(t, err) - return protoBytes, unsignedMessage + tests := []struct { + name string + sourceAddress []byte + validationID ids.ID + setupUptimeTracker func(*uptimetracker.UptimeTracker, *mockable.Clock) + wantError bool + wantMetrics metricExpectations + }{ + { + name: "non-empty source address", + sourceAddress: []byte{1, 2, 3}, + validationID: ids.GenerateTestID(), + setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) {}, + wantError: true, + wantMetrics: metricExpectations{ + addressedCallVerifyFail: 1, + }, + }, + { + name: "unknown validation ID", + sourceAddress: []byte{}, + validationID: ids.GenerateTestID(), + setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) {}, + wantError: true, + wantMetrics: metricExpectations{ + uptimeVerifyFail: 1, + }, + }, + { + name: "validator not connected", + sourceAddress: []byte{}, + validationID: validationID, + setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) { + }, + wantError: true, + wantMetrics: metricExpectations{ + uptimeVerifyFail: 1, + }, + }, + { + name: "insufficient uptime", + sourceAddress: []byte{}, + validationID: validationID, + setupUptimeTracker: func(tracker *uptimetracker.UptimeTracker, clk *mockable.Clock) { + require.NoError(t, tracker.Connect(nodeID)) + clk.Set(clk.Time().Add(40 * time.Second)) + }, + wantError: true, + wantMetrics: metricExpectations{ + uptimeVerifyFail: 1, + }, + }, + { + name: "sufficient uptime", + sourceAddress: []byte{}, + validationID: validationID, + setupUptimeTracker: func(tracker *uptimetracker.UptimeTracker, clk *mockable.Clock) { + require.NoError(t, tracker.Connect(nodeID)) + clk.Set(clk.Time().Add(80 * time.Second)) + }, + wantMetrics: metricExpectations{}, + }, } - sigCache := lru.NewCache[ids.ID, []byte](100) - - // TODO(JonathanOppenheimer): see func NewTestValidatorState() -- this should be examined - // when we address the issue of that function. - validatorState := &validatorstest.State{ - GetCurrentValidatorSetF: func(context.Context, ids.ID) (map[ids.ID]*validators.GetCurrentValidatorOutput, uint64, error) { - return map[ids.ID]*validators.GetCurrentValidatorOutput{ - validationID: { - ValidationID: validationID, - NodeID: nodeID, - Weight: 1, - StartTime: startTime, - IsActive: true, - IsL1Validator: true, + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validatorState := &validatorstest.State{ + GetCurrentValidatorSetF: func(context.Context, ids.ID) (map[ids.ID]*validators.GetCurrentValidatorOutput, uint64, error) { + return map[ids.ID]*validators.GetCurrentValidatorOutput{ + validationID: { + ValidationID: validationID, + NodeID: nodeID, + Weight: 1, + StartTime: startTime, + IsActive: true, + IsL1Validator: true, + }, + }, 0, nil }, - }, 0, nil - }, + } + + clk := &mockable.Clock{} + uptimeTracker, err := uptimetracker.New(validatorState, snowCtx.SubnetID, memdb.New(), clk) + require.NoError(t, err) + require.NoError(t, uptimeTracker.Sync(ctx)) + + tt.setupUptimeTracker(uptimeTracker, clk) + + setup := setupHandler(t, ctx, memdb.New(), emptyBlockStore, uptimeTracker, snowCtx.NetworkID, snowCtx.ChainID) + + uptimePayload := &ValidatorUptime{ + ValidationID: tt.validationID, + TotalUptime: requestedUptime, + } + uptimeBytes, err := uptimePayload.Bytes() + require.NoError(t, err) + + addressedCall, err := payload.NewAddressedCall(tt.sourceAddress, uptimeBytes) + require.NoError(t, err) + + message, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) + require.NoError(t, err) + + sendSignatureRequest(t, ctx, setup, message, tt.wantError) + + requireMetrics(t, setup.verifier, tt.wantMetrics) + }) } +} - clk := &mockable.Clock{} - uptimeTracker, err := uptimetracker.New( - validatorState, - snowCtx.SubnetID, - memdb.New(), - clk, - ) - require.NoError(t, err) +func TestHandlerCacheBehavior(t *testing.T) { + metricstest.WithMetrics(t) - require.NoError(t, uptimeTracker.Sync(t.Context())) - - db := NewDB(database) - verifier := NewVerifier(db, emptyBlockStore, uptimeTracker) - handler := NewHandler(sigCache, verifier, snowCtx.WarpSigner) - - // sourceAddress nonZero - protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID()) - _, appErr := handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, "2: source address should be empty for offchain addressed messages", appErr.Error()) - - // not existing validationID - vID := ids.GenerateTestID() - protoBytes, _ = getUptimeMessageBytes([]byte{}, vID) - _, appErr = handler.AppRequest(t.Context(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: failed to get uptime: validationID not found: %s", vID), appErr.Error()) - - // uptime is less than requested (not connected) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: current uptime 0 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - - // uptime is less than requested (not enough time) - require.NoError(t, uptimeTracker.Connect(nodeID)) - clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID) - _, appErr = handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) - require.Equal(t, fmt.Sprintf("2: current uptime 40 is less than queried uptime 80 for validationID %s", validationID), appErr.Error()) - - // valid uptime (enough time has passed) - clk.Set(clk.Time().Add(40 * time.Second)) - protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID) - responseBytes, appErr := handler.AppRequest(t.Context(), nodeID, time.Time{}, protoBytes) - require.Nil(t, appErr) - wantSignature, err := snowCtx.WarpSigner.Sign(msg) + ctx := context.Background() + snowCtx := snowtest.Context(t, snowtest.CChainID) + setup := setupHandler(t, ctx, memdb.New(), emptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) + + knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) + require.NoError(t, err) + message, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) require.NoError(t, err) - response := &sdk.SignatureResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - require.Equal(t, wantSignature, response.Signature) + require.NoError(t, setup.db.Add(message)) + + firstSignature := sendSignatureRequest(t, ctx, setup, message, false) + + cachedSig, ok := setup.sigCache.Get(message.ID()) + require.True(t, ok) + require.Equal(t, firstSignature, cachedSig) + + secondSignature := sendSignatureRequest(t, ctx, setup, message, false) + require.Equal(t, firstSignature, secondSignature) } From 5213f1c4e37e094f19bd242116d49586c58ccb01 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Mon, 5 Jan 2026 14:47:04 -0500 Subject: [PATCH 46/53] chore: lint --- graft/coreth/plugin/evm/vm.go | 7 +++- graft/subnet-evm/plugin/evm/vm.go | 7 +++- vms/evm/warp/db_test.go | 7 ++-- vms/evm/warp/helpers_test.go | 17 ++++----- vms/evm/warp/verifier_test.go | 63 +++++++++++++++++++------------ 5 files changed, 63 insertions(+), 38 deletions(-) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index 0061926edf32..55ef23e58dac 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -114,6 +114,7 @@ const ( ethMetricsPrefix = "eth" sdkMetricsPrefix = "sdk" chainStateMetricsPrefix = "chain_state" + warpMetricsPrefix = "warp" ) // Define the API endpoints for the VM @@ -453,7 +454,11 @@ func (vm *VM) Initialize( } vm.warpMsgDB = warp.NewDB(vm.warpDB) - vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, nil) + warpMetrics := prometheus.NewRegistry() + vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, nil, warpMetrics) + if err := vm.ctx.Metrics.Register(warpMetricsPrefix, warpMetrics); err != nil { + return err + } // Create warp API. The signatureAggregator will be set later in CreateHandlers // when the p2p network client becomes available. diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index 3ac97d40fae3..a24f4e5de91f 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -116,6 +116,7 @@ const ( ethMetricsPrefix = "eth" sdkMetricsPrefix = "sdk" chainStateMetricsPrefix = "chain_state" + warpMetricsPrefix = "warp" ) // Define the API endpoints for the VM @@ -484,7 +485,11 @@ func (vm *VM) Initialize( } vm.warpMsgDB = warp.NewDB(vm.warpDB) - vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, vm.uptimeTracker) + warpMetrics := prometheus.NewRegistry() + vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, vm.uptimeTracker, warpMetrics) + if err := vm.ctx.Metrics.Register(warpMetricsPrefix, warpMetrics); err != nil { + return err + } // Create warp API. The signatureAggregator will be set later in CreateHandlers // when the p2p network client becomes available. diff --git a/vms/evm/warp/db_test.go b/vms/evm/warp/db_test.go index 0f14836bee40..91c90873ed00 100644 --- a/vms/evm/warp/db_test.go +++ b/vms/evm/warp/db_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" @@ -62,9 +63,9 @@ func TestDBGetCorruptedData(t *testing.T) { msgID := testUnsignedMessage.ID() require.NoError(t, db.Put(msgID[:], corruptedBytes)) - _, err := messageDB.Get(msgID) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to parse unsigned message") + msg, err := messageDB.Get(msgID) + require.Nil(t, msg) + require.ErrorIs(t, err, codec.ErrUnknownVersion) } func TestDBAddDuplicate(t *testing.T) { diff --git a/vms/evm/warp/helpers_test.go b/vms/evm/warp/helpers_test.go index 5b4ddbd6db3e..08b2b8a89914 100644 --- a/vms/evm/warp/helpers_test.go +++ b/vms/evm/warp/helpers_test.go @@ -18,6 +18,7 @@ import ( "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/p2ptest" "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" @@ -120,14 +121,13 @@ func setupHandler( } } -// sendSignatureRequest sends a signature request and returns the signature bytes +// sendSignatureRequest sends a signature request and returns the signature bytes and any error func sendSignatureRequest( t *testing.T, ctx context.Context, setup *handlerTestSetup, message *warp.UnsignedMessage, - wantError bool, -) []byte { +) ([]byte, *common.AppError) { t.Helper() request := &sdk.SignatureRequest{ @@ -137,17 +137,16 @@ func sendSignatureRequest( require.NoError(t, err) var signature []byte + var appErr *common.AppError responseChan := make(chan struct{}) - onResponse := func(_ context.Context, _ ids.NodeID, responseBytes []byte, appErr error) { + onResponse := func(_ context.Context, _ ids.NodeID, responseBytes []byte, err error) { defer close(responseChan) - if wantError { - require.Error(t, appErr) + if err != nil { + appErr, _ = err.(*common.AppError) return } - require.NoError(t, appErr) - var response sdk.SignatureResponse require.NoError(t, proto.Unmarshal(responseBytes, &response)) signature = response.Signature @@ -160,5 +159,5 @@ func sendSignatureRequest( require.NoError(t, setup.client.AppRequest(ctx, set.Of(setup.serverNodeID), requestBytes, onResponse)) <-responseChan - return signature + return signature, appErr } diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index 0344b7eacc85..03ef73384a77 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -225,8 +225,7 @@ func TestVerifierMalformedPayload(t *testing.T) { msg, err := warp.NewUnsignedMessage(networkID, sourceChainID, invalidPayload) require.NoError(t, err) - appErr := v.Verify(context.Background(), msg) - require.NotNil(t, appErr) + appErr := v.Verify(t.Context(), msg) require.Equal(t, int32(ParseErrCode), appErr.Code) require.Equal(t, float64(1), testutil.ToFloat64(v.messageParseFail)) @@ -245,8 +244,7 @@ func TestVerifierMalformedUptimePayload(t *testing.T) { msg, err := warp.NewUnsignedMessage(networkID, sourceChainID, addressedCall.Bytes()) require.NoError(t, err) - appErr := v.Verify(context.Background(), msg) - require.NotNil(t, appErr) + appErr := v.Verify(t.Context(), msg) require.Equal(t, int32(ParseErrCode), appErr.Code) require.Equal(t, float64(1), testutil.ToFloat64(v.messageParseFail)) @@ -255,13 +253,13 @@ func TestVerifierMalformedUptimePayload(t *testing.T) { func TestHandlerMessageSignature(t *testing.T) { metricstest.WithMetrics(t) - ctx := context.Background() + ctx := t.Context() snowCtx := snowtest.Context(t, snowtest.CChainID) tests := []struct { name string setupMessage func(db *DB) *warp.UnsignedMessage - wantError bool + wantErrCode *int32 // nil if no error expected wantSignature bool wantMetrics metricExpectations }{ @@ -290,7 +288,7 @@ func TestHandlerMessageSignature(t *testing.T) { require.NoError(t, err) return msg }, - wantError: true, + wantErrCode: func() *int32 { i := int32(ParseErrCode); return &i }(), wantMetrics: metricExpectations{ messageParseFail: 1, blockVerifyFail: 0, @@ -303,7 +301,12 @@ func TestHandlerMessageSignature(t *testing.T) { setup := setupHandler(t, ctx, memdb.New(), emptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) message := tt.setupMessage(setup.db) - sendSignatureRequest(t, ctx, setup, message, tt.wantError) + _, appErr := sendSignatureRequest(t, ctx, setup, message) + if tt.wantErrCode != nil { + require.Equal(t, *tt.wantErrCode, appErr.Code) + } else { + require.Nil(t, appErr) + } requireMetrics(t, setup.verifier, tt.wantMetrics) }) @@ -313,7 +316,7 @@ func TestHandlerMessageSignature(t *testing.T) { func TestHandlerBlockSignature(t *testing.T) { metricstest.WithMetrics(t) - ctx := context.Background() + ctx := t.Context() snowCtx := snowtest.Context(t, snowtest.CChainID) knownBlkID := ids.GenerateTestID() @@ -322,7 +325,7 @@ func TestHandlerBlockSignature(t *testing.T) { tests := []struct { name string blockID ids.ID - wantError bool + wantErrCode *int32 // nil if no error expected wantMetrics metricExpectations }{ { @@ -334,9 +337,9 @@ func TestHandlerBlockSignature(t *testing.T) { }, }, { - name: "unknown block", - blockID: ids.GenerateTestID(), - wantError: true, + name: "unknown block", + blockID: ids.GenerateTestID(), + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), wantMetrics: metricExpectations{ blockVerifyFail: 1, messageParseFail: 0, @@ -353,7 +356,12 @@ func TestHandlerBlockSignature(t *testing.T) { message, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) require.NoError(t, err) - sendSignatureRequest(t, ctx, setup, message, tt.wantError) + _, appErr := sendSignatureRequest(t, ctx, setup, message) + if tt.wantErrCode != nil { + require.Equal(t, *tt.wantErrCode, appErr.Code) + } else { + require.Nil(t, appErr) + } requireMetrics(t, setup.verifier, tt.wantMetrics) }) @@ -363,7 +371,7 @@ func TestHandlerBlockSignature(t *testing.T) { func TestHandlerUptimeSignature(t *testing.T) { metricstest.WithMetrics(t) - ctx := context.Background() + ctx := t.Context() snowCtx := snowtest.Context(t, snowtest.CChainID) validationID := ids.GenerateTestID() @@ -376,7 +384,7 @@ func TestHandlerUptimeSignature(t *testing.T) { sourceAddress []byte validationID ids.ID setupUptimeTracker func(*uptimetracker.UptimeTracker, *mockable.Clock) - wantError bool + wantErrCode *int32 // nil if no error expected wantMetrics metricExpectations }{ { @@ -384,7 +392,7 @@ func TestHandlerUptimeSignature(t *testing.T) { sourceAddress: []byte{1, 2, 3}, validationID: ids.GenerateTestID(), setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) {}, - wantError: true, + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), wantMetrics: metricExpectations{ addressedCallVerifyFail: 1, }, @@ -394,7 +402,7 @@ func TestHandlerUptimeSignature(t *testing.T) { sourceAddress: []byte{}, validationID: ids.GenerateTestID(), setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) {}, - wantError: true, + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), wantMetrics: metricExpectations{ uptimeVerifyFail: 1, }, @@ -405,7 +413,7 @@ func TestHandlerUptimeSignature(t *testing.T) { validationID: validationID, setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) { }, - wantError: true, + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), wantMetrics: metricExpectations{ uptimeVerifyFail: 1, }, @@ -418,7 +426,7 @@ func TestHandlerUptimeSignature(t *testing.T) { require.NoError(t, tracker.Connect(nodeID)) clk.Set(clk.Time().Add(40 * time.Second)) }, - wantError: true, + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), wantMetrics: metricExpectations{ uptimeVerifyFail: 1, }, @@ -474,7 +482,12 @@ func TestHandlerUptimeSignature(t *testing.T) { message, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedCall.Bytes()) require.NoError(t, err) - sendSignatureRequest(t, ctx, setup, message, tt.wantError) + _, appErr := sendSignatureRequest(t, ctx, setup, message) + if tt.wantErrCode != nil { + require.Equal(t, *tt.wantErrCode, appErr.Code) + } else { + require.Nil(t, appErr) + } requireMetrics(t, setup.verifier, tt.wantMetrics) }) @@ -484,7 +497,7 @@ func TestHandlerUptimeSignature(t *testing.T) { func TestHandlerCacheBehavior(t *testing.T) { metricstest.WithMetrics(t) - ctx := context.Background() + ctx := t.Context() snowCtx := snowtest.Context(t, snowtest.CChainID) setup := setupHandler(t, ctx, memdb.New(), emptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) @@ -494,12 +507,14 @@ func TestHandlerCacheBehavior(t *testing.T) { require.NoError(t, err) require.NoError(t, setup.db.Add(message)) - firstSignature := sendSignatureRequest(t, ctx, setup, message, false) + firstSignature, appErr := sendSignatureRequest(t, ctx, setup, message) + require.Nil(t, appErr) cachedSig, ok := setup.sigCache.Get(message.ID()) require.True(t, ok) require.Equal(t, firstSignature, cachedSig) - secondSignature := sendSignatureRequest(t, ctx, setup, message, false) + secondSignature, appErr := sendSignatureRequest(t, ctx, setup, message) + require.Nil(t, appErr) require.Equal(t, firstSignature, secondSignature) } From 42ab82eb606fe8502c81c0d72df3de54613b15d9 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Wed, 14 Jan 2026 17:03:01 -0500 Subject: [PATCH 47/53] fix: warp service initialization --- graft/coreth/plugin/evm/vm.go | 53 ++++++++++----------- graft/coreth/plugin/evm/vm_warp_test.go | 18 +++---- graft/subnet-evm/plugin/evm/vm.go | 37 +++++++------- graft/subnet-evm/plugin/evm/vm_warp_test.go | 18 +++---- vms/evm/warp/service.go | 6 --- 5 files changed, 60 insertions(+), 72 deletions(-) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index 55ef23e58dac..04e763e0c1c3 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -250,11 +250,10 @@ type VM struct { // Avalanche Warp Messaging components // Used to serve BLS signatures of warp messages over RPC - warpMsgDB *warp.DB - warpVerifier *warp.Verifier - warpSignatureCache cache.Cacher[ids.ID, []byte] - offchainWarpMessages [][]byte - warpAPI *warp.Service + warpMsgDB *warp.DB + warpVerifier *warp.Verifier + warpSignatureCache cache.Cacher[ids.ID, []byte] + warpService *warp.Service ethTxPushGossiper avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*GossipEthTx]] @@ -434,11 +433,6 @@ func (vm *VM) Initialize( return fmt.Errorf("failed to create network: %w", err) } - // Initialize warp backend - vm.offchainWarpMessages = make([][]byte, len(vm.config.WarpOffChainMessages)) - for i, hexMsg := range vm.config.WarpOffChainMessages { - vm.offchainWarpMessages[i] = []byte(hexMsg) - } warpSignatureCache := lru.NewCache[ids.ID, []byte](warpSignatureCacheSize) meteredCache, err := metercacher.New("warp_signature_cache", vm.sdkMetrics, warpSignatureCache) if err != nil { @@ -460,22 +454,6 @@ func (vm *VM) Initialize( return err } - // Create warp API. The signatureAggregator will be set later in CreateHandlers - // when the p2p network client becomes available. - vm.warpAPI, err = warp.NewService( - vm.ctx.NetworkID, - vm.ctx.ChainID, - vm.ctx.ValidatorState, - vm.warpMsgDB, - vm.ctx.WarpSigner, - vm.warpVerifier, - vm.warpSignatureCache, - nil, // signatureAggregator is set in CreateHandlers via SetSignatureAggregator - vm.offchainWarpMessages, - ) - if err != nil { - return err - } if err := vm.initializeChain(lastAcceptedHash); err != nil { return err } @@ -1100,9 +1078,28 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.Network.NewClient(p2p.SignatureRequestHandlerID) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - vm.warpAPI.SetSignatureAggregator(signatureAggregator) + offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) + for i, hexMsg := range vm.config.WarpOffChainMessages { + offchainWarpMessages[i] = []byte(hexMsg) + } + + var err error + vm.warpService, err = warp.NewService( + vm.ctx.NetworkID, + vm.ctx.ChainID, + vm.ctx.ValidatorState, + vm.warpMsgDB, + vm.ctx.WarpSigner, + vm.warpVerifier, + vm.warpSignatureCache, + signatureAggregator, + offchainWarpMessages, + ) + if err != nil { + return nil, err + } - if err := handler.RegisterName("warp", vm.warpAPI); err != nil { + if err := handler.RegisterName("warp", vm.warpService); err != nil { return nil, err } enabledAPIs = append(enabledAPIs, "warp") diff --git a/graft/coreth/plugin/evm/vm_warp_test.go b/graft/coreth/plugin/evm/vm_warp_test.go index b18a2bf762bf..d208517a860e 100644 --- a/graft/coreth/plugin/evm/vm_warp_test.go +++ b/graft/coreth/plugin/evm/vm_warp_test.go @@ -140,9 +140,9 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.NoError(err) // Verify the signature cannot be fetched before the block is accepted - _, err = vm.warpAPI.GetMessageSignature(t.Context(), unsignedMessage.ID()) + _, err = vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) require.ErrorIs(err, warp.ErrMessageNotFound) - _, err = vm.warpAPI.GetBlockSignature(t.Context(), blk.ID()) + _, err = vm.warpService.GetBlockSignature(t.Context(), blk.ID()) require.ErrorIs(err, warp.ErrBlockNotFound) require.NoError(vm.SetPreference(t.Context(), blk.ID())) @@ -150,7 +150,7 @@ func testSendWarpMessage(t *testing.T, scheme string) { vm.blockChain.DrainAcceptorQueue() // Verify the message signature after accepting the block. - rawSignatureBytes, err := vm.warpAPI.GetMessageSignature(t.Context(), unsignedMessage.ID()) + rawSignatureBytes, err := vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) require.NoError(err) blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes) require.NoError(err) @@ -167,7 +167,7 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) // Verify the blockID will now be signed by the backend and produces a valid signature. - rawSignatureBytes, err = vm.warpAPI.GetBlockSignature(t.Context(), blk.ID()) + rawSignatureBytes, err = vm.warpService.GetBlockSignature(t.Context(), blk.ID()) require.NoError(err) blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes) require.NoError(err) @@ -827,13 +827,13 @@ func testSignatureRequestsToVM(t *testing.T, scheme string) { // Add the known message and get its signature to confirm require.NoError(t, vm.warpMsgDB.Add(knownWarpMessage)) - knownMessageSignature, err := vm.warpAPI.GetMessageSignature(t.Context(), knownWarpMessage.ID()) + knownMessageSignature, err := vm.warpService.GetMessageSignature(t.Context(), knownWarpMessage.ID()) require.NoError(t, err) // Setup known block lastAcceptedID, err := vm.LastAccepted(t.Context()) require.NoError(t, err) - knownBlockSignature, err := vm.warpAPI.GetBlockSignature(t.Context(), lastAcceptedID) + knownBlockSignature, err := vm.warpService.GetBlockSignature(t.Context(), lastAcceptedID) require.NoError(t, err) type testCase struct { @@ -938,7 +938,7 @@ func TestClearWarpDB(t *testing.T) { require.NoError(t, err) require.NoError(t, vm.warpMsgDB.Add(unsignedMsg)) // ensure that the message was added - _, err = vm.warpAPI.GetMessageSignature(t.Context(), unsignedMsg.ID()) + _, err = vm.warpService.GetMessageSignature(t.Context(), unsignedMsg.ID()) require.NoError(t, err) messages = append(messages, unsignedMsg) } @@ -953,7 +953,7 @@ func TestClearWarpDB(t *testing.T) { // check messages are still present for _, message := range messages { - bytes, err := vm.warpAPI.GetMessageSignature(t.Context(), message.ID()) + bytes, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) require.NoError(t, err) require.NotEmpty(t, bytes) } @@ -972,7 +972,7 @@ func TestClearWarpDB(t *testing.T) { // ensure all messages have been deleted for _, message := range messages { - _, err := vm.warpAPI.GetMessageSignature(t.Context(), message.ID()) + _, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) require.ErrorIs(t, err, warp.ErrMessageNotFound) } } diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index a24f4e5de91f..e68fe0069967 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -261,7 +261,7 @@ type VM struct { warpVerifier *warp.Verifier warpSignatureCache cache.Cacher[ids.ID, []byte] offchainWarpMessages [][]byte - warpAPI *warp.Service + warpService *warp.Service // Initialize only sets these if nil so they can be overridden in tests ethTxGossipHandler p2p.Handler @@ -491,22 +491,6 @@ func (vm *VM) Initialize( return err } - // Create warp API. The signatureAggregator will be set later in CreateHandlers - // when the p2p network client becomes available. - vm.warpAPI, err = warp.NewService( - vm.ctx.NetworkID, - vm.ctx.ChainID, - vm.ctx.ValidatorState, - vm.warpMsgDB, - vm.ctx.WarpSigner, - vm.warpVerifier, - vm.warpSignatureCache, - nil, // signatureAggregator is set in CreateHandlers via SetSignatureAggregator - vm.offchainWarpMessages, - ) - if err != nil { - return err - } if err := vm.initializeChain(lastAcceptedHash, vm.ethConfig); err != nil { return err } @@ -1226,10 +1210,23 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.Network.NewClient(p2p.SignatureRequestHandlerID) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - // Set the signature aggregator on the existing warpAPI instance - vm.warpAPI.SetSignatureAggregator(signatureAggregator) + var err error + vm.warpService, err = warp.NewService( + vm.ctx.NetworkID, + vm.ctx.ChainID, + vm.ctx.ValidatorState, + vm.warpMsgDB, + vm.ctx.WarpSigner, + vm.warpVerifier, + vm.warpSignatureCache, + signatureAggregator, + vm.offchainWarpMessages, + ) + if err != nil { + return nil, err + } - if err := handler.RegisterName("warp", vm.warpAPI); err != nil { + if err := handler.RegisterName("warp", vm.warpService); err != nil { return nil, err } enabledAPIs = append(enabledAPIs, "warp") diff --git a/graft/subnet-evm/plugin/evm/vm_warp_test.go b/graft/subnet-evm/plugin/evm/vm_warp_test.go index 1c1a353fa402..3f03b8c79050 100644 --- a/graft/subnet-evm/plugin/evm/vm_warp_test.go +++ b/graft/subnet-evm/plugin/evm/vm_warp_test.go @@ -157,9 +157,9 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.NoError(err) // Verify the signature cannot be fetched before the block is accepted - _, err = tvm.vm.warpAPI.GetMessageSignature(t.Context(), unsignedMessage.ID()) + _, err = tvm.vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) require.ErrorIs(err, warp.ErrMessageNotFound) - _, err = tvm.vm.warpAPI.GetBlockSignature(t.Context(), blk.ID()) + _, err = tvm.vm.warpService.GetBlockSignature(t.Context(), blk.ID()) require.ErrorIs(err, warp.ErrBlockNotFound) require.NoError(tvm.vm.SetPreference(t.Context(), blk.ID())) @@ -167,7 +167,7 @@ func testSendWarpMessage(t *testing.T, scheme string) { tvm.vm.blockChain.DrainAcceptorQueue() // Verify the message signature after accepting the block. - rawSignatureBytes, err := tvm.vm.warpAPI.GetMessageSignature(t.Context(), unsignedMessage.ID()) + rawSignatureBytes, err := tvm.vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) require.NoError(err) blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes) require.NoError(err) @@ -184,7 +184,7 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.True(bls.Verify(tvm.vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) // Verify the blockID will now be signed by the backend and produces a valid signature. - rawSignatureBytes, err = tvm.vm.warpAPI.GetBlockSignature(t.Context(), blk.ID()) + rawSignatureBytes, err = tvm.vm.warpService.GetBlockSignature(t.Context(), blk.ID()) require.NoError(err) blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes) require.NoError(err) @@ -848,13 +848,13 @@ func testSignatureRequestsToVM(t *testing.T, scheme string) { // Add the known message and get its signature to confirm require.NoError(t, tvm.vm.warpMsgDB.Add(knownWarpMessage)) - knownMessageSignature, err := tvm.vm.warpAPI.GetMessageSignature(t.Context(), knownWarpMessage.ID()) + knownMessageSignature, err := tvm.vm.warpService.GetMessageSignature(t.Context(), knownWarpMessage.ID()) require.NoError(t, err) // Setup known block lastAcceptedID, err := tvm.vm.LastAccepted(t.Context()) require.NoError(t, err) - knownBlockSignature, err := tvm.vm.warpAPI.GetBlockSignature(t.Context(), lastAcceptedID) + knownBlockSignature, err := tvm.vm.warpService.GetBlockSignature(t.Context(), lastAcceptedID) require.NoError(t, err) type testCase struct { @@ -959,7 +959,7 @@ func TestClearWarpDB(t *testing.T) { require.NoError(t, err) require.NoError(t, vm.warpMsgDB.Add(unsignedMsg)) // ensure that the message was added - _, err = vm.warpAPI.GetMessageSignature(t.Context(), unsignedMsg.ID()) + _, err = vm.warpService.GetMessageSignature(t.Context(), unsignedMsg.ID()) require.NoError(t, err) messages = append(messages, unsignedMsg) } @@ -974,7 +974,7 @@ func TestClearWarpDB(t *testing.T) { // check messages are still present for _, message := range messages { - bytes, err := vm.warpAPI.GetMessageSignature(t.Context(), message.ID()) + bytes, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) require.NoError(t, err) require.NotEmpty(t, bytes) } @@ -993,7 +993,7 @@ func TestClearWarpDB(t *testing.T) { // ensure all messages have been deleted for _, message := range messages { - _, err := vm.warpAPI.GetMessageSignature(t.Context(), message.ID()) + _, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) require.ErrorIs(t, err, warp.ErrMessageNotFound) } } diff --git a/vms/evm/warp/service.go b/vms/evm/warp/service.go index 776ff6567417..f47102fd0b69 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/service.go @@ -149,12 +149,6 @@ func (s *Service) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexuti return s.signMessage(ctx, unsignedMessage) } -// SetSignatureAggregator sets the signature aggregator for the service. -// This must be called before any aggregate signature methods are used. -func (s *Service) SetSignatureAggregator(signatureAggregator *acp118.SignatureAggregator) { - s.signatureAggregator = signatureAggregator -} - // GetMessageAggregateSignature fetches the aggregate signature for the requested messageID. func (s *Service) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { unsignedMessage, err := s.getMessage(messageID) From dafd9576981ebeb8cc5ed0600a1c52fbce80a78c Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 15 Jan 2026 12:24:34 -0500 Subject: [PATCH 48/53] refactor: Josh suggestions --- graft/coreth/plugin/evm/vm.go | 12 +- graft/coreth/plugin/evm/vm_warp_test.go | 7 +- .../precompile/precompileconfig/config.go | 8 +- graft/coreth/tests/warp/warp_test.go | 10 +- graft/subnet-evm/plugin/evm/vm.go | 25 ++--- graft/subnet-evm/plugin/evm/vm_warp_test.go | 7 +- .../precompile/precompileconfig/config.go | 8 +- graft/subnet-evm/tests/warp/warp_test.go | 10 +- vms/evm/warp/db_test.go | 103 ++++++++++-------- vms/evm/warp/helpers_test.go | 3 - vms/evm/warp/{ => rpc}/client.go | 2 +- vms/evm/warp/{ => rpc}/service.go | 14 ++- vms/evm/warp/verifier.go | 17 +-- vms/evm/warp/verifier_test.go | 24 ++-- 14 files changed, 127 insertions(+), 123 deletions(-) rename vms/evm/warp/{ => rpc}/client.go (99%) rename vms/evm/warp/{ => rpc}/service.go (97%) diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index ae891f436715..400b3432baa8 100644 --- a/graft/coreth/plugin/evm/vm.go +++ b/graft/coreth/plugin/evm/vm.go @@ -90,6 +90,7 @@ import ( commonEng "github.com/ava-labs/avalanchego/snow/engine/common" avalancheUtils "github.com/ava-labs/avalanchego/utils" avalanchegoprometheus "github.com/ava-labs/avalanchego/vms/evm/metrics/prometheus" + warpRPC "github.com/ava-labs/avalanchego/vms/evm/warp/rpc" ethparams "github.com/ava-labs/libevm/params" ) @@ -251,7 +252,7 @@ type VM struct { warpMsgDB *warp.DB warpVerifier *warp.Verifier warpSignatureCache cache.Cacher[ids.ID, []byte] - warpService *warp.Service + warpService *warpRPC.Service ethTxPushGossiper avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*GossipEthTx]] @@ -954,7 +955,6 @@ func (vm *VM) getBlock(_ context.Context, id ids.ID) (snowman.Block, error) { } // HasBlock returns nil if the block is accepted, or an error otherwise. -// Implements warp.BlockStore. func (vm *VM) HasBlock(ctx context.Context, blkID ids.ID) error { blk, err := vm.GetBlock(ctx, blkID) if err != nil { @@ -1038,13 +1038,13 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.P2PNetwork().NewClient(p2p.SignatureRequestHandlerID, vm.P2PValidators()) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) - offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) + offChainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) for i, hexMsg := range vm.config.WarpOffChainMessages { - offchainWarpMessages[i] = []byte(hexMsg) + offChainWarpMessages[i] = []byte(hexMsg) } var err error - vm.warpService, err = warp.NewService( + vm.warpService, err = warpRPC.NewService( vm.ctx.NetworkID, vm.ctx.ChainID, vm.ctx.ValidatorState, @@ -1053,7 +1053,7 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { vm.warpVerifier, vm.warpSignatureCache, signatureAggregator, - offchainWarpMessages, + offChainWarpMessages, ) if err != nil { return nil, err diff --git a/graft/coreth/plugin/evm/vm_warp_test.go b/graft/coreth/plugin/evm/vm_warp_test.go index dfef9a37ed9e..be6563f7445d 100644 --- a/graft/coreth/plugin/evm/vm_warp_test.go +++ b/graft/coreth/plugin/evm/vm_warp_test.go @@ -50,6 +50,7 @@ import ( warpcontract "github.com/ava-labs/avalanchego/graft/coreth/precompile/contracts/warp" commonEng "github.com/ava-labs/avalanchego/snow/engine/common" avagoUtils "github.com/ava-labs/avalanchego/utils" + warpRPC "github.com/ava-labs/avalanchego/vms/evm/warp/rpc" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) @@ -141,9 +142,9 @@ func testSendWarpMessage(t *testing.T, scheme string) { // Verify the signature cannot be fetched before the block is accepted _, err = vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) - require.ErrorIs(err, warp.ErrMessageNotFound) + require.ErrorIs(err, warpRPC.ErrMessageNotFound) _, err = vm.warpService.GetBlockSignature(t.Context(), blk.ID()) - require.ErrorIs(err, warp.ErrBlockNotFound) + require.ErrorIs(err, warpRPC.ErrBlockNotFound) require.NoError(vm.SetPreference(t.Context(), blk.ID())) require.NoError(blk.Accept(t.Context())) @@ -973,6 +974,6 @@ func TestClearWarpDB(t *testing.T) { // ensure all messages have been deleted for _, message := range messages { _, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) - require.ErrorIs(t, err, warp.ErrMessageNotFound) + require.ErrorIs(t, err, warpRPC.ErrMessageNotFound) } } diff --git a/graft/coreth/precompile/precompileconfig/config.go b/graft/coreth/precompile/precompileconfig/config.go index f86064aff8ee..f5b14ae2c063 100644 --- a/graft/coreth/precompile/precompileconfig/config.go +++ b/graft/coreth/precompile/precompileconfig/config.go @@ -10,7 +10,7 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/vms/evm/predicate" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/evm/warp" ) // StatefulPrecompileConfig defines the interface for a stateful precompile to @@ -53,14 +53,10 @@ type Predicater interface { VerifyPredicate(predicateContext *PredicateContext, pred predicate.Predicate) error } -type WarpMessageWriter interface { - Add(unsignedMessage *warp.UnsignedMessage) error -} - // AcceptContext defines the context passed in to a precompileconfig's Accepter type AcceptContext struct { SnowCtx *snow.Context - Warp WarpMessageWriter + Warp *warp.DB } // Accepter is an optional interface for StatefulPrecompiledContracts to implement. diff --git a/graft/coreth/tests/warp/warp_test.go b/graft/coreth/tests/warp/warp_test.go index 4643e85ea943..7848e9bdb0e2 100644 --- a/graft/coreth/tests/warp/warp_test.go +++ b/graft/coreth/tests/warp/warp_test.go @@ -39,7 +39,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/platformvm/api" - warpBackend "github.com/ava-labs/avalanchego/vms/evm/warp" + warpRPC "github.com/ava-labs/avalanchego/vms/evm/warp/rpc" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ethereum "github.com/ava-labs/libevm" ginkgo "github.com/onsi/ginkgo/v2" @@ -351,9 +351,9 @@ func (w *warpTest) aggregateSignaturesViaAPI() { tc := e2e.NewTestContext() ctx := tc.DefaultContext() - warpAPIs := make(map[ids.NodeID]*warpBackend.Client, len(w.sendingSubnetURIs)) + warpAPIs := make(map[ids.NodeID]*warpRPC.Client, len(w.sendingSubnetURIs)) for _, uri := range w.sendingSubnetURIs { - client, err := warpBackend.NewClient(uri, w.sendingSubnet.BlockchainID.String()) + client, err := warpRPC.NewClient(uri, w.sendingSubnet.BlockchainID.String()) require.NoError(err) infoClient := info.NewClient(uri) @@ -382,7 +382,7 @@ func (w *warpTest) aggregateSignaturesViaAPI() { require.NotEmpty(warpValidators) // Verify that the signature aggregation matches the results of manually constructing the warp message - client, err := warpBackend.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) + client, err := warpRPC.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) require.NoError(err) ginkgo.GinkgoLogr.Info("Fetching addressed call aggregate signature via p2p API") @@ -591,7 +591,7 @@ func (w *warpTest) warpLoad() { require.NoError(warpSendLoader.Execute(ctx)) require.NoError(warpSendLoader.ConfirmReachedTip(ctx)) - warpClient, err := warpBackend.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) + warpClient, err := warpRPC.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) require.NoError(err) subnetID := ids.Empty if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID { diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index edf2ae1b1df9..1c33fc696aab 100644 --- a/graft/subnet-evm/plugin/evm/vm.go +++ b/graft/subnet-evm/plugin/evm/vm.go @@ -88,6 +88,7 @@ import ( avalancheUtils "github.com/ava-labs/avalanchego/utils" avajson "github.com/ava-labs/avalanchego/utils/json" avalanchegoprometheus "github.com/ava-labs/avalanchego/vms/evm/metrics/prometheus" + warpRPC "github.com/ava-labs/avalanchego/vms/evm/warp/rpc" ethparams "github.com/ava-labs/libevm/params" avalancheRPC "github.com/gorilla/rpc/v2" ) @@ -255,11 +256,10 @@ type VM struct { // Avalanche Warp Messaging components // Used to serve BLS signatures of warp messages over RPC - warpMsgDB *warp.DB - warpVerifier *warp.Verifier - warpSignatureCache cache.Cacher[ids.ID, []byte] - offchainWarpMessages [][]byte - warpService *warp.Service + warpMsgDB *warp.DB + warpVerifier *warp.Verifier + warpSignatureCache cache.Cacher[ids.ID, []byte] + warpService *warpRPC.Service // Initialize only sets these if nil so they can be overridden in tests ethTxPushGossiper avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*GossipEthTx]] @@ -462,11 +462,6 @@ func (vm *VM) Initialize( return fmt.Errorf("failed to initialize uptime tracker: %w", err) } - // Initialize warp backend - vm.offchainWarpMessages = make([][]byte, len(vm.config.WarpOffChainMessages)) - for i, hexMsg := range vm.config.WarpOffChainMessages { - vm.offchainWarpMessages[i] = []byte(hexMsg) - } warpSignatureCache := lru.NewCache[ids.ID, []byte](warpSignatureCacheSize) meteredCache, err := metercacher.New("warp_signature_cache", vm.sdkMetrics, warpSignatureCache) if err != nil { @@ -1060,7 +1055,6 @@ func (vm *VM) getBlock(_ context.Context, id ids.ID) (snowman.Block, error) { } // HasBlock returns nil if the block is accepted, or an error otherwise. -// Implements warp.BlockStore. func (vm *VM) HasBlock(ctx context.Context, blkID ids.ID) error { blk, err := vm.GetBlock(ctx, blkID) if err != nil { @@ -1168,8 +1162,13 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { warpSDKClient := vm.P2PNetwork().NewClient(p2p.SignatureRequestHandlerID, vm.P2PValidators()) signatureAggregator := acp118.NewSignatureAggregator(vm.ctx.Log, warpSDKClient) + offChainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) + for i, hexMsg := range vm.config.WarpOffChainMessages { + offChainWarpMessages[i] = []byte(hexMsg) + } + var err error - vm.warpService, err = warp.NewService( + vm.warpService, err = warpRPC.NewService( vm.ctx.NetworkID, vm.ctx.ChainID, vm.ctx.ValidatorState, @@ -1178,7 +1177,7 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { vm.warpVerifier, vm.warpSignatureCache, signatureAggregator, - vm.offchainWarpMessages, + offChainWarpMessages, ) if err != nil { return nil, err diff --git a/graft/subnet-evm/plugin/evm/vm_warp_test.go b/graft/subnet-evm/plugin/evm/vm_warp_test.go index 88317e1149d1..3ce771b626b3 100644 --- a/graft/subnet-evm/plugin/evm/vm_warp_test.go +++ b/graft/subnet-evm/plugin/evm/vm_warp_test.go @@ -51,6 +51,7 @@ import ( warpcontract "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" commonEng "github.com/ava-labs/avalanchego/snow/engine/common" avagoUtils "github.com/ava-labs/avalanchego/utils" + warpRPC "github.com/ava-labs/avalanchego/vms/evm/warp/rpc" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) @@ -158,9 +159,9 @@ func testSendWarpMessage(t *testing.T, scheme string) { // Verify the signature cannot be fetched before the block is accepted _, err = tvm.vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) - require.ErrorIs(err, warp.ErrMessageNotFound) + require.ErrorIs(err, warpRPC.ErrMessageNotFound) _, err = tvm.vm.warpService.GetBlockSignature(t.Context(), blk.ID()) - require.ErrorIs(err, warp.ErrBlockNotFound) + require.ErrorIs(err, warpRPC.ErrBlockNotFound) require.NoError(tvm.vm.SetPreference(t.Context(), blk.ID())) require.NoError(blk.Accept(t.Context())) @@ -994,6 +995,6 @@ func TestClearWarpDB(t *testing.T) { // ensure all messages have been deleted for _, message := range messages { _, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) - require.ErrorIs(t, err, warp.ErrMessageNotFound) + require.ErrorIs(t, err, warpRPC.ErrMessageNotFound) } } diff --git a/graft/subnet-evm/precompile/precompileconfig/config.go b/graft/subnet-evm/precompile/precompileconfig/config.go index 00a90ea89062..f4bca0fb2580 100644 --- a/graft/subnet-evm/precompile/precompileconfig/config.go +++ b/graft/subnet-evm/precompile/precompileconfig/config.go @@ -11,7 +11,7 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/vms/evm/predicate" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/evm/warp" ) // StatefulPrecompileConfig defines the interface for a stateful precompile to @@ -54,14 +54,10 @@ type Predicater interface { VerifyPredicate(predicateContext *PredicateContext, pred predicate.Predicate) error } -type WarpMessageWriter interface { - Add(unsignedMessage *warp.UnsignedMessage) error -} - // AcceptContext defines the context passed in to a precompileconfig's Accepter type AcceptContext struct { SnowCtx *snow.Context - Warp WarpMessageWriter + Warp *warp.DB } // Accepter is an optional interface for StatefulPrecompiledContracts to implement. diff --git a/graft/subnet-evm/tests/warp/warp_test.go b/graft/subnet-evm/tests/warp/warp_test.go index cea1d03b4a49..a7b606e052b0 100644 --- a/graft/subnet-evm/tests/warp/warp_test.go +++ b/graft/subnet-evm/tests/warp/warp_test.go @@ -37,12 +37,12 @@ import ( "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/vms/evm/predicate" - "github.com/ava-labs/avalanchego/vms/evm/warp" "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/platformvm/api" warpContract "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" warptestbindings "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp/warptest/bindings" + warpRPC "github.com/ava-labs/avalanchego/vms/evm/warp/rpc" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ethereum "github.com/ava-labs/libevm" @@ -436,9 +436,9 @@ func (w *warpTest) aggregateSignaturesViaAPI() { tc := e2e.NewTestContext() ctx := tc.DefaultContext() - warpAPIs := make(map[ids.NodeID]*warp.Client, len(w.sendingSubnetURIs)) + warpAPIs := make(map[ids.NodeID]*warpRPC.Client, len(w.sendingSubnetURIs)) for _, uri := range w.sendingSubnetURIs { - client, err := warp.NewClient(uri, w.sendingSubnet.BlockchainID.String()) + client, err := warpRPC.NewClient(uri, w.sendingSubnet.BlockchainID.String()) require.NoError(err) infoClient := info.NewClient(uri) @@ -467,7 +467,7 @@ func (w *warpTest) aggregateSignaturesViaAPI() { require.NotEmpty(warpValidators) // Verify that the signature aggregation matches the results of manually constructing the warp message - client, err := warp.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) + client, err := warpRPC.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) require.NoError(err) log.Info("Fetching addressed call aggregate signature via p2p API") @@ -736,7 +736,7 @@ func (w *warpTest) warpLoad() { require.NoError(warpSendLoader.Execute(ctx)) require.NoError(warpSendLoader.ConfirmReachedTip(ctx)) - warpClient, err := warp.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) + warpClient, err := warpRPC.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String()) require.NoError(err) subnetID := ids.Empty if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID { diff --git a/vms/evm/warp/db_test.go b/vms/evm/warp/db_test.go index 91c90873ed00..a6d4e7a66c5a 100644 --- a/vms/evm/warp/db_test.go +++ b/vms/evm/warp/db_test.go @@ -11,6 +11,7 @@ import ( "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) @@ -35,47 +36,63 @@ func TestDBAddAndSign(t *testing.T) { } func TestDBGet(t *testing.T) { - db := memdb.New() - messageDB := NewDB(db) - - require.NoError(t, messageDB.Add(testUnsignedMessage)) - - gotMsg, err := messageDB.Get(testUnsignedMessage.ID()) - require.NoError(t, err) - require.Equal(t, testUnsignedMessage.Bytes(), gotMsg.Bytes()) - require.Equal(t, testUnsignedMessage.ID(), gotMsg.ID()) -} - -func TestDBGetNotFound(t *testing.T) { - db := memdb.New() - messageDB := NewDB(db) - - unknownID := testUnsignedMessage.ID() - _, err := messageDB.Get(unknownID) - require.ErrorIs(t, err, database.ErrNotFound) -} - -func TestDBGetCorruptedData(t *testing.T) { - db := memdb.New() - messageDB := NewDB(db) - - corruptedBytes := []byte{0xFF, 0xFF, 0xFF} - msgID := testUnsignedMessage.ID() - require.NoError(t, db.Put(msgID[:], corruptedBytes)) - - msg, err := messageDB.Get(msgID) - require.Nil(t, msg) - require.ErrorIs(t, err, codec.ErrUnknownVersion) -} - -func TestDBAddDuplicate(t *testing.T) { - db := memdb.New() - messageDB := NewDB(db) - - require.NoError(t, messageDB.Add(testUnsignedMessage)) - require.NoError(t, messageDB.Add(testUnsignedMessage)) - - gotMsg, err := messageDB.Get(testUnsignedMessage.ID()) - require.NoError(t, err) - require.Equal(t, testUnsignedMessage.Bytes(), gotMsg.Bytes()) + tests := []struct { + name string + setup func(testing.TB, database.Database, *DB) + queryID func() ids.ID + wantMsg *warp.UnsignedMessage + wantErr error + }{ + { + name: "existing message", + setup: func(t testing.TB, _ database.Database, db *DB) { + require.NoError(t, db.Add(testUnsignedMessage)) + }, + queryID: testUnsignedMessage.ID, + wantMsg: testUnsignedMessage, + }, + { + name: "not found", + setup: func(testing.TB, database.Database, *DB) {}, + queryID: testUnsignedMessage.ID, + wantErr: database.ErrNotFound, + }, + { + name: "corrupted data", + setup: func(t testing.TB, rawDB database.Database, _ *DB) { + msgID := testUnsignedMessage.ID() + require.NoError(t, rawDB.Put(msgID[:], []byte{0xFF, 0xFF, 0xFF})) + }, + queryID: testUnsignedMessage.ID, + wantErr: codec.ErrUnknownVersion, + }, + { + name: "duplicate add", + setup: func(t testing.TB, _ database.Database, db *DB) { + require.NoError(t, db.Add(testUnsignedMessage)) + require.NoError(t, db.Add(testUnsignedMessage)) + }, + queryID: testUnsignedMessage.ID, + wantMsg: testUnsignedMessage, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rawDB := memdb.New() + messageDB := NewDB(rawDB) + tt.setup(t, rawDB, messageDB) + + got, err := messageDB.Get(tt.queryID()) + + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + require.Nil(t, got) + return + } + require.NoError(t, err) + require.Equal(t, tt.wantMsg.Bytes(), got.Bytes()) + require.Equal(t, tt.wantMsg.ID(), got.ID()) + }) + } } diff --git a/vms/evm/warp/helpers_test.go b/vms/evm/warp/helpers_test.go index 08b2b8a89914..79a6335347eb 100644 --- a/vms/evm/warp/helpers_test.go +++ b/vms/evm/warp/helpers_test.go @@ -34,8 +34,6 @@ var ( testSourceAddress = utils.RandomBytes(20) testPayload = []byte("test") testUnsignedMessage *warp.UnsignedMessage - - emptyBlockStore BlockStore = testBlockStore{} ) func init() { @@ -49,7 +47,6 @@ func init() { } } -// testBlockStore implements BlockStore for testing type testBlockStore set.Set[ids.ID] func (t testBlockStore) HasBlock(_ context.Context, blockID ids.ID) error { diff --git a/vms/evm/warp/client.go b/vms/evm/warp/rpc/client.go similarity index 99% rename from vms/evm/warp/client.go rename to vms/evm/warp/rpc/client.go index df84de8ce1d7..6a18b44df0d9 100644 --- a/vms/evm/warp/client.go +++ b/vms/evm/warp/rpc/client.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package warp +package rpc import ( "context" diff --git a/vms/evm/warp/service.go b/vms/evm/warp/rpc/service.go similarity index 97% rename from vms/evm/warp/service.go rename to vms/evm/warp/rpc/service.go index f47102fd0b69..10ca04680248 100644 --- a/vms/evm/warp/service.go +++ b/vms/evm/warp/rpc/service.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package warp +package rpc import ( "context" @@ -19,6 +19,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + + evmwarp "github.com/ava-labs/avalanchego/vms/evm/warp" ) var ( @@ -34,9 +36,9 @@ type Service struct { chainID ids.ID validatorState validators.State - db *DB + db *evmwarp.DB signer warp.Signer - verifier *Verifier + verifier *evmwarp.Verifier signatureAggregator *acp118.SignatureAggregator messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] @@ -48,9 +50,9 @@ func NewService( networkID uint32, chainID ids.ID, validatorState validators.State, - db *DB, + db *evmwarp.DB, signer warp.Signer, - verifier *Verifier, + verifier *evmwarp.Verifier, signatureCache cache.Cacher[ids.ID, []byte], signatureAggregator *acp118.SignatureAggregator, offChainMessages [][]byte, @@ -223,7 +225,7 @@ func (s *Service) signMessage(ctx context.Context, unsignedMessage *warp.Unsigne } if err := s.verifier.Verify(ctx, unsignedMessage); err != nil { - if err.Code == VerifyErrCode { + if err.Code == evmwarp.VerifyErrCode { return nil, fmt.Errorf("%w: %w", ErrBlockNotFound, err) } return nil, fmt.Errorf("failed to verify message %s: %w", msgID, err) diff --git a/vms/evm/warp/verifier.go b/vms/evm/warp/verifier.go index bf8d0a56e7b6..eacaa78a70fd 100644 --- a/vms/evm/warp/verifier.go +++ b/vms/evm/warp/verifier.go @@ -53,8 +53,8 @@ func init() { // ValidatorUptime is signed when the ValidationID is known and the validator // has been up for TotalUptime seconds. type ValidatorUptime struct { - ValidationID ids.ID `serialize:"true"` - TotalUptime uint64 `serialize:"true"` // in seconds + ValidationID ids.ID `serialize:"true"` + TotalUptimeSeconds uint64 `serialize:"true"` // in seconds } // ParseValidatorUptime converts a slice of bytes into a ValidatorUptime. @@ -76,14 +76,9 @@ type BlockStore interface { HasBlock(ctx context.Context, blockID ids.ID) error } -// MessageDB provides access to warp messages. -type MessageDB interface { - Get(ids.ID) (*warp.UnsignedMessage, error) -} - // Verifier validates whether a warp message should be signed. type Verifier struct { - db MessageDB + db *DB blockClient BlockStore uptimeTracker *uptimetracker.UptimeTracker @@ -95,7 +90,7 @@ type Verifier struct { // NewVerifier creates a new warp message verifier. func NewVerifier( - db MessageDB, + db *DB, blockClient BlockStore, uptimeTracker *uptimetracker.UptimeTracker, reg prometheus.Registerer, @@ -221,10 +216,10 @@ func (v *Verifier) verifyUptimeMessage(uptimeMsg *ValidatorUptime) *common.AppEr currentUptimeSeconds := uint64(currentUptime.Seconds()) // verify the current uptime against the total uptime in the message - if currentUptimeSeconds < uptimeMsg.TotalUptime { + if currentUptimeSeconds < uptimeMsg.TotalUptimeSeconds { return &common.AppError{ Code: VerifyErrCode, - Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for validationID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, uptimeMsg.ValidationID), + Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for validationID %s", currentUptimeSeconds, uptimeMsg.TotalUptimeSeconds, uptimeMsg.ValidationID), } } diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index 03ef73384a77..474b081d1890 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -37,8 +37,8 @@ func TestCodecSerialization(t *testing.T) { { name: "zero values", msg: &ValidatorUptime{ - ValidationID: ids.Empty, - TotalUptime: 0, + ValidationID: ids.Empty, + TotalUptimeSeconds: 0, }, wantBytes: []byte{ // Codec version (0) @@ -61,7 +61,7 @@ func TestCodecSerialization(t *testing.T) { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, }, - TotalUptime: 12345, + TotalUptimeSeconds: 12345, }, wantBytes: []byte{ // Codec version (0) @@ -84,7 +84,7 @@ func TestCodecSerialization(t *testing.T) { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }, - TotalUptime: ^uint64(0), + TotalUptimeSeconds: ^uint64(0), }, wantBytes: []byte{ // Codec version (0) @@ -111,7 +111,7 @@ func TestCodecSerialization(t *testing.T) { gotMsg, err := ParseValidatorUptime(tt.wantBytes) require.NoError(err) require.Equal(tt.msg.ValidationID, gotMsg.ValidationID) - require.Equal(tt.msg.TotalUptime, gotMsg.TotalUptime) + require.Equal(tt.msg.TotalUptimeSeconds, gotMsg.TotalUptimeSeconds) }) } } @@ -219,7 +219,7 @@ func TestVerifierBlockMessage(t *testing.T) { func TestVerifierMalformedPayload(t *testing.T) { db := memdb.New() messageDB := NewDB(db) - v := NewVerifier(messageDB, emptyBlockStore, nil, prometheus.NewRegistry()) + v := NewVerifier(messageDB, nil, nil, prometheus.NewRegistry()) invalidPayload := []byte{0xFF, 0xFF, 0xFF, 0xFF} msg, err := warp.NewUnsignedMessage(networkID, sourceChainID, invalidPayload) @@ -235,7 +235,7 @@ func TestVerifierMalformedPayload(t *testing.T) { func TestVerifierMalformedUptimePayload(t *testing.T) { db := memdb.New() messageDB := NewDB(db) - v := NewVerifier(messageDB, emptyBlockStore, nil, prometheus.NewRegistry()) + v := NewVerifier(messageDB, nil, nil, prometheus.NewRegistry()) invalidUptimeBytes := []byte{0xFF, 0xFF, 0xFF} addressedCall, err := payload.NewAddressedCall([]byte{}, invalidUptimeBytes) @@ -298,7 +298,7 @@ func TestHandlerMessageSignature(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - setup := setupHandler(t, ctx, memdb.New(), emptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) + setup := setupHandler(t, ctx, memdb.New(), nil, nil, snowCtx.NetworkID, snowCtx.ChainID) message := tt.setupMessage(setup.db) _, appErr := sendSignatureRequest(t, ctx, setup, message) @@ -467,11 +467,11 @@ func TestHandlerUptimeSignature(t *testing.T) { tt.setupUptimeTracker(uptimeTracker, clk) - setup := setupHandler(t, ctx, memdb.New(), emptyBlockStore, uptimeTracker, snowCtx.NetworkID, snowCtx.ChainID) + setup := setupHandler(t, ctx, memdb.New(), nil, uptimeTracker, snowCtx.NetworkID, snowCtx.ChainID) uptimePayload := &ValidatorUptime{ - ValidationID: tt.validationID, - TotalUptime: requestedUptime, + ValidationID: tt.validationID, + TotalUptimeSeconds: requestedUptime, } uptimeBytes, err := uptimePayload.Bytes() require.NoError(t, err) @@ -499,7 +499,7 @@ func TestHandlerCacheBehavior(t *testing.T) { ctx := t.Context() snowCtx := snowtest.Context(t, snowtest.CChainID) - setup := setupHandler(t, ctx, memdb.New(), emptyBlockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) + setup := setupHandler(t, ctx, memdb.New(), nil, nil, snowCtx.NetworkID, snowCtx.ChainID) knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) From 1296300f838a3fd01bfcb084d8d50f3ac9bb332d Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 15 Jan 2026 14:00:30 -0500 Subject: [PATCH 49/53] refactor: more Josh feedback --- vms/evm/warp/helpers_test.go | 14 +++++++++----- vms/evm/warp/rpc/service.go | 12 ++++++------ vms/evm/warp/verifier_test.go | 6 +++--- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/vms/evm/warp/helpers_test.go b/vms/evm/warp/helpers_test.go index 79a6335347eb..20fa7b37f58c 100644 --- a/vms/evm/warp/helpers_test.go +++ b/vms/evm/warp/helpers_test.go @@ -19,7 +19,6 @@ import ( "github.com/ava-labs/avalanchego/network/p2p/p2ptest" "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" "github.com/ava-labs/avalanchego/utils/set" @@ -29,10 +28,15 @@ import ( ) var ( - networkID uint32 = 54321 - sourceChainID = ids.GenerateTestID() - testSourceAddress = utils.RandomBytes(20) - testPayload = []byte("test") + networkID uint32 = 54321 + sourceChainID = ids.GenerateTestID() + testSourceAddress = []byte{ + 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14} + testPayload = []byte("test") testUnsignedMessage *warp.UnsignedMessage ) diff --git a/vms/evm/warp/rpc/service.go b/vms/evm/warp/rpc/service.go index 10ca04680248..42a0cf97d3ab 100644 --- a/vms/evm/warp/rpc/service.go +++ b/vms/evm/warp/rpc/service.go @@ -29,7 +29,7 @@ var ( ErrBlockNotFound = errors.New("block not found") ) -// Service introduces snowman specific functionality to the evm. +// Service provides an RPC interface for warp messaging. // It provides caching and orchestration over the core warp primitives. type Service struct { networkID uint32 @@ -94,8 +94,8 @@ func NewService( } // GetMessage returns the Warp message associated with the given messageID. -func (s *Service) GetMessage(messageID ids.ID) (hexutil.Bytes, error) { - message, err := s.getMessage(messageID) +func (s *Service) GetMessage(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { + message, err := s.getMessage(ctx, messageID) if err != nil { return nil, fmt.Errorf("failed to get message %s: %w", messageID, err) } @@ -103,7 +103,7 @@ func (s *Service) GetMessage(messageID ids.ID) (hexutil.Bytes, error) { } // getMessage retrieves a message from cache, offchain messages, or database. -func (s *Service) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { +func (s *Service) getMessage(_ context.Context, messageID ids.ID) (*warp.UnsignedMessage, error) { if msg, ok := s.messageCache.Get(messageID); ok { return msg, nil } @@ -123,7 +123,7 @@ func (s *Service) getMessage(messageID ids.ID) (*warp.UnsignedMessage, error) { // GetMessageSignature returns the BLS signature associated with the given messageID. func (s *Service) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - unsignedMessage, err := s.getMessage(messageID) + unsignedMessage, err := s.getMessage(ctx, messageID) if err != nil { return nil, fmt.Errorf("%w %s: %w", ErrMessageNotFound, messageID, err) } @@ -153,7 +153,7 @@ func (s *Service) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexuti // GetMessageAggregateSignature fetches the aggregate signature for the requested messageID. func (s *Service) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetID ids.ID) (signedMessageBytes hexutil.Bytes, err error) { - unsignedMessage, err := s.getMessage(messageID) + unsignedMessage, err := s.getMessage(ctx, messageID) if err != nil { return nil, err } diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index 474b081d1890..9ec42e2982a0 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -259,7 +259,7 @@ func TestHandlerMessageSignature(t *testing.T) { tests := []struct { name string setupMessage func(db *DB) *warp.UnsignedMessage - wantErrCode *int32 // nil if no error expected + wantErrCode error wantSignature bool wantMetrics metricExpectations }{ @@ -288,7 +288,7 @@ func TestHandlerMessageSignature(t *testing.T) { require.NoError(t, err) return msg }, - wantErrCode: func() *int32 { i := int32(ParseErrCode); return &i }(), + wantErrCode: &common.AppError{Code: ParseErrCode}, wantMetrics: metricExpectations{ messageParseFail: 1, blockVerifyFail: 0, @@ -303,7 +303,7 @@ func TestHandlerMessageSignature(t *testing.T) { message := tt.setupMessage(setup.db) _, appErr := sendSignatureRequest(t, ctx, setup, message) if tt.wantErrCode != nil { - require.Equal(t, *tt.wantErrCode, appErr.Code) + require.ErrorIs(t, appErr, tt.wantErrCode) } else { require.Nil(t, appErr) } From 6fa4ddcacc7fcb6b55122fc1773f4c6d1e0e7d09 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 15 Jan 2026 14:09:58 -0500 Subject: [PATCH 50/53] chore: lint --- vms/evm/warp/helpers_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vms/evm/warp/helpers_test.go b/vms/evm/warp/helpers_test.go index 20fa7b37f58c..3974c7753a37 100644 --- a/vms/evm/warp/helpers_test.go +++ b/vms/evm/warp/helpers_test.go @@ -35,7 +35,8 @@ var ( 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - 0x11, 0x12, 0x13, 0x14} + 0x11, 0x12, 0x13, 0x14, + } testPayload = []byte("test") testUnsignedMessage *warp.UnsignedMessage ) From bef2f7ec4d80ecb5fcca1acafd6a15bbc4d4f9b0 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 15 Jan 2026 14:16:07 -0500 Subject: [PATCH 51/53] chore: fix license --- vms/evm/warp/db.go | 2 +- vms/evm/warp/db_test.go | 2 +- vms/evm/warp/helpers_test.go | 2 +- vms/evm/warp/rpc/client.go | 2 +- vms/evm/warp/rpc/service.go | 2 +- vms/evm/warp/verifier.go | 2 +- vms/evm/warp/verifier_test.go | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vms/evm/warp/db.go b/vms/evm/warp/db.go index 17e755d1e27e..9f4c9a2999a7 100644 --- a/vms/evm/warp/db.go +++ b/vms/evm/warp/db.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package warp diff --git a/vms/evm/warp/db_test.go b/vms/evm/warp/db_test.go index a6d4e7a66c5a..6b6d7c929afd 100644 --- a/vms/evm/warp/db_test.go +++ b/vms/evm/warp/db_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package warp diff --git a/vms/evm/warp/helpers_test.go b/vms/evm/warp/helpers_test.go index 3974c7753a37..31773ed9b6e6 100644 --- a/vms/evm/warp/helpers_test.go +++ b/vms/evm/warp/helpers_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package warp diff --git a/vms/evm/warp/rpc/client.go b/vms/evm/warp/rpc/client.go index 6a18b44df0d9..4df20faabf64 100644 --- a/vms/evm/warp/rpc/client.go +++ b/vms/evm/warp/rpc/client.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package rpc diff --git a/vms/evm/warp/rpc/service.go b/vms/evm/warp/rpc/service.go index 42a0cf97d3ab..1192e4f31c62 100644 --- a/vms/evm/warp/rpc/service.go +++ b/vms/evm/warp/rpc/service.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package rpc diff --git a/vms/evm/warp/verifier.go b/vms/evm/warp/verifier.go index eacaa78a70fd..1a9b25724ea9 100644 --- a/vms/evm/warp/verifier.go +++ b/vms/evm/warp/verifier.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package warp diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go index 9ec42e2982a0..2334b4775a61 100644 --- a/vms/evm/warp/verifier_test.go +++ b/vms/evm/warp/verifier_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2026, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package warp From 2ba5fce985dacbd67be65b1460012d7900adaab0 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Thu, 15 Jan 2026 14:48:28 -0500 Subject: [PATCH 52/53] test: do not introspect on warp service --- graft/coreth/plugin/evm/vm_warp_test.go | 57 +++++++++++---------- graft/subnet-evm/plugin/evm/vm_warp_test.go | 54 ++++++++++--------- 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/graft/coreth/plugin/evm/vm_warp_test.go b/graft/coreth/plugin/evm/vm_warp_test.go index be6563f7445d..6d33502fb7ea 100644 --- a/graft/coreth/plugin/evm/vm_warp_test.go +++ b/graft/coreth/plugin/evm/vm_warp_test.go @@ -20,6 +20,7 @@ import ( _ "embed" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/graft/coreth/eth/tracers" "github.com/ava-labs/avalanchego/graft/coreth/params" "github.com/ava-labs/avalanchego/graft/coreth/params/extras" @@ -50,7 +51,6 @@ import ( warpcontract "github.com/ava-labs/avalanchego/graft/coreth/precompile/contracts/warp" commonEng "github.com/ava-labs/avalanchego/snow/engine/common" avagoUtils "github.com/ava-labs/avalanchego/utils" - warpRPC "github.com/ava-labs/avalanchego/vms/evm/warp/rpc" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) @@ -140,21 +140,18 @@ func testSendWarpMessage(t *testing.T, scheme string) { unsignedMessage, err := warpcontract.UnpackSendWarpEventDataToMessage(logData) require.NoError(err) - // Verify the signature cannot be fetched before the block is accepted - _, err = vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) - require.ErrorIs(err, warpRPC.ErrMessageNotFound) - _, err = vm.warpService.GetBlockSignature(t.Context(), blk.ID()) - require.ErrorIs(err, warpRPC.ErrBlockNotFound) + // Verify the message is not in the DB before the block is accepted + _, err = vm.warpMsgDB.Get(unsignedMessage.ID()) + require.ErrorIs(err, database.ErrNotFound) require.NoError(vm.SetPreference(t.Context(), blk.ID())) require.NoError(blk.Accept(t.Context())) vm.blockChain.DrainAcceptorQueue() - // Verify the message signature after accepting the block. - rawSignatureBytes, err := vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) - require.NoError(err) - blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes) + // Verify the message is now in the DB after accepting the block + storedMessage, err := vm.warpMsgDB.Get(unsignedMessage.ID()) require.NoError(err) + require.Equal(unsignedMessage.ID(), storedMessage.ID()) select { case acceptedLogs := <-acceptedLogsChan: @@ -164,22 +161,24 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.Fail("Failed to read accepted logs from subscription") } - // Verify the produced message signature is valid - require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) - - // Verify the blockID will now be signed by the backend and produces a valid signature. - rawSignatureBytes, err = vm.warpService.GetBlockSignature(t.Context(), blk.ID()) + // Verify we can sign the message and the signature is valid + signatureBytes, err := vm.ctx.WarpSigner.Sign(unsignedMessage) require.NoError(err) - blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes) + blsSignature, err := bls.SignatureFromBytes(signatureBytes) require.NoError(err) + require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) + // Verify we can sign a block hash payload blockHashPayload, err := payload.NewHash(blk.ID()) require.NoError(err) - unsignedMessage, err = avalancheWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, blockHashPayload.Bytes()) + blockHashMessage, err := avalancheWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, blockHashPayload.Bytes()) require.NoError(err) - // Verify the produced message signature is valid - require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) + signatureBytes, err = vm.ctx.WarpSigner.Sign(blockHashMessage) + require.NoError(err) + blsSignature, err = bls.SignatureFromBytes(signatureBytes) + require.NoError(err) + require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, blockHashMessage.Bytes())) } func TestValidateWarpMessage(t *testing.T) { @@ -813,13 +812,18 @@ func testSignatureRequestsToVM(t *testing.T, scheme string) { fork := upgradetest.Durango vm := newDefaultTestVM() tvm := vmtest.SetupTestVM(t, vm, vmtest.TestVMConfig{ - Fork: &fork, - Scheme: scheme, + Fork: &fork, + Scheme: scheme, + ConfigJSON: `{"warp-api-enabled": true}`, }) defer func() { require.NoError(t, vm.Shutdown(t.Context())) }() + // Initialize the warp service by creating handlers + _, err := vm.CreateHandlers(t.Context()) + require.NoError(t, err) + // Setup known message knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) @@ -939,8 +943,9 @@ func TestClearWarpDB(t *testing.T) { require.NoError(t, err) require.NoError(t, vm.warpMsgDB.Add(unsignedMsg)) // ensure that the message was added - _, err = vm.warpService.GetMessageSignature(t.Context(), unsignedMsg.ID()) + storedMsg, err := vm.warpMsgDB.Get(unsignedMsg.ID()) require.NoError(t, err) + require.Equal(t, unsignedMsg.ID(), storedMsg.ID()) messages = append(messages, unsignedMsg) } @@ -954,9 +959,9 @@ func TestClearWarpDB(t *testing.T) { // check messages are still present for _, message := range messages { - bytes, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) + storedMsg, err := vm.warpMsgDB.Get(message.ID()) require.NoError(t, err) - require.NotEmpty(t, bytes) + require.Equal(t, message.ID(), storedMsg.ID()) } require.NoError(t, vm.Shutdown(t.Context())) @@ -973,7 +978,7 @@ func TestClearWarpDB(t *testing.T) { // ensure all messages have been deleted for _, message := range messages { - _, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) - require.ErrorIs(t, err, warpRPC.ErrMessageNotFound) + _, err := vm.warpMsgDB.Get(message.ID()) + require.ErrorIs(t, err, database.ErrNotFound) } } diff --git a/graft/subnet-evm/plugin/evm/vm_warp_test.go b/graft/subnet-evm/plugin/evm/vm_warp_test.go index 3ce771b626b3..e2af0f710f81 100644 --- a/graft/subnet-evm/plugin/evm/vm_warp_test.go +++ b/graft/subnet-evm/plugin/evm/vm_warp_test.go @@ -20,6 +20,7 @@ import ( _ "embed" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/graft/evm/utils" "github.com/ava-labs/avalanchego/graft/subnet-evm/core" "github.com/ava-labs/avalanchego/graft/subnet-evm/eth/tracers" @@ -51,7 +52,6 @@ import ( warpcontract "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/contracts/warp" commonEng "github.com/ava-labs/avalanchego/snow/engine/common" avagoUtils "github.com/ava-labs/avalanchego/utils" - warpRPC "github.com/ava-labs/avalanchego/vms/evm/warp/rpc" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) @@ -157,21 +157,18 @@ func testSendWarpMessage(t *testing.T, scheme string) { unsignedMessage, err := warpcontract.UnpackSendWarpEventDataToMessage(logData) require.NoError(err) - // Verify the signature cannot be fetched before the block is accepted - _, err = tvm.vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) - require.ErrorIs(err, warpRPC.ErrMessageNotFound) - _, err = tvm.vm.warpService.GetBlockSignature(t.Context(), blk.ID()) - require.ErrorIs(err, warpRPC.ErrBlockNotFound) + // Verify the message is not in the DB before the block is accepted + _, err = tvm.vm.warpMsgDB.Get(unsignedMessage.ID()) + require.ErrorIs(err, database.ErrNotFound) require.NoError(tvm.vm.SetPreference(t.Context(), blk.ID())) require.NoError(blk.Accept(t.Context())) tvm.vm.blockChain.DrainAcceptorQueue() - // Verify the message signature after accepting the block. - rawSignatureBytes, err := tvm.vm.warpService.GetMessageSignature(t.Context(), unsignedMessage.ID()) - require.NoError(err) - blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes) + // Verify the message is now in the DB after accepting the block + storedMessage, err := tvm.vm.warpMsgDB.Get(unsignedMessage.ID()) require.NoError(err) + require.Equal(unsignedMessage.ID(), storedMessage.ID()) select { case acceptedLogs := <-acceptedLogsChan: @@ -181,22 +178,24 @@ func testSendWarpMessage(t *testing.T, scheme string) { require.Fail("Failed to read accepted logs from subscription") } - // Verify the produced message signature is valid - require.True(bls.Verify(tvm.vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) - - // Verify the blockID will now be signed by the backend and produces a valid signature. - rawSignatureBytes, err = tvm.vm.warpService.GetBlockSignature(t.Context(), blk.ID()) + // Verify we can sign the message and the signature is valid + signatureBytes, err := tvm.vm.ctx.WarpSigner.Sign(unsignedMessage) require.NoError(err) - blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes) + blsSignature, err := bls.SignatureFromBytes(signatureBytes) require.NoError(err) + require.True(bls.Verify(tvm.vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) + // Verify we can sign a block hash payload blockHashPayload, err := payload.NewHash(blk.ID()) require.NoError(err) - unsignedMessage, err = avalancheWarp.NewUnsignedMessage(tvm.vm.ctx.NetworkID, tvm.vm.ctx.ChainID, blockHashPayload.Bytes()) + blockHashMessage, err := avalancheWarp.NewUnsignedMessage(tvm.vm.ctx.NetworkID, tvm.vm.ctx.ChainID, blockHashPayload.Bytes()) require.NoError(err) - // Verify the produced message signature is valid - require.True(bls.Verify(tvm.vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) + signatureBytes, err = tvm.vm.ctx.WarpSigner.Sign(blockHashMessage) + require.NoError(err) + blsSignature, err = bls.SignatureFromBytes(signatureBytes) + require.NoError(err) + require.True(bls.Verify(tvm.vm.ctx.PublicKey, blsSignature, blockHashMessage.Bytes())) } func TestValidateWarpMessage(t *testing.T) { @@ -834,13 +833,17 @@ func testSignatureRequestsToVM(t *testing.T, scheme string) { fork := upgradetest.Durango tvm := newVM(t, testVMConfig{ fork: &fork, - configJSON: getConfig(scheme, ""), + configJSON: getConfig(scheme, `"warp-api-enabled": true`), }) defer func() { require.NoError(t, tvm.vm.Shutdown(t.Context())) }() + // Initialize the warp service by creating handlers + _, err := tvm.vm.CreateHandlers(t.Context()) + require.NoError(t, err) + // Setup known message knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) require.NoError(t, err) @@ -960,8 +963,9 @@ func TestClearWarpDB(t *testing.T) { require.NoError(t, err) require.NoError(t, vm.warpMsgDB.Add(unsignedMsg)) // ensure that the message was added - _, err = vm.warpService.GetMessageSignature(t.Context(), unsignedMsg.ID()) + storedMsg, err := vm.warpMsgDB.Get(unsignedMsg.ID()) require.NoError(t, err) + require.Equal(t, unsignedMsg.ID(), storedMsg.ID()) messages = append(messages, unsignedMsg) } @@ -975,9 +979,9 @@ func TestClearWarpDB(t *testing.T) { // check messages are still present for _, message := range messages { - bytes, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) + storedMsg, err := vm.warpMsgDB.Get(message.ID()) require.NoError(t, err) - require.NotEmpty(t, bytes) + require.Equal(t, message.ID(), storedMsg.ID()) } require.NoError(t, vm.Shutdown(t.Context())) @@ -994,7 +998,7 @@ func TestClearWarpDB(t *testing.T) { // ensure all messages have been deleted for _, message := range messages { - _, err := vm.warpService.GetMessageSignature(t.Context(), message.ID()) - require.ErrorIs(t, err, warpRPC.ErrMessageNotFound) + _, err := vm.warpMsgDB.Get(message.ID()) + require.ErrorIs(t, err, database.ErrNotFound) } } From 1530d658411c1392347c7e519f4834a4d9d18a8b Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Tue, 27 Jan 2026 12:53:05 -0500 Subject: [PATCH 53/53] fix: function call --- vms/evm/warp/rpc/service.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vms/evm/warp/rpc/service.go b/vms/evm/warp/rpc/service.go index 1192e4f31c62..917b8dbdc499 100644 --- a/vms/evm/warp/rpc/service.go +++ b/vms/evm/warp/rpc/service.go @@ -181,11 +181,12 @@ func (s *Service) aggregateSignatures(ctx context.Context, unsignedMessage *warp return nil, err } - validatorSet, err := validatorState.GetWarpValidatorSet(ctx, pChainHeight, subnetID) + validatorSets, err := validatorState.GetWarpValidatorSets(ctx, pChainHeight) if err != nil { - return nil, fmt.Errorf("failed to get validator set: %w", err) + return nil, fmt.Errorf("failed to get validator sets: %w", err) } - if len(validatorSet.Validators) == 0 { + validatorSet, ok := validatorSets[subnetID] + if !ok || len(validatorSet.Validators) == 0 { return nil, fmt.Errorf("%w (SubnetID: %s, Height: %d)", errNoValidators, subnetID, pChainHeight) }