diff --git a/plugin/evm/vm_warp_test.go b/plugin/evm/vm_warp_test.go index eb235379ee..abaf68f4fc 100644 --- a/plugin/evm/vm_warp_test.go +++ b/plugin/evm/vm_warp_test.go @@ -126,10 +126,9 @@ func TestSendWarpMessage(t *testing.T) { logData := receipts[0].Logs[0].Data unsignedMessage, err := warp.UnpackSendWarpEventDataToMessage(logData) require.NoError(err) - unsignedMessageID := unsignedMessage.ID() // Verify the signature cannot be fetched before the block is accepted - _, err = vm.warpBackend.GetMessageSignature(unsignedMessageID) + _, err = vm.warpBackend.GetMessageSignature(unsignedMessage) require.Error(err) _, err = vm.warpBackend.GetBlockSignature(blk.ID()) require.Error(err) @@ -139,7 +138,7 @@ func TestSendWarpMessage(t *testing.T) { vm.blockChain.DrainAcceptorQueue() // Verify the message signature after accepting the block. - rawSignatureBytes, err := vm.warpBackend.GetMessageSignature(unsignedMessageID) + rawSignatureBytes, err := vm.warpBackend.GetMessageSignature(unsignedMessage) require.NoError(err) blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes[:]) require.NoError(err) @@ -750,7 +749,7 @@ func TestMessageSignatureRequestsToVM(t *testing.T) { // Add the known message and get its signature to confirm. err = vm.warpBackend.AddMessage(warpMessage) require.NoError(t, err) - signature, err := vm.warpBackend.GetMessageSignature(warpMessage.ID()) + signature, err := vm.warpBackend.GetMessageSignature(warpMessage) require.NoError(t, err) tests := map[string]struct { diff --git a/warp/backend.go b/warp/backend.go index 7e7377ad57..360161a336 100644 --- a/warp/backend.go +++ b/warp/backend.go @@ -15,6 +15,7 @@ import ( "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/warp/messages" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" ) @@ -36,8 +37,8 @@ type Backend interface { // AddMessage signs [unsignedMessage] and adds it to the warp backend database AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) error - // GetMessageSignature returns the signature of the requested message hash. - GetMessageSignature(messageID ids.ID) ([bls.SignatureLen]byte, error) + // GetMessageSignature returns the signature of the requested message. + GetMessageSignature(message *avalancheWarp.UnsignedMessage) ([bls.SignatureLen]byte, error) // GetBlockSignature returns the signature of the requested message hash. GetBlockSignature(blockID ids.ID) ([bls.SignatureLen]byte, error) @@ -142,15 +143,16 @@ func (b *backend) AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) err return nil } -func (b *backend) GetMessageSignature(messageID ids.ID) ([bls.SignatureLen]byte, error) { +func (b *backend) GetMessageSignature(unsignedMessage *avalancheWarp.UnsignedMessage) ([bls.SignatureLen]byte, error) { + messageID := unsignedMessage.ID() + log.Debug("Getting warp message from backend", "messageID", messageID) if sig, ok := b.messageSignatureCache.Get(messageID); ok { return sig, nil } - unsignedMessage, err := b.GetMessage(messageID) - if err != nil { - return [bls.SignatureLen]byte{}, fmt.Errorf("failed to get warp message %s from db: %w", messageID.String(), err) + if err := b.ValidateMessage(unsignedMessage); err != nil { + return [bls.SignatureLen]byte{}, fmt.Errorf("failed to validate warp message: %w", err) } var signature [bls.SignatureLen]byte @@ -164,6 +166,37 @@ func (b *backend) GetMessageSignature(messageID ids.ID) ([bls.SignatureLen]byte, return signature, nil } +func (b *backend) ValidateMessage(unsignedMessage *avalancheWarp.UnsignedMessage) error { + // Known on-chain messages should be signed + if _, err := b.GetMessage(unsignedMessage.ID()); err == nil { + return nil + } + + // Try to parse the payload as an AddressedCall + addressedCall, err := payload.ParseAddressedCall(unsignedMessage.Payload) + if err != nil { + return fmt.Errorf("failed to parse unknown message as AddressedCall: %w", err) + } + + // Further, parse the payload to see if it is a known type. + parsed, err := messages.Parse(addressedCall.Payload) + if err != nil { + return fmt.Errorf("failed to parse unknown message: %w", err) + } + + // Check if the message is a known type that can be signed on demand + signable, ok := parsed.(messages.Signable) + if !ok { + return fmt.Errorf("parsed message is not Signable: %T", signable) + } + + // Check if the message should be signed according to its type + if err := signable.VerifyMesssage(addressedCall.SourceAddress); err != nil { + return fmt.Errorf("failed to verify Signable message: %w", err) + } + return nil +} + func (b *backend) GetBlockSignature(blockID ids.ID) ([bls.SignatureLen]byte, error) { log.Debug("Getting block from backend", "blockID", blockID) if sig, ok := b.blockSignatureCache.Get(blockID); ok { diff --git a/warp/backend_test.go b/warp/backend_test.go index a262d760ef..21013dfc24 100644 --- a/warp/backend_test.go +++ b/warp/backend_test.go @@ -10,7 +10,6 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/hashing" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/warp/warptest" @@ -49,18 +48,17 @@ func TestClearDB(t *testing.T) { // use multiple messages to test that all messages get cleared payloads := [][]byte{[]byte("test1"), []byte("test2"), []byte("test3"), []byte("test4"), []byte("test5")} - messageIDs := []ids.ID{} + messages := make([]*avalancheWarp.UnsignedMessage, 0, len(payloads)) // add all messages for _, payload := range payloads { unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, payload) require.NoError(t, err) - messageID := hashing.ComputeHash256Array(unsignedMsg.Bytes()) - messageIDs = append(messageIDs, messageID) + messages = append(messages, unsignedMsg) err = backend.AddMessage(unsignedMsg) require.NoError(t, err) // ensure that the message was added - _, err = backend.GetMessageSignature(messageID) + _, err = backend.GetMessageSignature(unsignedMsg) require.NoError(t, err) } @@ -74,9 +72,9 @@ func TestClearDB(t *testing.T) { require.False(t, it.Next()) // ensure all messages have been deleted - for _, messageID := range messageIDs { - _, err := backend.GetMessageSignature(messageID) - require.ErrorContains(t, err, "failed to get warp message") + for _, message := range messages { + _, err := backend.GetMessageSignature(message) + require.ErrorContains(t, err, "failed to validate warp message") } } @@ -94,8 +92,7 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, err) // Verify that a signature is returned successfully, and compare to expected signature. - messageID := testUnsignedMessage.ID() - signature, err := backend.GetMessageSignature(messageID) + signature, err := backend.GetMessageSignature(testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -113,8 +110,7 @@ func TestAddAndGetUnknownMessage(t *testing.T) { require.NoError(t, err) // Try getting a signature for a message that was not added. - messageID := testUnsignedMessage.ID() - _, err = backend.GetMessageSignature(messageID) + _, err = backend.GetMessageSignature(testUnsignedMessage) require.Error(t, err) } @@ -162,8 +158,7 @@ func TestZeroSizedCache(t *testing.T) { require.NoError(t, err) // Verify that a signature is returned successfully, and compare to expected signature. - messageID := testUnsignedMessage.ID() - signature, err := backend.GetMessageSignature(messageID) + signature, err := backend.GetMessageSignature(testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -192,7 +187,7 @@ func TestOffChainMessages(t *testing.T) { require.NoError(err) require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - signature, err := b.GetMessageSignature(testUnsignedMessage.ID()) + signature, err := b.GetMessageSignature(testUnsignedMessage) require.NoError(err) expectedSignatureBytes, err := warpSigner.Sign(msg) require.NoError(err) diff --git a/warp/handlers/signature_request.go b/warp/handlers/signature_request.go index cab7914243..3a28cd994e 100644 --- a/warp/handlers/signature_request.go +++ b/warp/handlers/signature_request.go @@ -45,13 +45,20 @@ func (s *SignatureRequestHandler) OnMessageSignatureRequest(ctx context.Context, s.stats.UpdateMessageSignatureRequestTime(time.Since(startTime)) }() - signature, err := s.backend.GetMessageSignature(signatureRequest.MessageID) + var signature [bls.SignatureLen]byte + unsignedMessage, err := s.backend.GetMessage(signatureRequest.MessageID) if err != nil { - log.Debug("Unknown warp signature requested", "messageID", signatureRequest.MessageID) + log.Debug("Unknown warp message requested", "messageID", signatureRequest.MessageID) s.stats.IncMessageSignatureMiss() - signature = [bls.SignatureLen]byte{} } else { - s.stats.IncMessageSignatureHit() + signature, err = s.backend.GetMessageSignature(unsignedMessage) + if err != nil { + log.Debug("Unknown warp signature requested", "messageID", signatureRequest.MessageID) + s.stats.IncMessageSignatureMiss() + signature = [bls.SignatureLen]byte{} + } else { + s.stats.IncMessageSignatureHit() + } } response := message.SignatureResponse{Signature: signature} diff --git a/warp/handlers/signature_request_p2p.go b/warp/handlers/signature_request_p2p.go index 0728f8b808..cb711974b2 100644 --- a/warp/handlers/signature_request_p2p.go +++ b/warp/handlers/signature_request_p2p.go @@ -28,6 +28,10 @@ const ( ErrFailedToMarshal ) +type AddressedCallHandler interface { + GetMessageSignature(*avalancheWarp.UnsignedMessage) ([bls.SignatureLen]byte, error) +} + // SignatureRequestHandlerP2P serves warp signature requests using the p2p // framework from avalanchego. It is a peer.RequestHandler for // message.MessageSignatureRequest. @@ -79,11 +83,7 @@ func (s *SignatureRequestHandlerP2P) AppRequest( var sig [bls.SignatureLen]byte switch p := parsed.(type) { case *payload.AddressedCall: - // Note we pass the unsigned message ID to GetMessageSignature since - // that is what the backend expects. - // However, we verify the types and format of the payload to ensure - // the message conforms to the ACP-118 spec. - sig, err = s.GetMessageSignature(unsignedMessage.ID()) + sig, err = s.GetMessageSignature(unsignedMessage) if err != nil { s.stats.IncMessageSignatureMiss() } else { @@ -122,7 +122,7 @@ func (s *SignatureRequestHandlerP2P) AppRequest( return respBytes, nil } -func (s *SignatureRequestHandlerP2P) GetMessageSignature(messageID ids.ID) ([bls.SignatureLen]byte, error) { +func (s *SignatureRequestHandlerP2P) GetMessageSignature(message *avalancheWarp.UnsignedMessage) ([bls.SignatureLen]byte, error) { startTime := time.Now() s.stats.IncMessageSignatureRequest() @@ -131,7 +131,7 @@ func (s *SignatureRequestHandlerP2P) GetMessageSignature(messageID ids.ID) ([bls s.stats.UpdateMessageSignatureRequestTime(time.Since(startTime)) }() - return s.backend.GetMessageSignature(messageID) + return s.backend.GetMessageSignature(message) } func (s *SignatureRequestHandlerP2P) GetBlockSignature(blockID ids.ID) ([bls.SignatureLen]byte, error) { diff --git a/warp/handlers/signature_request_p2p_test.go b/warp/handlers/signature_request_p2p_test.go index 1f8f9530cb..3104fe59b3 100644 --- a/warp/handlers/signature_request_p2p_test.go +++ b/warp/handlers/signature_request_p2p_test.go @@ -42,11 +42,10 @@ func TestMessageSignatureHandlerP2P(t *testing.T) { require.NoError(t, err) msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offchainPayload.Bytes()) require.NoError(t, err) - messageID := msg.ID() require.NoError(t, backend.AddMessage(msg)) - signature, err := backend.GetMessageSignature(messageID) + signature, err := backend.GetMessageSignature(msg) require.NoError(t, err) - offchainSignature, err := backend.GetMessageSignature(offchainMessage.ID()) + offchainSignature, err := backend.GetMessageSignature(offchainMessage) require.NoError(t, err) unknownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("unknown message")) diff --git a/warp/handlers/signature_request_test.go b/warp/handlers/signature_request_test.go index 172f182c96..1f699324cc 100644 --- a/warp/handlers/signature_request_test.go +++ b/warp/handlers/signature_request_test.go @@ -38,9 +38,9 @@ func TestMessageSignatureHandler(t *testing.T) { require.NoError(t, err) messageID := msg.ID() require.NoError(t, backend.AddMessage(msg)) - signature, err := backend.GetMessageSignature(messageID) + signature, err := backend.GetMessageSignature(msg) require.NoError(t, err) - offchainSignature, err := backend.GetMessageSignature(offchainMessage.ID()) + offchainSignature, err := backend.GetMessageSignature(offchainMessage) require.NoError(t, err) unknownMessageID := ids.GenerateTestID() diff --git a/warp/messages/codec.go b/warp/messages/codec.go new file mode 100644 index 0000000000..87d2fa334a --- /dev/null +++ b/warp/messages/codec.go @@ -0,0 +1,33 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package messages + +import ( + "errors" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/utils/units" +) + +const ( + CodecVersion = 0 + + MaxMessageSize = 24 * units.KiB +) + +var Codec codec.Manager + +func init() { + Codec = codec.NewManager(MaxMessageSize) + lc := linearcodec.NewDefault() + + err := errors.Join( + lc.RegisterType(&ValidatorUptime{}), + Codec.RegisterCodec(CodecVersion, lc), + ) + if err != nil { + panic(err) + } +} diff --git a/warp/messages/payload.go b/warp/messages/payload.go new file mode 100644 index 0000000000..3776a1356d --- /dev/null +++ b/warp/messages/payload.go @@ -0,0 +1,45 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package messages + +import ( + "errors" + "fmt" +) + +var errWrongType = errors.New("wrong payload type") + +// Payload provides a common interface for all payloads implemented by this +// package. +type Payload interface { + // Bytes returns the binary representation of this payload. + Bytes() []byte + + // initialize the payload with the provided binary representation. + initialize(b []byte) +} + +// 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 { + return nil, err + } + payload.initialize(bytes) + return payload, nil +} + +func initialize(p Payload) error { + bytes, err := Codec.Marshal(CodecVersion, &p) + if err != nil { + return fmt.Errorf("couldn't marshal %T payload: %w", p, err) + } + p.initialize(bytes) + return nil +} diff --git a/warp/messages/validator_uptime.go b/warp/messages/validator_uptime.go new file mode 100644 index 0000000000..3d3e4dd5dd --- /dev/null +++ b/warp/messages/validator_uptime.go @@ -0,0 +1,51 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package messages + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" +) + +// ValidatorUptime is signed when the ValidationID is known and the validator +// has been up for TotalUptime seconds. +type ValidatorUptime struct { + ValidationID ids.ID `serialize:"true"` + TotalUptime uint64 `serialize:"true"` + + bytes []byte +} + +// NewValidatorUptime creates a new *ValidatorUptime and initializes it. +func NewValidatorUptime(validationID ids.ID, totalUptime uint64) (*ValidatorUptime, error) { + bhp := &ValidatorUptime{ + ValidationID: validationID, + TotalUptime: totalUptime, + } + return bhp, initialize(bhp) +} + +// ParseValidatorUptime converts a slice of bytes into an initialized ValidatorUptime. +func ParseValidatorUptime(b []byte) (*ValidatorUptime, error) { + payloadIntf, err := Parse(b) + if err != nil { + return nil, err + } + payload, ok := payloadIntf.(*ValidatorUptime) + if !ok { + return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf) + } + return payload, nil +} + +// Bytes returns the binary representation of this payload. It assumes that the +// payload is initialized from either NewValidatorUptime or Parse. +func (b *ValidatorUptime) Bytes() []byte { + return b.bytes +} + +func (b *ValidatorUptime) initialize(bytes []byte) { + b.bytes = bytes +} diff --git a/warp/service.go b/warp/service.go index 4afe93a168..2472c63b09 100644 --- a/warp/service.go +++ b/warp/service.go @@ -54,7 +54,11 @@ func (a *API) GetMessage(ctx context.Context, messageID ids.ID) (hexutil.Bytes, // GetMessageSignature returns the BLS signature associated with a messageID. func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - signature, err := a.backend.GetMessageSignature(messageID) + unsignedMessage, err := a.backend.GetMessage(messageID) + if err != nil { + return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) + } + signature, err := a.backend.GetMessageSignature(unsignedMessage) if err != nil { return nil, fmt.Errorf("failed to get signature for message %s with error %w", messageID, err) }