From f1d549caeaabbbe1096ae001ef6ad18e9b752775 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 14 Nov 2024 01:43:56 +0300 Subject: [PATCH] Sign validator uptime warp msg (#1367) * Bump avalanchego to master * always sign uptime messages (testing branch) * nits * cleanup * assign to correct `err` * fix handler * move ValidatorUptime type to subnet-evm * disable always signing * implement on the type itself * remove unneeded code * fix ut * add validator state * add pausable uptime manager * remove stuttering name * rename state listener * add uptime tracking to VM * remove unused param * add wg for update validators * update state before network shutdown * restart bootstrapping status in test * add get validator to state * rename uptime to validator * fix mock state * tests * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * use update enum * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * respond to comments * update avalanchego dep branch * reviews * reword errs * fix test changes * fix upgrades after deactivating latest in context * use test branch from avalanchego * use branch commit for ava version * update e2e ava version * update avago dep * remove extra line... * export struct * implement acp118 signer and verifier * avoid revalidating in sign * refactor warp backend to use acp118 handler * prune warp db before backend init * add cache tests * remove uptime msg type * add cache test * fix linter * add validator uptimes * bump avago getcurrentvalidators branch * rename get validator IDs to NodeIDs * sign uptime warp msg base on uptime calculator * add tests * reviews * conflict fix * custom err msg * add listener mock * bump avago test branch * remove config * remove api changes * Revert "remove api changes" This reverts commit 8ef763fca65a7144ac35b69889873728d3485558. * remove wrapped cache * use non-version db for validatorsDB * remove errs from resume and pause * check after stopping * use expectedTime in tests * reviews * Update plugin/evm/vm.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * fix len * fix tests * update avago branch * use ctx from utils * add empty check for source address * nits * remove log * disable validators api by default * fix test context * use interfaces from pkgs * improve comments * Uptime validation nits (#1378) * add uptime warp example * remove log * nit unused interface * add weight and isSov as fields * use validator struct in AddValidator * add comments to example file * fix test * add new fields to tests --------- Signed-off-by: Ceyhun Onur * Update plugin/evm/validators/state.go Co-authored-by: Michael Kaplan <55204436+michaelkaplan13@users.noreply.github.com> Signed-off-by: Ceyhun Onur * pass locker * rename addresscall verifier fn * new fields and refactorings * add new fields * merge nits * fix linter * clarify comments * update comment * remove getnodeID * bump to poc branch * enable validators API by default * reviews * reviews * update comment * update to avago master * revert test change --------- Signed-off-by: Ceyhun Onur Signed-off-by: Ceyhun Onur Co-authored-by: Darioush Jalali Co-authored-by: Michael Kaplan <55204436+michaelkaplan13@users.noreply.github.com> --- examples/sign-uptime-message/main.go | 124 ++++++++++++++++++ .../validators/validatorstest/noop_state.go | 64 +++++++++ plugin/evm/vm.go | 3 + warp/backend.go | 14 +- warp/backend_test.go | 20 ++- warp/handlers/signature_request_test.go | 7 +- warp/messages/payload.go | 6 - warp/messages/validator_uptime.go | 2 +- warp/verifier_backend.go | 82 +++++++++++- warp/verifier_backend_test.go | 108 ++++++++++++++- warp/verifier_stats.go | 24 +++- 11 files changed, 428 insertions(+), 26 deletions(-) create mode 100644 examples/sign-uptime-message/main.go create mode 100644 plugin/evm/validators/validatorstest/noop_state.go diff --git a/examples/sign-uptime-message/main.go b/examples/sign-uptime-message/main.go new file mode 100644 index 0000000000..94929bc2da --- /dev/null +++ b/examples/sign-uptime-message/main.go @@ -0,0 +1,124 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "log" + "net/netip" + "time" + + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/networking/router" + "github.com/ava-labs/avalanchego/utils/compression" + "github.com/ava-labs/avalanchego/utils/logging" + "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" + "github.com/ava-labs/subnet-evm/warp/messages" + + 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( + logging.NoLog{}, + 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/plugin/evm/validators/validatorstest/noop_state.go b/plugin/evm/validators/validatorstest/noop_state.go new file mode 100644 index 0000000000..3594999574 --- /dev/null +++ b/plugin/evm/validators/validatorstest/noop_state.go @@ -0,0 +1,64 @@ +package validatorstest + +import ( + "time" + + ids "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" +) + +var NoOpState interfaces.State = &noOpState{} + +type noOpState struct{} + +func (n *noOpState) GetStatus(vID ids.ID) (bool, error) { return false, nil } + +func (n *noOpState) GetValidationIDs() set.Set[ids.ID] { return set.NewSet[ids.ID](0) } + +func (n *noOpState) GetNodeIDs() set.Set[ids.NodeID] { return set.NewSet[ids.NodeID](0) } + +func (n *noOpState) GetValidator(vID ids.ID) (interfaces.Validator, error) { + return interfaces.Validator{}, nil +} + +func (n *noOpState) GetNodeID(vID ids.ID) (ids.NodeID, error) { return ids.NodeID{}, nil } + +func (n *noOpState) AddValidator(vdr interfaces.Validator) error { + return nil +} + +func (n *noOpState) UpdateValidator(vdr interfaces.Validator) error { + return nil +} + +func (n *noOpState) DeleteValidator(vID ids.ID) error { + return nil +} +func (n *noOpState) WriteState() error { return nil } + +func (n *noOpState) SetStatus(vID ids.ID, isActive bool) error { return nil } + +func (n *noOpState) SetWeight(vID ids.ID, newWeight uint64) error { return nil } + +func (n *noOpState) RegisterListener(interfaces.StateCallbackListener) {} + +func (n *noOpState) GetUptime( + nodeID ids.NodeID, +) (upDuration time.Duration, lastUpdated time.Time, err error) { + return 0, time.Time{}, nil +} + +func (n *noOpState) SetUptime( + nodeID ids.NodeID, + upDuration time.Duration, + lastUpdated time.Time, +) error { + return nil +} + +func (n *noOpState) GetStartTime( + nodeID ids.NodeID, +) (startTime time.Time, err error) { + return time.Time{}, nil +} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index d937c0e175..ee0bf345ef 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -539,6 +539,9 @@ func (vm *VM) Initialize( vm.ctx.ChainID, vm.ctx.WarpSigner, vm, + vm.uptimeManager, + vm.validatorState, + vm.ctx.Lock.RLocker(), vm.warpDB, meteredCache, offchainWarpMessages, diff --git a/warp/backend.go b/warp/backend.go index 6e1f6a9553..eb38f1f410 100644 --- a/warp/backend.go +++ b/warp/backend.go @@ -7,14 +7,17 @@ import ( "context" "errors" "fmt" + "sync" "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/uptime" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" "github.com/ethereum/go-ethereum/log" ) @@ -56,6 +59,9 @@ type backend struct { db database.Database warpSigner avalancheWarp.Signer blockClient BlockClient + uptimeCalculator uptime.Calculator + validatorState interfaces.State + stateLock sync.Locker signatureCache cache.Cacher[ids.ID, []byte] messageCache *cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage] offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.UnsignedMessage @@ -68,6 +74,9 @@ func NewBackend( sourceChainID ids.ID, warpSigner avalancheWarp.Signer, blockClient BlockClient, + uptimeCalculator uptime.Calculator, + validatorsState interfaces.State, + stateLock sync.Locker, db database.Database, signatureCache cache.Cacher[ids.ID, []byte], offchainMessages [][]byte, @@ -79,6 +88,9 @@ func NewBackend( warpSigner: warpSigner, blockClient: blockClient, signatureCache: signatureCache, + uptimeCalculator: uptimeCalculator, + validatorState: validatorsState, + stateLock: stateLock, messageCache: &cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage]{Size: messageCacheSize}, stats: newVerifierStats(), offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), @@ -180,7 +192,7 @@ func (b *backend) GetMessage(messageID ids.ID) (*avalancheWarp.UnsignedMessage, unsignedMessageBytes, err := b.db.Get(messageID[:]) if err != nil { - return nil, fmt.Errorf("failed to get warp message %s from db: %w", messageID.String(), err) + return nil, err } unsignedMessage, err := avalancheWarp.ParseUnsignedMessage(unsignedMessageBytes) diff --git a/warp/backend_test.go b/warp/backend_test.go index cae9c14bc6..331bd7c1ff 100644 --- a/warp/backend_test.go +++ b/warp/backend_test.go @@ -5,15 +5,19 @@ package warp import ( "context" + "sync" "testing" "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/validatorstest" "github.com/ava-labs/subnet-evm/warp/warptest" "github.com/stretchr/testify/require" ) @@ -44,7 +48,7 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, nil) require.NoError(t, err) // Add testUnsignedMessage to the warp backend @@ -67,7 +71,7 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, nil) require.NoError(t, err) // Try getting a signature for a message that was not added. @@ -86,7 +90,7 @@ func TestGetBlockSignature(t *testing.T) { require.NoError(err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, nil) require.NoError(err) blockHashPayload, err := payload.NewHash(blkID) @@ -113,7 +117,7 @@ func TestZeroSizedCache(t *testing.T) { // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 0} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, nil) require.NoError(t, err) // Add testUnsignedMessage to the warp backend @@ -157,6 +161,12 @@ func TestOffChainMessages(t *testing.T) { 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, @@ -167,7 +177,7 @@ func TestOffChainMessages(t *testing.T) { db := memdb.New() messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 0} - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, test.offchainMessages) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, uptime.NoOpCalculator, validatorstest.NoOpState, &sync.RWMutex{}, db, messageSignatureCache, test.offchainMessages) require.ErrorIs(err, test.err) if test.check != nil { test.check(require, backend) diff --git a/warp/handlers/signature_request_test.go b/warp/handlers/signature_request_test.go index 3189478106..af664d5f16 100644 --- a/warp/handlers/signature_request_test.go +++ b/warp/handlers/signature_request_test.go @@ -10,10 +10,12 @@ import ( "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/plugin/evm/message" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/validatorstest" "github.com/ava-labs/subnet-evm/utils" "github.com/ava-labs/subnet-evm/warp" "github.com/ava-labs/subnet-evm/warp/warptest" @@ -33,7 +35,7 @@ func TestMessageSignatureHandler(t *testing.T) { require.NoError(t, err) messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 100} - backend, err := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, database, messageSignatureCache, [][]byte{offchainMessage.Bytes()}) + backend, err := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, uptime.NoOpCalculator, validatorstest.NoOpState, snowCtx.Lock.RLocker(), database, messageSignatureCache, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, []byte("test")) @@ -139,6 +141,9 @@ func TestBlockSignatureHandler(t *testing.T) { snowCtx.ChainID, warpSigner, blockClient, + uptime.NoOpCalculator, + validatorstest.NoOpState, + snowCtx.Lock.RLocker(), database, messageSignatureCache, nil, diff --git a/warp/messages/payload.go b/warp/messages/payload.go index 3776a1356d..facf54524d 100644 --- a/warp/messages/payload.go +++ b/warp/messages/payload.go @@ -20,12 +20,6 @@ type Payload interface { initialize(b []byte) } -// Signable is an optional interface that payloads can implement to allow -// on-the-fly signing of incoming messages by the warp backend. -type Signable interface { - VerifyMesssage(sourceAddress []byte) error -} - func Parse(bytes []byte) (Payload, error) { var payload Payload if _, err := Codec.Unmarshal(bytes, &payload); err != nil { diff --git a/warp/messages/validator_uptime.go b/warp/messages/validator_uptime.go index 3d3e4dd5dd..cd14b39538 100644 --- a/warp/messages/validator_uptime.go +++ b/warp/messages/validator_uptime.go @@ -13,7 +13,7 @@ import ( // has been up for TotalUptime seconds. type ValidatorUptime struct { ValidationID ids.ID `serialize:"true"` - TotalUptime uint64 `serialize:"true"` + TotalUptime uint64 `serialize:"true"` // in seconds bytes []byte } diff --git a/warp/verifier_backend.go b/warp/verifier_backend.go index c70563c585..71a33356cc 100644 --- a/warp/verifier_backend.go +++ b/warp/verifier_backend.go @@ -7,6 +7,9 @@ import ( "context" "fmt" + "github.com/ava-labs/subnet-evm/warp/messages" + + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/snow/engine/common" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" @@ -24,6 +27,11 @@ func (b *backend) Verify(ctx context.Context, unsignedMessage *avalancheWarp.Uns // 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) @@ -36,6 +44,8 @@ func (b *backend) Verify(ctx context.Context, unsignedMessage *avalancheWarp.Uns } switch p := parsed.(type) { + case *payload.AddressedCall: + return b.verifyOffchainAddressedCall(p) case *payload.Hash: return b.verifyBlockMessage(ctx, p) default: @@ -53,7 +63,7 @@ func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payl blockID := blockHashPayload.Hash _, err := b.blockClient.GetAcceptedBlock(ctx, blockID) if err != nil { - b.stats.IncBlockSignatureValidationFail() + b.stats.IncBlockValidationFail() return &common.AppError{ Code: VerifyErrCode, Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), @@ -62,3 +72,73 @@ func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payl 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 { + b.stateLock.Lock() + defer b.stateLock.Unlock() + // first get the validator's nodeID + vdr, err := b.validatorState.GetValidator(uptimeMsg.ValidationID) + if err != nil { + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to get validator for validationID %s: %s", uptimeMsg.ValidationID, err.Error()), + } + } + nodeID := vdr.NodeID + + // then get the current uptime + currentUptime, _, err := b.uptimeCalculator.CalculateUptime(nodeID) + if err != nil { + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to calculate uptime for nodeID %s: %s", nodeID, err.Error()), + } + } + + currentUptimeSeconds := uint64(currentUptime.Seconds()) + // verify the current uptime against the total uptime in the message + if currentUptimeSeconds < uptimeMsg.TotalUptime { + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("current uptime %d is less than queried uptime %d for nodeID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, nodeID), + } + } + + return nil +} diff --git a/warp/verifier_backend_test.go b/warp/verifier_backend_test.go index 54fd8dbf19..a3546f60c7 100644 --- a/warp/verifier_backend_test.go +++ b/warp/verifier_backend_test.go @@ -14,10 +14,16 @@ import ( "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/uptime" "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/timer/mockable" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/plugin/evm/validators" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/validatorstest" "github.com/ava-labs/subnet-evm/utils" + "github.com/ava-labs/subnet-evm/warp/messages" "github.com/ava-labs/subnet-evm/warp/warptest" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -56,7 +62,7 @@ func TestAddressedCallSignatures(t *testing.T) { }, verifyStats: func(t *testing.T, stats *verifierStats) { require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureValidationFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.blockValidationFail.Snapshot().Count()) }, }, "offchain message": { @@ -65,7 +71,7 @@ func TestAddressedCallSignatures(t *testing.T) { }, verifyStats: func(t *testing.T, stats *verifierStats) { require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureValidationFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.blockValidationFail.Snapshot().Count()) }, }, "unknown message": { @@ -78,7 +84,7 @@ func TestAddressedCallSignatures(t *testing.T) { }, verifyStats: func(t *testing.T, stats *verifierStats) { require.EqualValues(t, 1, stats.messageParseFail.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureValidationFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.blockValidationFail.Snapshot().Count()) }, err: &common.AppError{Code: ParseErrCode}, }, @@ -98,7 +104,7 @@ func TestAddressedCallSignatures(t *testing.T) { } else { sigCache = &cache.Empty[ids.ID, []byte]{} } - warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, database, sigCache, [][]byte{offchainMessage.Bytes()}) + warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, uptime.NoOpCalculator, validatorstest.NoOpState, snowCtx.Lock.RLocker(), database, sigCache, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) handler := acp118.NewCachedHandler(sigCache, warpBackend, warpSigner) @@ -177,7 +183,7 @@ func TestBlockSignatures(t *testing.T) { return toMessageBytes(knownBlkID), signature[:] }, verifyStats: func(t *testing.T, stats *verifierStats) { - require.EqualValues(t, 0, stats.blockSignatureValidationFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.blockValidationFail.Snapshot().Count()) require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count()) }, }, @@ -187,7 +193,7 @@ func TestBlockSignatures(t *testing.T) { return toMessageBytes(unknownBlockID), nil }, verifyStats: func(t *testing.T, stats *verifierStats) { - require.EqualValues(t, 1, stats.blockSignatureValidationFail.Snapshot().Count()) + require.EqualValues(t, 1, stats.blockValidationFail.Snapshot().Count()) require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count()) }, err: &common.AppError{Code: VerifyErrCode}, @@ -213,6 +219,9 @@ func TestBlockSignatures(t *testing.T) { snowCtx.ChainID, warpSigner, blockClient, + uptime.NoOpCalculator, + validatorstest.NoOpState, + snowCtx.Lock.RLocker(), database, sigCache, nil, @@ -253,3 +262,90 @@ func TestBlockSignatures(t *testing.T) { } } } + +func TestUptimeSignatures(t *testing.T) { + database := memdb.New() + snowCtx := utils.TestSnowContext() + blsSecretKey, err := bls.NewSecretKey() + require.NoError(t, err) + warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) + + getUptimeMessageBytes := func(sourceAddress []byte, vID ids.ID, totalUptime uint64) ([]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 = &cache.LRU[ids.ID, []byte]{Size: 100} + } else { + sigCache = &cache.Empty[ids.ID, []byte]{} + } + state, err := validators.NewState(memdb.New()) + require.NoError(t, err) + clk := &mockable.Clock{} + uptimeManager := uptime.NewManager(state, clk) + uptimeManager.StartTracking([]ids.NodeID{}) + warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, uptimeManager, state, snowCtx.Lock.RLocker(), database, sigCache, nil) + require.NoError(t, err) + handler := acp118.NewCachedHandler(sigCache, warpBackend, warpSigner) + + // sourceAddress nonZero + protoBytes, _ := getUptimeMessageBytes([]byte{1, 2, 3}, ids.GenerateTestID(), 80) + _, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Contains(t, appErr.Error(), "source address should be empty") + + // not existing validationID + vID := ids.GenerateTestID() + protoBytes, _ = getUptimeMessageBytes([]byte{}, vID, 80) + _, appErr = handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Contains(t, appErr.Error(), "failed to get validator") + + // uptime is less than requested (not connected) + validationID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + require.NoError(t, state.AddValidator(interfaces.Validator{ + ValidationID: validationID, + NodeID: nodeID, + Weight: 1, + StartTimestamp: clk.Unix(), + IsActive: true, + IsSoV: true, + })) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID, 80) + _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Contains(t, appErr.Error(), "current uptime 0 is less than queried uptime 80") + + // uptime is less than requested (not enough) + require.NoError(t, uptimeManager.Connect(nodeID)) + clk.Set(clk.Time().Add(40 * time.Second)) + protoBytes, _ = getUptimeMessageBytes([]byte{}, validationID, 80) + _, appErr = handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + require.ErrorIs(t, appErr, &common.AppError{Code: VerifyErrCode}) + require.Contains(t, appErr.Error(), "current uptime 40 is less than queried uptime 80") + + // valid uptime + clk.Set(clk.Time().Add(40 * time.Second)) + protoBytes, msg := getUptimeMessageBytes([]byte{}, validationID, 80) + responseBytes, appErr := handler.AppRequest(context.Background(), nodeID, time.Time{}, protoBytes) + require.Nil(t, appErr) + expectedSignature, err := 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/warp/verifier_stats.go b/warp/verifier_stats.go index bc56d725a5..d1ef62a50f 100644 --- a/warp/verifier_stats.go +++ b/warp/verifier_stats.go @@ -9,21 +9,35 @@ import ( type verifierStats struct { messageParseFail metrics.Counter + // AddressedCall metrics + addressedCallValidationFail metrics.Counter // BlockRequest metrics - blockSignatureValidationFail metrics.Counter + blockValidationFail metrics.Counter + // Uptime metrics + uptimeValidationFail metrics.Counter } func newVerifierStats() *verifierStats { return &verifierStats{ - messageParseFail: metrics.NewRegisteredCounter("message_parse_fail", nil), - blockSignatureValidationFail: metrics.NewRegisteredCounter("block_signature_validation_fail", nil), + 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) IncBlockSignatureValidationFail() { - h.blockSignatureValidationFail.Inc(1) +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) +}