diff --git a/vms/saevm/BUILD.bazel b/vms/saevm/BUILD.bazel index 7670009149e2..fea476c55681 100644 --- a/vms/saevm/BUILD.bazel +++ b/vms/saevm/BUILD.bazel @@ -4,12 +4,14 @@ load("//.bazel:defs.bzl", "go_test") go_library( name = "saevm", srcs = [ + "config.go", "factory.go", "vm.go", ], importpath = "github.com/ava-labs/avalanchego/vms/saevm", visibility = ["//visibility:public"], deps = [ + "//cache/lru", "//database", "//database/prefixdb", "//graft/coreth/params", @@ -30,12 +32,13 @@ go_library( "//vms/components/gas", "//vms/evm/acp226", "//vms/evm/database", + "//vms/platformvm/warp", "//vms/saevm/api", "//vms/saevm/hook", "//vms/saevm/hook/acp176", "//vms/saevm/tx", "//vms/saevm/txpool", - "//vms/saevm/warp/backend", + "//vms/saevm/warp", "@com_github_ava_labs_libevm//common/hexutil", "@com_github_ava_labs_libevm//core", "@com_github_ava_labs_libevm//core/rawdb", @@ -52,7 +55,10 @@ go_library( go_test( name = "saevm_test", - srcs = ["vm_warp_test.go"], + srcs = [ + "config_test.go", + "vm_warp_test.go", + ], embed = [":saevm"], deps = [ "//database/memdb", @@ -62,7 +68,6 @@ go_test( "//graft/coreth/plugin/evm/customheader", "//graft/coreth/plugin/evm/vmtest", "//graft/coreth/precompile/contracts/warp", - "//graft/coreth/warp", "//ids", "//snow", "//snow/engine/common", @@ -80,6 +85,7 @@ go_test( "//vms/evm/predicate", "//vms/platformvm/warp", "//vms/platformvm/warp/payload", + "//vms/saevm/warp", "@com_github_ava_labs_libevm//common", "@com_github_ava_labs_libevm//core/txpool/legacypool", "@com_github_ava_labs_libevm//core/types", diff --git a/vms/saevm/config.go b/vms/saevm/config.go new file mode 100644 index 000000000000..af7f1143b319 --- /dev/null +++ b/vms/saevm/config.go @@ -0,0 +1,60 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package saevm + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/ava-labs/libevm/common/hexutil" + + "github.com/ava-labs/avalanchego/vms/components/gas" + + avawarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +type Config struct { + // GasTarget is the target gas per second that this node will attempt to use + // when creating blocks. If this config is not specified, the node will + // default to use the parent block's target gas per second. + GasTarget *gas.Gas `json:"gas-target,omitempty"` + + // MinDelayTarget is the minimum delay between blocks (in milliseconds) that + // this node will attempt to use when creating blocks. If this config is not + // specified, the node will default to use the parent block's target delay + // per second. + MinDelayTarget *uint64 `json:"min-delay-target,omitempty"` + + // WarpOffChainMessages encodes off-chain messages (unrelated to any + // on-chain event ie. block or AddressedCall) that the node should be + // willing to sign. + WarpOffChainMessages []hexutil.Bytes `json:"warp-off-chain-messages"` +} + +func ParseConfig(b []byte) (Config, error) { + var c Config + if len(b) == 0 { + return c, nil + } + + if err := json.Unmarshal(b, &c); err != nil { + return Config{}, fmt.Errorf("json.Unmarshal(%T): %w", c, err) + } + return c, nil +} + +var errParsingWarpMessage = errors.New("parsing warp message") + +func (c Config) WarpMessages() ([]*avawarp.UnsignedMessage, error) { + msgs := make([]*avawarp.UnsignedMessage, len(c.WarpOffChainMessages)) + for i, bytes := range c.WarpOffChainMessages { + msg, err := avawarp.ParseUnsignedMessage(bytes) + if err != nil { + return nil, fmt.Errorf("%w: at index %d: %w", errParsingWarpMessage, i, err) + } + msgs[i] = msg + } + return msgs, nil +} diff --git a/vms/saevm/config_test.go b/vms/saevm/config_test.go new file mode 100644 index 000000000000..f0ec888fa6f7 --- /dev/null +++ b/vms/saevm/config_test.go @@ -0,0 +1,65 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package saevm + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +func TestConfig_WarpMessages(t *testing.T) { + payload, err := payload.NewAddressedCall( + utils.RandomBytes(20), + []byte("test"), + ) + require.NoError(t, err) + + msg, err := warp.NewUnsignedMessage(12345, ids.GenerateTestID(), payload.Bytes()) + require.NoError(t, err) + + tests := []struct { + name string + bytes [][]byte + want []*warp.UnsignedMessage + wantErr error + }{ + { + name: "empty", + want: []*warp.UnsignedMessage{}, + }, + { + name: "single_message", + bytes: [][]byte{msg.Bytes()}, + want: []*warp.UnsignedMessage{msg}, + }, + { + name: "multiple_messages", + bytes: [][]byte{msg.Bytes(), msg.Bytes()}, + want: []*warp.UnsignedMessage{msg, msg}, + }, + { + name: "invalid_message", + bytes: [][]byte{{0xff}}, + wantErr: errParsingWarpMessage, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var c Config + for _, msgBytes := range test.bytes { + c.WarpOffChainMessages = append(c.WarpOffChainMessages, msgBytes) + } + + got, err := c.WarpMessages() + require.ErrorIs(t, err, test.wantErr) + require.Equal(t, test.want, got) + }) + } +} diff --git a/vms/saevm/hook/points.go b/vms/saevm/hook/points.go index b554e20cc7f7..9ed7162361ea 100644 --- a/vms/saevm/hook/points.go +++ b/vms/saevm/hook/points.go @@ -43,7 +43,7 @@ var _ hook.PointsG[*txpool.Tx] = (*Points)(nil) type Points struct { blockBuilder db database.Database - warpBackend precompileconfig.WarpMessageWriter + warpStorage *warp.Storage } func NewPoints( @@ -53,7 +53,7 @@ func NewPoints( desiredDelayExcess *acp226.DelayExcess, desiredTargetExcess *acp176.TargetExcess, pool *txpool.Txs, - warpBackend precompileconfig.WarpMessageWriter, + warpStorage *warp.Storage, ) *Points { return &Points{ blockBuilder{ @@ -66,7 +66,7 @@ func NewPoints( chainConfig: chainConfig, }, db, - warpBackend, + warpStorage, } } @@ -171,7 +171,7 @@ func (p *Points) AfterExecutingBlock(statedb *state.StateDB, b *types.Block, rec rules := p.chainConfig.Rules(b.Number(), corethparams.IsMergeTODO, b.Time()) acceptCtx := &precompileconfig.AcceptContext{ SnowCtx: p.ctx, - Warp: p.warpBackend, + Warp: p.warpStorage, } if err := warp.HandlePrecompileAccept(rules, acceptCtx, receipts); err != nil { return fmt.Errorf("failed to handle precompile accept for block %s (%d): %w", b.Hash(), b.NumberU64(), err) diff --git a/vms/saevm/vm.go b/vms/saevm/vm.go index f7aee549d471..6a5a69d83120 100644 --- a/vms/saevm/vm.go +++ b/vms/saevm/vm.go @@ -15,7 +15,6 @@ import ( "sync/atomic" "time" - "github.com/ava-labs/libevm/common/hexutil" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" @@ -24,6 +23,7 @@ import ( "github.com/ava-labs/strevm/sae" "github.com/prometheus/client_golang/prometheus" + "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database/prefixdb" "github.com/ava-labs/avalanchego/graft/coreth/params/extras" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" @@ -36,7 +36,6 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/evm/acp226" "github.com/ava-labs/avalanchego/vms/evm/database" "github.com/ava-labs/avalanchego/vms/saevm/api" @@ -48,7 +47,7 @@ import ( avadb "github.com/ava-labs/avalanchego/database" corethparams "github.com/ava-labs/avalanchego/graft/coreth/params" warpcontract "github.com/ava-labs/avalanchego/graft/coreth/precompile/contracts/warp" - warpbackend "github.com/ava-labs/avalanchego/vms/saevm/warp/backend" + saewarp "github.com/ava-labs/avalanchego/vms/saevm/warp" ) // SinceGenesis is a harness around an [sae.VM], providing an `Initialize` @@ -61,7 +60,9 @@ type SinceGenesis struct { db avadb.Database mempool *txpool.Mempool pushGossiper *gossip.PushGossiper[*tx.Tx] - warpBackend *warpbackend.Backend + + // TODO(StephenButtolph): Remove. This is only used by the tests. + warpVerifier *saewarp.Verifier // onClose are executed in reverse order during [SinceGenesis.Shutdown]. // If a resource depends on another resource, it MUST be added AFTER the @@ -79,24 +80,7 @@ func NewSinceGenesis(c sae.Config) *SinceGenesis { } } -type Config struct { - // GasTarget is the target gas per second that this node will attempt to use - // when creating blocks. If this config is not specified, the node will - // default to use the parent block's target gas per second. - GasTarget *gas.Gas `json:"gas-target,omitempty"` - - // MinDelayTarget is the minimum delay between blocks (in milliseconds) that - // this node will attempt to use when creating blocks. If this config is not - // specified, the node will default to use the parent block's target delay - // per second. - MinDelayTarget *uint64 `json:"min-delay-target,omitempty"` - - // WarpOffChainMessages encodes off-chain messages (unrelated to any on-chain event ie. block or AddressedCall) - // that the node should be willing to sign. - // Note: only supports AddressedCall payloads as defined here: - // https://github.com/ava-labs/avalanchego/tree/7623ffd4be915a5185c9ed5e11fa9be15a6e1f00/vms/platformvm/warp/payload#addressedcall - WarpOffChainMessages []hexutil.Bytes `json:"warp-off-chain-messages"` -} +const warpSignatureCacheSize = 512 var ethDBPrefix = []byte("ethdb") @@ -161,30 +145,15 @@ func (vm *SinceGenesis) Initialize( return fmt.Errorf("core.SetupGenesisBlock(...): %w", err) } - var userConfig Config - if len(configBytes) > 0 { - if err := json.Unmarshal(configBytes, &userConfig); err != nil { - return fmt.Errorf("json.Unmarshal(%T): %w", userConfig, err) - } + userConfig, err := ParseConfig(configBytes) + if err != nil { + return err } - // Initialize warp backend - offchainWarpMessages := make([][]byte, len(userConfig.WarpOffChainMessages)) - for i, hexMsg := range userConfig.WarpOffChainMessages { - offchainWarpMessages[i] = []byte(hexMsg) - } - warpBackend, warpSignatureCache, err := warpbackend.New( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - &blockClient{vm: vm}, - avaDB, - offchainWarpMessages, - ) + warpMessages, err := userConfig.WarpMessages() if err != nil { - return fmt.Errorf("warp backend: %w", err) + return err } - vm.warpBackend = warpBackend var desiredDelayExcess *acp226.DelayExcess if userConfig.MinDelayTarget != nil { @@ -198,6 +167,7 @@ func (vm *SinceGenesis) Initialize( } txs := txpool.NewTxs() + warpStorage := saewarp.NewStorage(avaDB, warpMessages...) hooks := hook.NewPoints( snowCtx, avaDB, @@ -205,7 +175,7 @@ func (vm *SinceGenesis) Initialize( desiredDelayExcess, desiredTargetExcess, txs, - vm.warpBackend, + warpStorage, ) inner, err := sae.NewVM(ctx, hooks, vm.config, snowCtx, config, db, genesis.ToBlock(), appSender) if err != nil { @@ -270,7 +240,12 @@ func (vm *SinceGenesis) Initialize( } { // ========== Warp Handler ========== - warpHandler := acp118.NewCachedHandler(warpSignatureCache, warpBackend, snowCtx.WarpSigner) + vm.warpVerifier = saewarp.NewVerifier(&blockClient{vm: inner}, warpStorage) + warpHandler := acp118.NewCachedHandler( + lru.NewCache[ids.ID, []byte](warpSignatureCacheSize), + vm.warpVerifier, + snowCtx.WarpSigner, + ) if err := inner.AddHandler(p2p.SignatureRequestHandlerID, warpHandler); err != nil { return fmt.Errorf("network.AddHandler(warp): %w", err) } @@ -408,12 +383,12 @@ func (vm *SinceGenesis) Shutdown(ctx context.Context) error { return vm.VM.Shutdown(ctx) } -// blockClient adapts [SinceGenesis] to the [warpbackend.BlockClient] interface. +// blockClient adapts [sae.VM] to the [saewarp.BlockClient] interface. type blockClient struct { - vm *SinceGenesis + vm *sae.VM } -var _ warpbackend.BlockClient = (*blockClient)(nil) +var _ saewarp.BlockClient = (*blockClient)(nil) func (c *blockClient) IsAccepted(ctx context.Context, blockID ids.ID) error { b, err := c.vm.GetBlock(ctx, blockID) diff --git a/vms/saevm/vm_warp_test.go b/vms/saevm/vm_warp_test.go index a2a52af40ac2..4edb0c6cbaec 100644 --- a/vms/saevm/vm_warp_test.go +++ b/vms/saevm/vm_warp_test.go @@ -26,7 +26,6 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customheader" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/vmtest" - "github.com/ava-labs/avalanchego/graft/coreth/warp" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/enginetest" @@ -42,6 +41,7 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/evm/predicate" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/avalanchego/vms/saevm/warp" warpcontract "github.com/ava-labs/avalanchego/graft/coreth/precompile/contracts/warp" engcommon "github.com/ava-labs/avalanchego/snow/engine/common" @@ -105,11 +105,11 @@ func TestSendWarpMessage(t *testing.T) { expectedBlockUnsignedMessage, err := avalancheWarp.NewUnsignedMessage(env.snowCtx.NetworkID, env.snowCtx.ChainID, expectedBlockHashPayload.Bytes()) require.NoError(err) - addressedErr := env.vm.warpBackend.Verify(env.ctx, expectedUnsignedMessage, nil) + addressedErr := env.vm.warpVerifier.Verify(env.ctx, expectedUnsignedMessage, nil) require.NotNil(addressedErr) - require.Equal(int32(warp.ParseErrCode), addressedErr.Code) + require.Equal(int32(warp.TypeErrCode), addressedErr.Code) - blockErr := env.vm.warpBackend.Verify(env.ctx, expectedBlockUnsignedMessage, nil) + blockErr := env.vm.warpVerifier.Verify(env.ctx, expectedBlockUnsignedMessage, nil) require.NotNil(blockErr) require.Equal(int32(warp.VerifyErrCode), blockErr.Code) @@ -130,8 +130,8 @@ func TestSendWarpMessage(t *testing.T) { unsignedMessage, err := warpcontract.UnpackSendWarpEventDataToMessage(logData) require.NoError(err) - require.Nil(env.vm.warpBackend.Verify(env.ctx, unsignedMessage, nil)) - require.Nil(env.vm.warpBackend.Verify(env.ctx, expectedBlockUnsignedMessage, nil)) + require.Nil(env.vm.warpVerifier.Verify(env.ctx, unsignedMessage, nil)) + require.Nil(env.vm.warpVerifier.Verify(env.ctx, expectedBlockUnsignedMessage, nil)) } func TestPredicateVerification(t *testing.T) { diff --git a/vms/saevm/warp/BUILD.bazel b/vms/saevm/warp/BUILD.bazel index 412f7ce706ab..3755a18b34d1 100644 --- a/vms/saevm/warp/BUILD.bazel +++ b/vms/saevm/warp/BUILD.bazel @@ -6,17 +6,27 @@ go_library( srcs = [ "precompile_accept.go", "predicates.go", + "storage.go", + "verifier.go", ], importpath = "github.com/ava-labs/avalanchego/vms/saevm/warp", visibility = ["//visibility:public"], deps = [ + "//cache/lru", + "//database", + "//database/prefixdb", "//graft/coreth/params", "//graft/coreth/params/extras", "//graft/coreth/precompile/precompileconfig", + "//ids", + "//network/p2p/acp118", "//snow", + "//snow/engine/common", "//snow/engine/snowman/block", "//utils/set", "//vms/evm/predicate", + "//vms/platformvm/warp", + "//vms/platformvm/warp/payload", "@com_github_ava_labs_libevm//core/types", "@org_uber_go_zap//:zap", ], @@ -27,17 +37,26 @@ go_test( srcs = [ "main_test.go", "predicates_test.go", + "storage_test.go", + "verifier_test.go", ], embed = [":warp"], deps = [ + "//database", + "//database/memdb", "//graft/coreth/core", "//graft/coreth/params", "//graft/coreth/params/extras", "//graft/coreth/precompile/precompileconfig", + "//ids", + "//snow/engine/common", "//snow/engine/snowman/block", "//snow/snowtest", + "//utils", "//utils/set", "//vms/evm/predicate", + "//vms/platformvm/warp", + "//vms/platformvm/warp/payload", "@com_github_ava_labs_libevm//common", "@com_github_ava_labs_libevm//core/types", "@com_github_stretchr_testify//require", diff --git a/vms/saevm/warp/backend/BUILD.bazel b/vms/saevm/warp/backend/BUILD.bazel deleted file mode 100644 index 80e2aba1acc9..000000000000 --- a/vms/saevm/warp/backend/BUILD.bazel +++ /dev/null @@ -1,52 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//.bazel:defs.bzl", "go_test") - -go_library( - name = "backend", - srcs = [ - "backend.go", - "verifier_backend.go", - "verifier_stats.go", - ], - importpath = "github.com/ava-labs/avalanchego/vms/saevm/warp/backend", - visibility = ["//visibility:public"], - deps = [ - "//cache", - "//cache/lru", - "//database", - "//database/prefixdb", - "//graft/coreth/precompile/precompileconfig", - "//ids", - "//snow/engine/common", - "//vms/platformvm/warp", - "//vms/platformvm/warp/payload", - "@com_github_ava_labs_libevm//log", - "@com_github_ava_labs_libevm//metrics", - ], -) - -go_test( - name = "backend_test", - srcs = [ - "backend_test.go", - "verifier_backend_test.go", - ], - embed = [":backend"], - deps = [ - "//database", - "//database/memdb", - "//graft/coreth/precompile/precompileconfig", - "//ids", - "//network/p2p/acp118", - "//proto/pb/sdk", - "//snow/engine/common", - "//snow/snowtest", - "//utils", - "//utils/crypto/bls/signer/localsigner", - "//vms/evm/metrics/metricstest", - "//vms/platformvm/warp", - "//vms/platformvm/warp/payload", - "@com_github_stretchr_testify//require", - "@org_golang_google_protobuf//proto", - ], -) diff --git a/vms/saevm/warp/backend/backend.go b/vms/saevm/warp/backend/backend.go deleted file mode 100644 index 4f0760e69d8e..000000000000 --- a/vms/saevm/warp/backend/backend.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package backend - -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/database/prefixdb" - "github.com/ava-labs/avalanchego/graft/coreth/precompile/precompileconfig" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" -) - -const ( - dbPrefix = "warp" - signatureCacheSize = 500 -) - -var ( - _ precompileconfig.WarpMessageWriter = (*Backend)(nil) - errParsingOffChainMessage = errors.New("failed to parse off-chain message") - - messageCacheSize = 500 -) - -type BlockClient interface { - IsAccepted(ctx context.Context, blockID ids.ID) error -} - -// 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 -} - -// New creates a new Backend with a prefixed database and signature cache. -// It returns the backend and the signature cache (for use with -// acp118.NewCachedHandler). -func New( - networkID uint32, - sourceChainID ids.ID, - warpSigner avalancheWarp.Signer, - blockClient BlockClient, - parentDB database.Database, - offchainMessages [][]byte, -) (*Backend, cache.Cacher[ids.ID, []byte], error) { - signatureCache := lru.NewCache[ids.ID, []byte](signatureCacheSize) - b := &Backend{ - networkID: networkID, - sourceChainID: sourceChainID, - db: prefixdb.New([]byte(dbPrefix), parentDB), - 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, signatureCache, 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) getMessage(messageID ids.ID) (*avalancheWarp.UnsignedMessage, error) { - if message, ok := b.messageCache.Get(messageID); ok { - return message, nil - } - if message, ok := b.offchainAddressedCallMsgs[messageID]; ok { - return message, nil - } - - unsignedMessageBytes, err := b.db.Get(messageID[:]) - if err != nil { - return nil, err - } - - unsignedMessage, err := avalancheWarp.ParseUnsignedMessage(unsignedMessageBytes) - if err != nil { - return nil, fmt.Errorf("failed to parse unsigned message %s: %w", messageID.String(), err) - } - b.messageCache.Put(messageID, unsignedMessage) - - return unsignedMessage, nil -} - -func (b *Backend) signMessage(unsignedMessage *avalancheWarp.UnsignedMessage) ([]byte, error) { - sig, err := b.warpSigner.Sign(unsignedMessage) - if err != nil { - return nil, fmt.Errorf("failed to sign warp message: %w", err) - } - - b.signatureCache.Put(unsignedMessage.ID(), sig) - return sig, nil -} diff --git a/vms/saevm/warp/backend/backend_test.go b/vms/saevm/warp/backend/backend_test.go deleted file mode 100644 index 1b42d9979ca6..000000000000 --- a/vms/saevm/warp/backend/backend_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package backend - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" - "github.com/ava-labs/avalanchego/vms/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 TestAddMessage(t *testing.T) { - sk, err := localsigner.New() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backend, _, err := New(networkID, sourceChainID, warpSigner, nil, memdb.New(), nil) - require.NoError(t, err) - - require.NoError(t, backend.AddMessage(testUnsignedMessage)) -} - -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()) - }, - }, - "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) - - b, _, err := New(networkID, sourceChainID, warpSigner, nil, memdb.New(), test.offchainMessages) - require.ErrorIs(err, test.err) - if test.check != nil { - test.check(require, b) - } - }) - } -} diff --git a/vms/saevm/warp/backend/verifier_backend.go b/vms/saevm/warp/backend/verifier_backend.go deleted file mode 100644 index 8dcd98bd7582..000000000000 --- a/vms/saevm/warp/backend/verifier_backend.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package backend - -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 - if err := b.blockClient.IsAccepted(ctx, blockID); 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/vms/saevm/warp/backend/verifier_backend_test.go b/vms/saevm/warp/backend/verifier_backend_test.go deleted file mode 100644 index 82d05a3e783b..000000000000 --- a/vms/saevm/warp/backend/verifier_backend_test.go +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package backend - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/graft/coreth/precompile/precompileconfig" - "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" -) - -type blockClientFunc func(ctx context.Context, blockID ids.ID) error - -func (f blockClientFunc) IsAccepted(ctx context.Context, blockID ids.ID) error { - return f(ctx, blockID) -} - -func makeBlockClient(accepted ...ids.ID) blockClientFunc { - return func(_ context.Context, blockID ids.ID) error { - for _, id := range accepted { - if id == blockID { - return nil - } - } - return database.ErrNotFound - } -} - -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 precompileconfig.WarpMessageWriter) (request []byte, expectedResponse []byte) - verifyStats func(t *testing.T, stats *verifierStats) - err *common.AppError - }{ - "known message": { - setup: func(backend precompileconfig.WarpMessageWriter) (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(_ precompileconfig.WarpMessageWriter) (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(_ precompileconfig.WarpMessageWriter) (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 { - t.Run(name, func(t *testing.T) { - warpBackend, sigCache, err := New(snowCtx.NetworkID, snowCtx.ChainID, snowCtx.WarpSigner, makeBlockClient(), database, [][]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.stats) - - if len(expectedResponse) == 0 { - require.Empty(t, responseBytes, "expected response to be empty") - return - } - require.NotZero(t, warpBackend.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 := 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 { - t.Run(name, func(t *testing.T) { - warpBackend, sigCache, err := New( - snowCtx.NetworkID, - snowCtx.ChainID, - snowCtx.WarpSigner, - blockClient, - database, - 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.stats) - - if len(expectedResponse) == 0 { - require.Empty(t, responseBytes, "expected response to be empty") - return - } - require.NotZero(t, warpBackend.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/vms/saevm/warp/backend/verifier_stats.go b/vms/saevm/warp/backend/verifier_stats.go deleted file mode 100644 index fab334188094..000000000000 --- a/vms/saevm/warp/backend/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 backend - -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/vms/saevm/warp/storage.go b/vms/saevm/warp/storage.go new file mode 100644 index 000000000000..6671a2028b08 --- /dev/null +++ b/vms/saevm/warp/storage.go @@ -0,0 +1,72 @@ +// 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/cache/lru" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/prefixdb" + "github.com/ava-labs/avalanchego/graft/coreth/precompile/precompileconfig" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +const ( + dbPrefix = "warp" + cacheSize = 500 +) + +var _ precompileconfig.WarpMessageWriter = (*Storage)(nil) + +// Storage persists and fetches warp messages. +type Storage struct { + db database.Database + cache *lru.Cache[ids.ID, *warp.UnsignedMessage] + overrides map[ids.ID]*warp.UnsignedMessage +} + +// NewStorage creates a new Storage backed by the provided database. +func NewStorage(db database.Database, msgs ...*warp.UnsignedMessage) *Storage { + overrides := make(map[ids.ID]*warp.UnsignedMessage, len(msgs)) + for _, m := range msgs { + overrides[m.ID()] = m + } + return &Storage{ + db: prefixdb.New([]byte(dbPrefix), db), + cache: lru.NewCache[ids.ID, *warp.UnsignedMessage](cacheSize), + overrides: overrides, + } +} + +func (b *Storage) AddMessage(m *warp.UnsignedMessage) error { + id := m.ID() + if err := b.db.Put(id[:], m.Bytes()); err != nil { + return fmt.Errorf("writing message: %w", err) + } + b.cache.Put(id, m) + return nil +} + +func (b *Storage) GetMessage(id ids.ID) (*warp.UnsignedMessage, error) { + if m, ok := b.cache.Get(id); ok { + return m, nil + } + if m, ok := b.overrides[id]; ok { + return m, nil + } + + bytes, err := b.db.Get(id[:]) + if err != nil { + return nil, err + } + + m, err := warp.ParseUnsignedMessage(bytes) + if err != nil { + return nil, fmt.Errorf("parsing message %s: %w", id, err) + } + b.cache.Put(id, m) + return m, nil +} diff --git a/vms/saevm/warp/storage_test.go b/vms/saevm/warp/storage_test.go new file mode 100644 index 000000000000..ce367bd7dba7 --- /dev/null +++ b/vms/saevm/warp/storage_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/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +const networkID uint32 = 54321 + +var sourceChainID = ids.GenerateTestID() + +func newHash(tb testing.TB) (*warp.UnsignedMessage, *payload.Hash) { + p, err := payload.NewHash( + ids.GenerateTestID(), + ) + require.NoError(tb, err) + + m, err := warp.NewUnsignedMessage(networkID, sourceChainID, p.Bytes()) + require.NoError(tb, err) + return m, p +} + +func newAddressedCall(tb testing.TB) (*warp.UnsignedMessage, *payload.AddressedCall) { + p, err := payload.NewAddressedCall( + utils.RandomBytes(20), + []byte("test"), + ) + require.NoError(tb, err) + + m, err := warp.NewUnsignedMessage(networkID, sourceChainID, p.Bytes()) + require.NoError(tb, err) + return m, p +} + +func TestStorage(t *testing.T) { + msg, _ := newAddressedCall(t) + tests := []struct { + name string + overrides []*warp.UnsignedMessage + add []*warp.UnsignedMessage + id ids.ID + want *warp.UnsignedMessage + wantErr error + }{ + { + name: "add_get", + add: []*warp.UnsignedMessage{ + msg, + }, + id: msg.ID(), + want: msg, + }, + { + name: "get_override", + overrides: []*warp.UnsignedMessage{ + msg, + }, + id: msg.ID(), + want: msg, + }, + { + name: "get_unknown", + id: msg.ID(), + wantErr: database.ErrNotFound, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db := memdb.New() + s := NewStorage(db, test.overrides...) + for _, msg := range test.add { + require.NoError(t, s.AddMessage(msg)) + } + + // Verify the message is fetchable. + msg, err := s.GetMessage(test.id) + require.ErrorIs(t, err, test.wantErr) + require.Equal(t, test.want, msg) + + // Verify the message was persisted. + s = NewStorage(db, test.overrides...) + msg, err = s.GetMessage(test.id) + require.ErrorIs(t, err, test.wantErr) + require.Equal(t, test.want, msg) + }) + } +} diff --git a/vms/saevm/warp/verifier.go b/vms/saevm/warp/verifier.go new file mode 100644 index 000000000000..ef03af8af112 --- /dev/null +++ b/vms/saevm/warp/verifier.go @@ -0,0 +1,83 @@ +// 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/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/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +const ( + StorageErrCode = iota + 1 + ParseErrCode + TypeErrCode + VerifyErrCode +) + +var _ acp118.Verifier = (*Verifier)(nil) + +type BlockClient interface { + IsAccepted(ctx context.Context, blockID ids.ID) error +} + +// Verifier verifies whether this node should be willing to sign a warp message. +type Verifier struct { + blocks BlockClient + storage *Storage +} + +// NewVerifier returns an ACP-118 message verifier. +func NewVerifier(blocks BlockClient, storage *Storage) *Verifier { + return &Verifier{ + blocks: blocks, + storage: storage, + } +} + +// Verify verifies that this node should sign the unsigned warp message. +func (v *Verifier) Verify(ctx context.Context, m *warp.UnsignedMessage, _ []byte) *common.AppError { + id := m.ID() + _, err := v.storage.GetMessage(id) + if err == nil { + return nil + } + if !errors.Is(err, database.ErrNotFound) { + return &common.AppError{ + Code: StorageErrCode, + Message: fmt.Sprintf("failed to get message %s: %s", id, err.Error()), + } + } + + parsed, err := payload.Parse(m.Payload) + if err != nil { + return &common.AppError{ + Code: ParseErrCode, + Message: "failed to parse payload: " + err.Error(), + } + } + + p, ok := parsed.(*payload.Hash) + if !ok { + return &common.AppError{ + Code: TypeErrCode, + Message: fmt.Sprintf("wrong payload type: %T", p), + } + } + + if err := v.blocks.IsAccepted(ctx, p.Hash); err != nil { + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to get block %s: %s", p.Hash, err.Error()), + } + } + return nil +} diff --git a/vms/saevm/warp/verifier_test.go b/vms/saevm/warp/verifier_test.go new file mode 100644 index 000000000000..3063266ff2ae --- /dev/null +++ b/vms/saevm/warp/verifier_test.go @@ -0,0 +1,97 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "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/engine/common" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +type blocks struct { + accepted set.Set[ids.ID] +} + +func newBlocks(ids ...ids.ID) blocks { + return blocks{ + set.Of(ids...), + } +} + +func (b blocks) IsAccepted(_ context.Context, id ids.ID) error { + if !b.accepted.Contains(id) { + return database.ErrNotFound + } + return nil +} + +func TestVerifier(t *testing.T) { + addressedCallMsg, _ := newAddressedCall(t) + hashMsg, hash := newHash(t) + + invalidPayloadMsg, err := warp.NewUnsignedMessage(networkID, sourceChainID, nil) + require.NoError(t, err) + + tests := []struct { + name string + acceptedBlocks []ids.ID + acceptedMessages []*warp.UnsignedMessage + m *warp.UnsignedMessage + want *common.AppError + }{ + { + name: "known_message", + acceptedMessages: []*warp.UnsignedMessage{ + addressedCallMsg, + }, + m: addressedCallMsg, + }, + { + name: "invalid_payload", + m: invalidPayloadMsg, + want: &common.AppError{ + Code: ParseErrCode, + }, + }, + { + name: "wrong_payload_type", + m: addressedCallMsg, + want: &common.AppError{ + Code: TypeErrCode, + }, + }, + { + name: "accepted_block", + acceptedBlocks: []ids.ID{ + hash.Hash, + }, + m: hashMsg, + }, + { + name: "unaccepted_block", + m: hashMsg, + want: &common.AppError{ + Code: VerifyErrCode, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + v := NewVerifier( + newBlocks(test.acceptedBlocks...), + NewStorage(memdb.New(), test.acceptedMessages...), + ) + err := v.Verify(t.Context(), test.m, nil) + require.ErrorIs(t, err, test.want) + }) + } +}