Skip to content

Commit

Permalink
Warp backend interface and implementation (#452)
Browse files Browse the repository at this point in the history
* base warp backend

* add signature caching

* add docs

* error handling

* pr fixes

* hash unsigned message for key

* quick pr fixes and merge

* save signature instead of whole msg

* use avaGO cache

* rename warpBackend and docs

* fix nits

* Update plugin/evm/warp_backend.go

* Update plugin/evm/warp_backend.go

* fix pr nits

* pr fixes and testing

* type check for caching

* fix imports

* use memdb and remove extra test

* remove unused

* fix imports

* saving message in db and pr fixes

* update copyright

* update backend variable naming

* add comment about saving db vs cache
  • Loading branch information
minghinmatthewlam authored Feb 1, 2023
1 parent 009c86f commit 1955873
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 0 deletions.
87 changes: 87 additions & 0 deletions plugin/evm/warp/backend.go
Original file line number Diff line number Diff line change
@@ -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
}
88 changes: 88 additions & 0 deletions plugin/evm/warp/backend_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 1955873

Please sign in to comment.