diff --git a/graft/coreth/plugin/evm/vm.go b/graft/coreth/plugin/evm/vm.go index e738f6b42375..dbf9425dee38 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" @@ -61,7 +62,6 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/sync/client/stats" "github.com/ava-labs/avalanchego/graft/coreth/sync/engine" "github.com/ava-labs/avalanchego/graft/coreth/sync/handlers" - "github.com/ava-labs/avalanchego/graft/coreth/warp" "github.com/ava-labs/avalanchego/graft/evm/constants" "github.com/ava-labs/avalanchego/graft/evm/rpc" "github.com/ava-labs/avalanchego/graft/evm/triedb/hashdb" @@ -81,6 +81,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" @@ -90,6 +91,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" ) @@ -113,6 +115,7 @@ const ( ethMetricsPrefix = "eth" sdkMetricsPrefix = "sdk" chainStateMetricsPrefix = "chain_state" + warpMetricsPrefix = "warp" ) // Define the API endpoints for the VM @@ -245,9 +248,12 @@ type VM struct { engine.Server engine.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] + warpService *warpRPC.Service ethTxPushGossiper avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*GossipEthTx]] @@ -427,16 +433,12 @@ func (vm *VM) Initialize( return fmt.Errorf("failed to create network: %w", err) } - // Initialize warp backend - offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) - for i, hexMsg := range vm.config.WarpOffChainMessages { - 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 { @@ -445,18 +447,13 @@ func (vm *VM) Initialize( } } - vm.warpBackend, err = warp.NewBackend( - vm.ctx.NetworkID, - vm.ctx.ChainID, - vm.ctx.WarpSigner, - vm, - vm.warpDB, - meteredCache, - offchainWarpMessages, - ) - if err != nil { + vm.warpMsgDB = warp.NewDB(vm.warpDB) + warpMetrics := prometheus.NewRegistry() + vm.warpVerifier = warp.NewVerifier(vm.warpMsgDB, vm, nil, warpMetrics) + if err := vm.ctx.Metrics.Register(warpMetricsPrefix, warpMetrics); err != nil { return err } + if err := vm.initializeChain(lastAcceptedHash); err != nil { return err } @@ -464,7 +461,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.P2PNetwork().AddHandler(p2p.SignatureRequestHandlerID, warpHandler); err != nil { return err } @@ -958,25 +955,22 @@ 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. +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 @@ -1045,7 +1039,28 @@ 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) - if err := handler.RegisterName("warp", warp.NewAPI(vm.ctx, vm.warpBackend, signatureAggregator)); err != nil { + offChainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) + for i, hexMsg := range vm.config.WarpOffChainMessages { + offChainWarpMessages[i] = []byte(hexMsg) + } + + var err error + vm.warpService, err = warpRPC.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.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 7342ee311864..a513e6fe271a 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" @@ -27,7 +28,6 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap0" "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/warp" "github.com/ava-labs/avalanchego/graft/evm/utils" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" @@ -45,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/coreth/precompile/contracts/warp" @@ -139,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.warpBackend.GetMessageSignature(t.Context(), unsignedMessage) - require.ErrorIs(err, warp.ErrVerifyWarpMessage) - _, err = vm.warpBackend.GetBlockSignature(t.Context(), blk.ID()) - require.ErrorIs(err, warp.ErrValidateBlock) + // 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.warpBackend.GetMessageSignature(t.Context(), unsignedMessage) - 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: @@ -163,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.warpBackend.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) { @@ -817,13 +817,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) @@ -831,14 +836,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.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.warpBackend.GetBlockSignature(t.Context(), lastAcceptedID) + knownBlockSignature, err := vm.warpService.GetBlockSignature(t.Context(), lastAcceptedID) require.NoError(t, err) type testCase struct { @@ -941,10 +946,11 @@ 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) + storedMsg, err := vm.warpMsgDB.Get(unsignedMsg.ID()) require.NoError(t, err) + require.Equal(t, unsignedMsg.ID(), storedMsg.ID()) messages = append(messages, unsignedMsg) } @@ -958,9 +964,9 @@ func TestClearWarpDB(t *testing.T) { // check messages are still present for _, message := range messages { - bytes, err := vm.warpBackend.GetMessageSignature(t.Context(), message) + 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())) @@ -977,7 +983,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.warpMsgDB.Get(message.ID()) + require.ErrorIs(t, err, database.ErrNotFound) } } diff --git a/graft/coreth/plugin/evm/wrapped_block.go b/graft/coreth/plugin/evm/wrapped_block.go index 853e563f3ffc..95d9afadbfe9 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 36fa98c87218..3eabe2bdff1f 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 f9792d2a20b9..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 { - AddMessage(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 e5ddfe143cac..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/graft/coreth/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,16 +382,16 @@ 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") - 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) @@ -403,7 +403,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) @@ -591,11 +591,11 @@ 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) - 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...") @@ -609,7 +609,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/coreth/warp/backend.go b/graft/coreth/warp/backend.go deleted file mode 100644 index 35f309a124ea..000000000000 --- a/graft/coreth/warp/backend.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (C) 2019, 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 6c9d43346859..000000000000 --- a/graft/coreth/warp/backend_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (C) 2019, 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 6aec6245a872..000000000000 --- a/graft/coreth/warp/client.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2019, 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/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/coreth/warp/service.go b/graft/coreth/warp/service.go deleted file mode 100644 index 51d6d923bce8..000000000000 --- a/graft/coreth/warp/service.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (C) 2019, 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") - errCannotRetrieveValidatorSet = errors.New("cannot retrieve validator set") -) - -// 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 - } - - validatorSets, err := validatorState.GetWarpValidatorSets(ctx, pChainHeight) - if err != nil { - return nil, fmt.Errorf("failed to get validator sets: %w", err) - } - validatorSet, ok := validatorSets[subnetID] - if !ok { - return nil, fmt.Errorf("%w: %s source subnet not found", errCannotRetrieveValidatorSet, subnetID) - } - 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 e185c537710a..000000000000 --- a/graft/coreth/warp/verifier_backend.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2019, 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 d4361a307db9..000000000000 --- a/graft/coreth/warp/verifier_backend_test.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (C) 2019, 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 03735ec25026..000000000000 --- a/graft/coreth/warp/verifier_stats.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2019, 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 1abf66b3779f..000000000000 --- a/graft/coreth/warp/warptest/block_client.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2019, 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 deleted file mode 100644 index 1acb978a746f..000000000000 --- a/graft/subnet-evm/examples/sign-uptime-message/main.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (C) 2019, 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/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/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 := messages.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) - } -} diff --git a/graft/subnet-evm/plugin/evm/vm.go b/graft/subnet-evm/plugin/evm/vm.go index d8c9c3adf764..48beda0c9a70 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" @@ -61,7 +62,6 @@ import ( "github.com/ava-labs/avalanchego/graft/subnet-evm/precompile/precompileconfig" "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/warp" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -77,6 +77,7 @@ import ( "github.com/ava-labs/avalanchego/vms/evm/acp226" "github.com/ava-labs/avalanchego/vms/evm/sync/customrawdb" "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" @@ -87,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" ) @@ -114,6 +116,7 @@ const ( ethMetricsPrefix = "eth" sdkMetricsPrefix = "sdk" chainStateMetricsPrefix = "chain_state" + warpMetricsPrefix = "warp" ) // Define the API endpoints for the VM @@ -251,9 +254,12 @@ 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] + warpService *warpRPC.Service // Initialize only sets these if nil so they can be overridden in tests ethTxPushGossiper avalancheUtils.Atomic[*avalanchegossip.PushGossiper[*GossipEthTx]] @@ -456,16 +462,12 @@ func (vm *VM) Initialize( return fmt.Errorf("failed to initialize uptime tracker: %w", err) } - // Initialize warp backend - offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) - for i, hexMsg := range vm.config.WarpOffChainMessages { - 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 { @@ -474,19 +476,13 @@ func (vm *VM) Initialize( } } - vm.warpBackend, err = warp.NewBackend( - vm.ctx.NetworkID, - vm.ctx.ChainID, - vm.ctx.WarpSigner, - vm, - vm.uptimeTracker, - vm.warpDB, - meteredCache, - offchainWarpMessages, - ) - if err != nil { + vm.warpMsgDB = warp.NewDB(vm.warpDB) + 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 } + if err := vm.initializeChain(lastAcceptedHash, vm.ethConfig); err != nil { return err } @@ -494,7 +490,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.P2PNetwork().AddHandler(p2p.SignatureRequestHandlerID, warpHandler); err != nil { return err } @@ -1058,25 +1054,22 @@ 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. +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 @@ -1169,7 +1162,28 @@ 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) - if err := handler.RegisterName("warp", warp.NewAPI(vm.ctx, vm.warpBackend, signatureAggregator)); err != nil { + offChainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) + for i, hexMsg := range vm.config.WarpOffChainMessages { + offChainWarpMessages[i] = []byte(hexMsg) + } + + var err error + vm.warpService, err = warpRPC.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.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 14ce7884f1df..080147d8b1fc 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" @@ -29,7 +30,6 @@ import ( "github.com/ava-labs/avalanchego/graft/subnet-evm/plugin/evm/customheader" "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/warp" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/network/p2p/acp118" @@ -46,6 +46,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" @@ -156,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.warpBackend.GetMessageSignature(t.Context(), unsignedMessage) - require.ErrorIs(err, warp.ErrVerifyWarpMessage) - _, err = tvm.vm.warpBackend.GetBlockSignature(t.Context(), blk.ID()) - require.ErrorIs(err, warp.ErrValidateBlock) + // 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.warpBackend.GetMessageSignature(t.Context(), unsignedMessage) - 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: @@ -180,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.warpBackend.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) { @@ -838,13 +838,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) @@ -852,14 +856,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.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.warpBackend.GetBlockSignature(t.Context(), lastAcceptedID) + knownBlockSignature, err := tvm.vm.warpService.GetBlockSignature(t.Context(), lastAcceptedID) require.NoError(t, err) type testCase struct { @@ -962,10 +966,11 @@ 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) + storedMsg, err := vm.warpMsgDB.Get(unsignedMsg.ID()) require.NoError(t, err) + require.Equal(t, unsignedMsg.ID(), storedMsg.ID()) messages = append(messages, unsignedMsg) } @@ -979,9 +984,9 @@ func TestClearWarpDB(t *testing.T) { // check messages are still present for _, message := range messages { - bytes, err := vm.warpBackend.GetMessageSignature(t.Context(), message) + 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())) @@ -998,7 +1003,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.warpMsgDB.Get(message.ID()) + require.ErrorIs(t, err, database.ErrNotFound) } } diff --git a/graft/subnet-evm/plugin/evm/wrapped_block.go b/graft/subnet-evm/plugin/evm/wrapped_block.go index e793a407d4bc..7254179aa801 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 b1107db97f91..aee63ccc29ad 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 1741f2f3d324..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 { - AddMessage(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 55ba7ee01402..a7b606e052b0 100644 --- a/graft/subnet-evm/tests/warp/warp_test.go +++ b/graft/subnet-evm/tests/warp/warp_test.go @@ -29,7 +29,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" - "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/utils" "github.com/ava-labs/avalanchego/ids" @@ -41,8 +40,9 @@ import ( "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" - warpBackend "github.com/ava-labs/avalanchego/graft/subnet-evm/warp" + 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" @@ -354,13 +354,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), @@ -397,7 +397,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( @@ -436,9 +436,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) @@ -467,35 +467,35 @@ 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) 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(), warp.WarpQuorumDenominator, subnetIDStr) + signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, w.addressedCallUnsignedMessage.ID(), warpContract.WarpQuorumDenominator, subnetID) 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, subnetID) 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 } @@ -515,12 +515,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), @@ -528,7 +528,7 @@ func (w *warpTest) deliverAddressedCallToReceivingSubnet() { Data: packedInput, AccessList: types.AccessList{ { - Address: warp.ContractAddress, + Address: warpContract.ContractAddress, StorageKeys: predicate.New(w.addressedCallSignedMessage.Bytes()), }, }, @@ -549,7 +549,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) @@ -573,12 +573,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), @@ -586,7 +586,7 @@ func (w *warpTest) deliverBlockHashPayload() { Data: packedInput, AccessList: types.AccessList{ { - Address: warp.ContractAddress, + Address: warpContract.ContractAddress, StorageKeys: predicate.New(w.blockPayloadSignedMessage.Bytes()), }, }, @@ -606,7 +606,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) @@ -627,7 +627,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) @@ -703,7 +703,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() { @@ -713,14 +713,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), @@ -736,11 +736,11 @@ 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) - 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...") @@ -748,25 +748,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, subnetID) 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), @@ -774,7 +774,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 ce847ed95753..000000000000 --- a/graft/subnet-evm/warp/backend.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (C) 2019, 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 baeb967161e9..000000000000 --- a/graft/subnet-evm/warp/backend_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (C) 2019, 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 6aec6245a872..000000000000 --- a/graft/subnet-evm/warp/client.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2019, 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/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 4e1f5e28fcc1..000000000000 --- a/graft/subnet-evm/warp/messages/codec.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2019, 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 12d3a5df2fec..000000000000 --- a/graft/subnet-evm/warp/messages/payload.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2019, 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 839341f7a975..000000000000 --- a/graft/subnet-evm/warp/messages/validator_uptime.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (C) 2019, 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 b4662ce826da..000000000000 --- a/graft/subnet-evm/warp/service.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (C) 2019, 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") - errCannotRetrieveValidatorSet = errors.New("cannot retrieve validator set") -) - -// 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 - } - - validatorSets, err := validatorState.GetWarpValidatorSets(ctx, pChainHeight) - if err != nil { - return nil, fmt.Errorf("failed to get validator sets: %w", err) - } - validatorSet, ok := validatorSets[subnetID] - if !ok { - return nil, fmt.Errorf("%w: %s source subnet not found", errCannotRetrieveValidatorSet, subnetID) - } - 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 24f17bb4837b..000000000000 --- a/graft/subnet-evm/warp/verifier_backend.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (C) 2019, 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 ba987ea9b515..000000000000 --- a/graft/subnet-evm/warp/verifier_backend_test.go +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright (C) 2019, 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/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, utilstest.SubnetEVMTestChainID) - - 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, utilstest.SubnetEVMTestChainID) - - 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, utilstest.SubnetEVMTestChainID) - - validationID := ids.GenerateTestID() - nodeID := ids.GenerateTestNodeID() - start := time.Now() - startTime := uint64(start.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{} - clk.Set(start) - 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 c8b4bb4929af..000000000000 --- a/graft/subnet-evm/warp/verifier_stats.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2019, 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 1abf66b3779f..000000000000 --- a/graft/subnet-evm/warp/warptest/block_client.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2019, 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/db.go b/vms/evm/warp/db.go new file mode 100644 index 000000000000..9f4c9a2999a7 --- /dev/null +++ b/vms/evm/warp/db.go @@ -0,0 +1,50 @@ +// Copyright (C) 2019, 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 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 { + 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..6b6d7c929afd --- /dev/null +++ b/vms/evm/warp/db_test.go @@ -0,0 +1,98 @@ +// Copyright (C) 2019, 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/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" +) + +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) +} + +func TestDBGet(t *testing.T) { + 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 new file mode 100644 index 000000000000..31773ed9b6e6 --- /dev/null +++ b/vms/evm/warp/helpers_test.go @@ -0,0 +1,165 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +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/snow/engine/common" + "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" +) + +var ( + 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 +) + +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) + } +} + +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 +} + +// 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 and any error +func sendSignatureRequest( + t *testing.T, + ctx context.Context, + setup *handlerTestSetup, + message *warp.UnsignedMessage, +) ([]byte, *common.AppError) { + t.Helper() + + request := &sdk.SignatureRequest{ + Message: message.Bytes(), + } + requestBytes, err := proto.Marshal(request) + require.NoError(t, err) + + var signature []byte + var appErr *common.AppError + responseChan := make(chan struct{}) + onResponse := func(_ context.Context, _ ids.NodeID, responseBytes []byte, err error) { + defer close(responseChan) + + if err != nil { + appErr, _ = err.(*common.AppError) + return + } + + 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, appErr +} diff --git a/vms/evm/warp/rpc/client.go b/vms/evm/warp/rpc/client.go new file mode 100644 index 000000000000..4df20faabf64 --- /dev/null +++ b/vms/evm/warp/rpc/client.go @@ -0,0 +1,74 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package rpc + +import ( + "context" + "fmt" + + "github.com/ava-labs/libevm/common/hexutil" + "github.com/ava-labs/libevm/rpc" + + "github.com/ava-labs/avalanchego/ids" +) + +type Client struct { + client *rpc.Client +} + +// NewClient returns a Client for interacting with EVM chain +func NewClient(uri, chain string) (*Client, error) { + c, err := rpc.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", uri, chain)) + if err != nil { + return nil, fmt.Errorf("failed to dial client: %w", err) + } + return &Client{ + client: c, + }, 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 { + return nil, fmt.Errorf("call to warp_getMessage failed: %w", err) + } + 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 { + return nil, fmt.Errorf("call to warp_getMessageSignature failed: %w", err) + } + 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 { + return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed: %w", err) + } + 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 { + return nil, fmt.Errorf("call to warp_getBlockSignature failed: %w", err) + } + 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 { + return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed: %w", err) + } + return res, nil +} diff --git a/vms/evm/warp/rpc/service.go b/vms/evm/warp/rpc/service.go new file mode 100644 index 000000000000..917b8dbdc499 --- /dev/null +++ b/vms/evm/warp/rpc/service.go @@ -0,0 +1,242 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package rpc + +import ( + "context" + "errors" + "fmt" + + "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/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" + + evmwarp "github.com/ava-labs/avalanchego/vms/evm/warp" +) + +var ( + errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") + ErrMessageNotFound = errors.New("message not found") + ErrBlockNotFound = errors.New("block not found") +) + +// Service provides an RPC interface for warp messaging. +// It provides caching and orchestration over the core warp primitives. +type Service struct { + networkID uint32 + chainID ids.ID + validatorState validators.State + + db *evmwarp.DB + signer warp.Signer + verifier *evmwarp.Verifier + signatureAggregator *acp118.SignatureAggregator + + messageCache *lru.Cache[ids.ID, *warp.UnsignedMessage] + signatureCache cache.Cacher[ids.ID, []byte] + offChainMessages map[ids.ID]*warp.UnsignedMessage +} + +func NewService( + networkID uint32, + chainID ids.ID, + validatorState validators.State, + db *evmwarp.DB, + signer warp.Signer, + verifier *evmwarp.Verifier, + signatureCache cache.Cacher[ids.ID, []byte], + signatureAggregator *acp118.SignatureAggregator, + offChainMessages [][]byte, +) (*Service, 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 != networkID { + return nil, fmt.Errorf("wrong network ID at index %d", i) + } + + if unsignedMsg.SourceChainID != 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 &Service{ + networkID: networkID, + chainID: chainID, + validatorState: validatorState, + db: db, + signer: signer, + verifier: verifier, + signatureAggregator: signatureAggregator, + messageCache: lru.NewCache[ids.ID, *warp.UnsignedMessage](500), + signatureCache: signatureCache, + offChainMessages: offchainMsgs, + }, nil +} + +// GetMessage returns the Warp message associated with the given 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) + } + return hexutil.Bytes(message.Bytes()), nil +} + +// getMessage retrieves a message from cache, offchain messages, or database. +func (s *Service) getMessage(_ context.Context, messageID ids.ID) (*warp.UnsignedMessage, error) { + if msg, ok := s.messageCache.Get(messageID); ok { + return msg, nil + } + + if msg, ok := s.offChainMessages[messageID]; ok { + return msg, nil + } + + msg, err := s.db.Get(messageID) + if err != nil { + return nil, err + } + + s.messageCache.Put(messageID, msg) + return msg, nil +} + +// 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(ctx, messageID) + if err != nil { + return nil, fmt.Errorf("%w %s: %w", ErrMessageNotFound, messageID, err) + } + return s.signMessage(ctx, unsignedMessage) +} + +// 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) { + blockHashPayload, err := payload.NewHash(blockID) + if err != nil { + return nil, fmt.Errorf("failed to create block hash payload: %w", err) + } + + unsignedMessage, err := warp.NewUnsignedMessage( + s.networkID, + s.chainID, + blockHashPayload.Bytes(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create unsigned warp message: %w", err) + } + + return s.signMessage(ctx, unsignedMessage) +} + +// 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(ctx, messageID) + if err != nil { + return nil, err + } + return s.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) +} + +// 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 { + return nil, err + } + unsignedMessage, err := warp.NewUnsignedMessage(s.networkID, s.chainID, blockHashPayload.Bytes()) + if err != nil { + return nil, err + } + + return s.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetID) +} + +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 + } + + validatorSets, err := validatorState.GetWarpValidatorSets(ctx, pChainHeight) + if err != nil { + return nil, fmt.Errorf("failed to get validator sets: %w", err) + } + validatorSet, ok := validatorSets[subnetID] + if !ok || 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 := s.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 +} + +// signMessage verifies, signs, and caches a signature for the given unsigned message. +func (s *Service) signMessage(ctx context.Context, unsignedMessage *warp.UnsignedMessage) (hexutil.Bytes, error) { + msgID := unsignedMessage.ID() + + if sig, ok := s.signatureCache.Get(msgID); ok { + return sig, nil + } + + if err := s.verifier.Verify(ctx, unsignedMessage); err != nil { + 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) + } + + signature, err := s.signer.Sign(unsignedMessage) + if err != nil { + return nil, fmt.Errorf("failed to sign message %s: %w", msgID, err) + } + + s.signatureCache.Put(msgID, signature) + return signature, nil +} diff --git a/vms/evm/warp/verifier.go b/vms/evm/warp/verifier.go new file mode 100644 index 000000000000..1a9b25724ea9 --- /dev/null +++ b/vms/evm/warp/verifier.go @@ -0,0 +1,249 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "errors" + "fmt" + + "github.com/prometheus/client_golang/prometheus" + + "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/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +const ( + ParseErrCode = iota + 1 + VerifyErrCode + + codecVersion = 0 + maxMessageSize = 24 * units.KiB +) + +var ( + _ acp118.Verifier = (*acp118Handler)(nil) + + c codec.Manager +) + +func init() { + c = codec.NewManager(maxMessageSize) + lc := linearcodec.NewDefault() + + err := errors.Join( + lc.RegisterType(&ValidatorUptime{}), + c.RegisterCodec(codecVersion, lc), + ) + if err != nil { + panic(err) + } +} + +// 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"` + TotalUptimeSeconds uint64 `serialize:"true"` // in seconds +} + +// ParseValidatorUptime converts a slice of bytes into a ValidatorUptime. +func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { + var msg ValidatorUptime + if _, err := c.Unmarshal(b, &msg); err != nil { + return nil, err + } + return &msg, nil +} + +// Bytes returns the binary representation of this payload. +func (v *ValidatorUptime) Bytes() ([]byte, error) { + return c.Marshal(codecVersion, v) +} + +// 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. +type Verifier struct { + db *DB + blockClient BlockStore + uptimeTracker *uptimetracker.UptimeTracker + + messageParseFail prometheus.Counter + addressedCallVerifyFail prometheus.Counter + blockVerifyFail prometheus.Counter + uptimeVerifyFail prometheus.Counter +} + +// NewVerifier creates a new warp message verifier. +func NewVerifier( + db *DB, + blockClient BlockStore, + uptimeTracker *uptimetracker.UptimeTracker, + reg prometheus.Registerer, +) *Verifier { + 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. +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 { + return nil + } else if !errors.Is(err, database.ErrNotFound) { + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("failed to get message %s: %s", messageID, err), + } + } + + parsed, err := payload.Parse(unsignedMessage.Payload) + if err != nil { + v.messageParseFail.Inc() + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("failed to parse payload: %s", err), + } + } + + switch p := parsed.(type) { + case *payload.AddressedCall: + return v.verifyOffchainAddressedCall(p) + case *payload.Hash: + return v.verifyBlockMessage(ctx, p) + default: + v.messageParseFail.Inc() + 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 (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() + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to get block %s: %s", blockID, err), + } + } + + return nil +} + +// verifyOffchainAddressedCall verifies the addressed call message +func (v *Verifier) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) *common.AppError { + if len(addressedCall.SourceAddress) != 0 { + v.addressedCallVerifyFail.Inc() + return &common.AppError{ + Code: VerifyErrCode, + Message: "source address should be empty for offchain addressed messages", + } + } + + uptimeMsg, err := ParseValidatorUptime(addressedCall.Payload) + if err != nil { + v.messageParseFail.Inc() + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("failed to parse addressed call message: %s", err), + } + } + + if err := v.verifyUptimeMessage(uptimeMsg); err != nil { + v.uptimeVerifyFail.Inc() + return err + } + + return nil +} + +func (v *Verifier) verifyUptimeMessage(uptimeMsg *ValidatorUptime) *common.AppError { + currentUptime, _, err := v.uptimeTracker.GetUptime(uptimeMsg.ValidationID) + if err != nil { + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to get uptime: %s", err), + } + } + + currentUptimeSeconds := uint64(currentUptime.Seconds()) + // verify the current uptime against the total uptime in the message + 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.TotalUptimeSeconds, uptimeMsg.ValidationID), + } + } + + return nil +} + +// acp118Handler supports signing warp messages requested by peers. +type acp118Handler struct { + verifier *Verifier +} + +func (a *acp118Handler) Verify(ctx context.Context, message *warp.UnsignedMessage, _ []byte) *common.AppError { + return a.verifier.Verify(ctx, message) +} + +// NewHandler returns a handler for signing warp messages requested by peers. +func NewHandler( + signatureCache cache.Cacher[ids.ID, []byte], + verifier *Verifier, + signer warp.Signer, +) *acp118.Handler { + return acp118.NewCachedHandler( + signatureCache, + &acp118Handler{verifier: verifier}, + signer, + ) +} diff --git a/vms/evm/warp/verifier_test.go b/vms/evm/warp/verifier_test.go new file mode 100644 index 000000000000..2334b4775a61 --- /dev/null +++ b/vms/evm/warp/verifier_test.go @@ -0,0 +1,520 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "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/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/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +// 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, + TotalUptimeSeconds: 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, + }, + TotalUptimeSeconds: 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, + }, + }, + { + 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, + }, + TotalUptimeSeconds: ^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 { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + gotBytes, err := tt.msg.Bytes() + require.NoError(err) + require.Equal(tt.wantBytes, gotBytes) + + gotMsg, err := ParseValidatorUptime(tt.wantBytes) + require.NoError(err) + require.Equal(tt.msg.ValidationID, gotMsg.ValidationID) + require.Equal(tt.msg.TotalUptimeSeconds, gotMsg.TotalUptimeSeconds) + }) + } +} + +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, prometheus.NewRegistry()) + + require.NoError(t, messageDB.Add(testUnsignedMessage)) + + appErr := verifier.Verify(t.Context(), testUnsignedMessage) + require.Nil(t, appErr) + + signature, err := warpSigner.Sign(testUnsignedMessage) + require.NoError(t, err) + + 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, prometheus.NewRegistry()) + + unknownPayload, err := payload.NewAddressedCall([]byte{}, []byte("unknown message")) + require.NoError(t, err) + unknownMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, unknownPayload.Bytes()) + require.NoError(t, err) + + 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) { + blkID := ids.GenerateTestID() + blockStore := testBlockStore(set.Of(blkID)) + db := memdb.New() + + sk, err := localsigner.New() + require.NoError(t, err) + warpSigner := warp.NewSigner(sk, networkID, sourceChainID) + + messageDB := NewDB(db) + verifier := NewVerifier(messageDB, blockStore, nil, prometheus.NewRegistry()) + + blockHashPayload, err := payload.NewHash(blkID) + require.NoError(t, err) + unsignedMessage, err := warp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) + require.NoError(t, err) + wantSig, err := warpSigner.Sign(unsignedMessage) + require.NoError(t, err) + + appErr := verifier.Verify(t.Context(), unsignedMessage) + require.Nil(t, appErr) + + signature, err := warpSigner.Sign(unsignedMessage) + require.NoError(t, err) + require.Equal(t, wantSig, signature) + + 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, nil, nil, prometheus.NewRegistry()) + + invalidPayload := []byte{0xFF, 0xFF, 0xFF, 0xFF} + msg, err := warp.NewUnsignedMessage(networkID, sourceChainID, invalidPayload) + require.NoError(t, err) + + appErr := v.Verify(t.Context(), msg) + 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, nil, 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(t.Context(), msg) + require.Equal(t, int32(ParseErrCode), appErr.Code) + + require.Equal(t, float64(1), testutil.ToFloat64(v.messageParseFail)) +} + +func TestHandlerMessageSignature(t *testing.T) { + metricstest.WithMetrics(t) + + ctx := t.Context() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + tests := []struct { + name string + setupMessage func(db *DB) *warp.UnsignedMessage + wantErrCode error + wantSignature bool + wantMetrics metricExpectations + }{ + { + name: "known message", + 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) + require.NoError(t, db.Add(msg)) + return msg + }, + wantSignature: true, + wantMetrics: metricExpectations{ + messageParseFail: 0, + blockVerifyFail: 0, + }, + }, + { + name: "unknown message", + setupMessage: func(_ *DB) *warp.UnsignedMessage { + unknownPayload, err := payload.NewAddressedCall([]byte{}, []byte("unknown message")) + require.NoError(t, err) + msg, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) + require.NoError(t, err) + return msg + }, + wantErrCode: &common.AppError{Code: ParseErrCode}, + wantMetrics: metricExpectations{ + messageParseFail: 1, + blockVerifyFail: 0, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setup := setupHandler(t, ctx, memdb.New(), nil, nil, snowCtx.NetworkID, snowCtx.ChainID) + + message := tt.setupMessage(setup.db) + _, appErr := sendSignatureRequest(t, ctx, setup, message) + if tt.wantErrCode != nil { + require.ErrorIs(t, appErr, tt.wantErrCode) + } else { + require.Nil(t, appErr) + } + + requireMetrics(t, setup.verifier, tt.wantMetrics) + }) + } +} + +func TestHandlerBlockSignature(t *testing.T) { + metricstest.WithMetrics(t) + + ctx := t.Context() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + knownBlkID := ids.GenerateTestID() + blockStore := testBlockStore(set.Of(knownBlkID)) + + tests := []struct { + name string + blockID ids.ID + wantErrCode *int32 // nil if no error expected + wantMetrics metricExpectations + }{ + { + name: "known block", + blockID: knownBlkID, + wantMetrics: metricExpectations{ + blockVerifyFail: 0, + messageParseFail: 0, + }, + }, + { + name: "unknown block", + blockID: ids.GenerateTestID(), + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), + wantMetrics: metricExpectations{ + blockVerifyFail: 1, + messageParseFail: 0, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setup := setupHandler(t, ctx, memdb.New(), blockStore, nil, snowCtx.NetworkID, snowCtx.ChainID) + + hashPayload, err := payload.NewHash(tt.blockID) + require.NoError(t, err) + message, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) + require.NoError(t, err) + + _, 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) + }) + } +} + +func TestHandlerUptimeSignature(t *testing.T) { + metricstest.WithMetrics(t) + + ctx := t.Context() + snowCtx := snowtest.Context(t, snowtest.CChainID) + + validationID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + startTime := uint64(time.Now().Unix()) + requestedUptime := uint64(80) + + tests := []struct { + name string + sourceAddress []byte + validationID ids.ID + setupUptimeTracker func(*uptimetracker.UptimeTracker, *mockable.Clock) + wantErrCode *int32 // nil if no error expected + wantMetrics metricExpectations + }{ + { + name: "non-empty source address", + sourceAddress: []byte{1, 2, 3}, + validationID: ids.GenerateTestID(), + setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) {}, + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), + wantMetrics: metricExpectations{ + addressedCallVerifyFail: 1, + }, + }, + { + name: "unknown validation ID", + sourceAddress: []byte{}, + validationID: ids.GenerateTestID(), + setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) {}, + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), + wantMetrics: metricExpectations{ + uptimeVerifyFail: 1, + }, + }, + { + name: "validator not connected", + sourceAddress: []byte{}, + validationID: validationID, + setupUptimeTracker: func(_ *uptimetracker.UptimeTracker, _ *mockable.Clock) { + }, + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), + 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)) + }, + wantErrCode: func() *int32 { i := int32(VerifyErrCode); return &i }(), + 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{}, + }, + } + + 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 + }, + } + + 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(), nil, uptimeTracker, snowCtx.NetworkID, snowCtx.ChainID) + + uptimePayload := &ValidatorUptime{ + ValidationID: tt.validationID, + TotalUptimeSeconds: 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) + + _, 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) + }) + } +} + +func TestHandlerCacheBehavior(t *testing.T) { + metricstest.WithMetrics(t) + + ctx := t.Context() + snowCtx := snowtest.Context(t, snowtest.CChainID) + 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) + message, err := warp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) + require.NoError(t, err) + require.NoError(t, setup.db.Add(message)) + + 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, appErr := sendSignatureRequest(t, ctx, setup, message) + require.Nil(t, appErr) + require.Equal(t, firstSignature, secondSignature) +}