diff --git a/plugin/evm/warp/backend.go b/plugin/evm/warp/backend.go new file mode 100644 index 0000000000..ac865daf51 --- /dev/null +++ b/plugin/evm/warp/backend.go @@ -0,0 +1,87 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "fmt" + + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/vms/platformvm/teleporter" +) + +var _ WarpBackend = &warpBackend{} + +// WarpBackend 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 WarpBackend interface { + // AddMessage signs [unsignedMessage] and adds it to the warp backend database + AddMessage(ctx context.Context, unsignedMessage *teleporter.UnsignedMessage) error + + // GetSignature returns the signature of the requested message hash. + GetSignature(ctx context.Context, messageHash ids.ID) ([]byte, error) +} + +// warpBackend implements WarpBackend, keeps track of warp messages, and generates message signatures. +type warpBackend struct { + db database.Database + snowCtx *snow.Context + signatureCache *cache.LRU +} + +// NewWarpBackend creates a new WarpBackend, and initializes the signature cache and message tracking database. +func NewWarpBackend(snowCtx *snow.Context, db database.Database, signatureCacheSize int) WarpBackend { + return &warpBackend{ + db: db, + snowCtx: snowCtx, + signatureCache: &cache.LRU{Size: signatureCacheSize}, + } +} + +func (w *warpBackend) AddMessage(ctx context.Context, unsignedMessage *teleporter.UnsignedMessage) error { + messageID := hashing.ComputeHash256Array(unsignedMessage.Bytes()) + + // 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 := w.db.Put(messageID[:], unsignedMessage.Bytes()); err != nil { + return fmt.Errorf("failed to put warp signature in db: %w", err) + } + + signature, err := w.snowCtx.TeleporterSigner.Sign(unsignedMessage) + if err != nil { + return fmt.Errorf("failed to sign warp message: %w", err) + } + + w.signatureCache.Put(ids.ID(messageID), signature) + return nil +} + +func (w *warpBackend) GetSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { + if sig, ok := w.signatureCache.Get(messageID); ok { + return sig.([]byte), nil + } + + unsignedMessageBytes, err := w.db.Get(messageID[:]) + if err != nil { + return nil, fmt.Errorf("failed to get warp message %s from db: %w", messageID.String(), err) + } + + unsignedMessage, err := teleporter.ParseUnsignedMessage(unsignedMessageBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse unsigned message %s: %w", messageID.String(), err) + } + + signature, err := w.snowCtx.TeleporterSigner.Sign(unsignedMessage) + if err != nil { + return nil, fmt.Errorf("failed to sign warp message: %w", err) + } + + w.signatureCache.Put(messageID[:], signature) + return signature, nil +} diff --git a/plugin/evm/warp/backend_test.go b/plugin/evm/warp/backend_test.go new file mode 100644 index 0000000000..de38246d30 --- /dev/null +++ b/plugin/evm/warp/backend_test.go @@ -0,0 +1,88 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/vms/platformvm/teleporter" + "github.com/stretchr/testify/require" +) + +var ( + sourceChainID = ids.GenerateTestID() + destinationChainID = ids.GenerateTestID() + payload = []byte("test") +) + +func TestAddAndGetValidMessage(t *testing.T) { + db := memdb.New() + + snowCtx := snow.DefaultContextTest() + sk, err := bls.NewSecretKey() + require.NoError(t, err) + snowCtx.TeleporterSigner = teleporter.NewSigner(sk, sourceChainID) + backend := NewWarpBackend(snowCtx, db, 500) + + // Create a new unsigned message and add it to the warp backend. + unsignedMsg, err := teleporter.NewUnsignedMessage(sourceChainID, destinationChainID, payload) + require.NoError(t, err) + err = backend.AddMessage(context.Background(), unsignedMsg) + require.NoError(t, err) + + // Verify that a signature is returned successfully, and compare to expected signature. + messageID := hashing.ComputeHash256Array(unsignedMsg.Bytes()) + signature, err := backend.GetSignature(context.Background(), messageID) + require.NoError(t, err) + + expectedSig, err := snowCtx.TeleporterSigner.Sign(unsignedMsg) + require.NoError(t, err) + require.Equal(t, expectedSig, signature) +} + +func TestAddAndGetUnknownMessage(t *testing.T) { + db := memdb.New() + + backend := NewWarpBackend(snow.DefaultContextTest(), db, 500) + unsignedMsg, err := teleporter.NewUnsignedMessage(sourceChainID, destinationChainID, payload) + require.NoError(t, err) + + // Try getting a signature for a message that was not added. + messageID := hashing.ComputeHash256Array(unsignedMsg.Bytes()) + _, err = backend.GetSignature(context.Background(), messageID) + require.Error(t, err) +} + +func TestZeroSizedCache(t *testing.T) { + db := memdb.New() + + snowCtx := snow.DefaultContextTest() + sk, err := bls.NewSecretKey() + require.NoError(t, err) + snowCtx.TeleporterSigner = teleporter.NewSigner(sk, sourceChainID) + + // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. + backend := NewWarpBackend(snowCtx, db, 0) + + // Create a new unsigned message and add it to the warp backend. + unsignedMsg, err := teleporter.NewUnsignedMessage(sourceChainID, destinationChainID, payload) + require.NoError(t, err) + err = backend.AddMessage(context.Background(), unsignedMsg) + require.NoError(t, err) + + // Verify that a signature is returned successfully, and compare to expected signature. + messageID := hashing.ComputeHash256Array(unsignedMsg.Bytes()) + signature, err := backend.GetSignature(context.Background(), messageID) + require.NoError(t, err) + + expectedSig, err := snowCtx.TeleporterSigner.Sign(unsignedMsg) + require.NoError(t, err) + require.Equal(t, expectedSig, signature) +}