2025-11-18 19:59:37 +01:00
|
|
|
package processor
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-18 10:49:47 -04:00
|
|
|
"bytes"
|
2025-12-08 21:31:32 +01:00
|
|
|
"context"
|
2025-11-18 19:59:37 +01:00
|
|
|
"crypto/ecdsa"
|
2026-03-18 10:49:47 -04:00
|
|
|
"errors"
|
2025-11-18 19:59:37 +01:00
|
|
|
"math"
|
2026-03-18 10:49:47 -04:00
|
|
|
"sync"
|
2025-11-18 19:59:37 +01:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/brianvoe/gofakeit/v6"
|
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
|
|
|
bindata "github.com/status-im/migrate/v4/source/go_bindata"
|
|
|
|
|
mvdsnode "github.com/status-im/mvds/node"
|
|
|
|
|
mvdsmigrations "github.com/status-im/mvds/persistenceutil"
|
|
|
|
|
mvdsproto "github.com/status-im/mvds/protobuf"
|
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
2025-12-18 12:24:40 +00:00
|
|
|
"github.com/status-im/status-go/internal/crypto"
|
2026-03-18 10:49:47 -04:00
|
|
|
cryptotypes "github.com/status-im/status-go/internal/crypto/types"
|
2025-12-08 21:31:32 +01:00
|
|
|
"github.com/status-im/status-go/internal/instrumentation/trace"
|
2025-12-17 15:21:08 +00:00
|
|
|
"github.com/status-im/status-go/internal/testutils"
|
2026-04-16 13:22:54 +01:00
|
|
|
common "github.com/status-im/status-go/pkg/messaging/common"
|
2025-12-17 19:40:40 +00:00
|
|
|
commonmigrations "github.com/status-im/status-go/pkg/messaging/common/migrations"
|
2026-04-16 13:22:54 +01:00
|
|
|
encryption "github.com/status-im/status-go/pkg/messaging/layers/encryption"
|
2025-12-17 19:40:40 +00:00
|
|
|
encryptionmigrations "github.com/status-im/status-go/pkg/messaging/layers/encryption/migrations"
|
|
|
|
|
"github.com/status-im/status-go/pkg/messaging/layers/reliability"
|
2026-07-20 15:51:11 -04:00
|
|
|
reliabilitypb "github.com/status-im/status-go/pkg/messaging/layers/reliability/protobuf"
|
2026-04-16 13:22:54 +01:00
|
|
|
segmentation "github.com/status-im/status-go/pkg/messaging/layers/segmentation"
|
2025-12-17 19:40:40 +00:00
|
|
|
segmentationmigrations "github.com/status-im/status-go/pkg/messaging/layers/segmentation/migrations"
|
2026-04-16 13:22:54 +01:00
|
|
|
transport "github.com/status-im/status-go/pkg/messaging/layers/transport"
|
2025-12-17 19:40:40 +00:00
|
|
|
transportmigrations "github.com/status-im/status-go/pkg/messaging/layers/transport/migrations"
|
|
|
|
|
"github.com/status-im/status-go/pkg/messaging/types"
|
2026-08-17 15:04:09 +02:00
|
|
|
"github.com/status-im/status-go/pkg/messaging/waku"
|
2025-11-18 19:59:37 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestProcessorSuite(t *testing.T) {
|
|
|
|
|
suite.Run(t, new(ProcessorSuite))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ProcessorSuite struct {
|
|
|
|
|
suite.Suite
|
|
|
|
|
|
|
|
|
|
processor *Processor
|
|
|
|
|
testPayload []byte
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:49:47 -04:00
|
|
|
func (s *ProcessorSuite) newStandaloneReliability() *reliability.Reliability {
|
|
|
|
|
db, err := testutils.SetupTestMemorySQLDB(testutils.NewTestDBInitializer(nil))
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
s.T().Cleanup(func() {
|
|
|
|
|
s.Require().NoError(db.Close())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
err = mvdsmigrations.Migrate(db)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
identity, err := crypto.GenerateKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
2026-07-20 15:51:11 -04:00
|
|
|
r, err := reliability.NewReliability(
|
2026-03-18 10:49:47 -04:00
|
|
|
mvdsnode.NewSQLitePersistence(db),
|
|
|
|
|
identity,
|
2026-07-20 15:51:11 -04:00
|
|
|
func(string, []string, string) error { return nil },
|
2026-03-18 10:49:47 -04:00
|
|
|
s.logger,
|
|
|
|
|
)
|
2026-07-20 15:51:11 -04:00
|
|
|
s.Require().NoError(err)
|
2026-03-18 10:49:47 -04:00
|
|
|
|
2026-08-06 14:01:49 -04:00
|
|
|
s.T().Cleanup(r.Close)
|
2026-03-18 10:49:47 -04:00
|
|
|
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 19:59:37 +01:00
|
|
|
func (s *ProcessorSuite) SetupTest() {
|
|
|
|
|
s.testPayload = []byte(gofakeit.Word())
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
s.logger, err = zap.NewDevelopment()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
identity, err := crypto.GenerateKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
2025-12-17 15:21:08 +00:00
|
|
|
db, err := testutils.SetupTestMemorySQLDB(testutils.NewTestDBInitializer([]*bindata.AssetSource{
|
2025-11-18 19:59:37 +01:00
|
|
|
{
|
|
|
|
|
Names: transportmigrations.AssetNames(),
|
|
|
|
|
AssetFunc: transportmigrations.Asset,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Names: segmentationmigrations.AssetNames(),
|
|
|
|
|
AssetFunc: segmentationmigrations.Asset,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Names: encryptionmigrations.AssetNames(),
|
|
|
|
|
AssetFunc: encryptionmigrations.Asset,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Names: commonmigrations.AssetNames(),
|
|
|
|
|
AssetFunc: commonmigrations.Asset,
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
err = mvdsmigrations.Migrate(db)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
2026-04-16 13:22:54 +01:00
|
|
|
stack := &common.MessagingStack{}
|
2025-11-18 19:59:37 +01:00
|
|
|
|
2026-08-17 15:04:09 +02:00
|
|
|
wakuConfig := waku.DefaultConfig
|
|
|
|
|
shh, err := waku.New(
|
2025-11-18 19:59:37 +01:00
|
|
|
nil,
|
|
|
|
|
&wakuConfig,
|
|
|
|
|
s.logger,
|
|
|
|
|
nil,
|
|
|
|
|
)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().NoError(shh.Start())
|
|
|
|
|
|
2026-04-16 13:22:54 +01:00
|
|
|
stack.Transport, err = transport.NewTransport(
|
2025-11-18 19:59:37 +01:00
|
|
|
shh,
|
|
|
|
|
identity,
|
2026-04-16 13:22:54 +01:00
|
|
|
transport.NewSQLiteKeysPersistence(db),
|
|
|
|
|
transport.NewSQLiteProcessedMessageIDsCachePersistence(db),
|
|
|
|
|
&transport.EnvelopesMonitorConfig{},
|
2025-11-18 19:59:37 +01:00
|
|
|
s.logger,
|
|
|
|
|
)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
2026-04-16 13:22:54 +01:00
|
|
|
stack.Segmentation = segmentation.NewSegmenter(
|
|
|
|
|
segmentation.NewSQLitePersistence(db),
|
2025-11-18 19:59:37 +01:00
|
|
|
s.logger,
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-16 13:22:54 +01:00
|
|
|
stack.Encryption = encryption.New(
|
|
|
|
|
encryption.NewSQLitePersistence(db),
|
2025-11-18 19:59:37 +01:00
|
|
|
"installation-1",
|
|
|
|
|
s.logger,
|
2025-12-08 21:31:32 +01:00
|
|
|
trace.NewNoopTracer(),
|
2025-11-18 19:59:37 +01:00
|
|
|
)
|
|
|
|
|
|
2026-07-20 15:51:11 -04:00
|
|
|
stack.Reliability, err = reliability.NewReliability(
|
2025-11-18 19:59:37 +01:00
|
|
|
mvdsnode.NewSQLitePersistence(db),
|
|
|
|
|
identity,
|
2026-07-20 15:51:11 -04:00
|
|
|
func(string, []string, string) error { return nil },
|
2025-11-18 19:59:37 +01:00
|
|
|
s.logger,
|
|
|
|
|
)
|
2026-07-20 15:51:11 -04:00
|
|
|
s.Require().NoError(err)
|
2025-11-18 19:59:37 +01:00
|
|
|
|
|
|
|
|
err = stack.Reliability.Start(func(*ecdsa.PublicKey, []byte, [][]byte) error { return nil })
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
2026-08-06 14:01:49 -04:00
|
|
|
s.T().Cleanup(stack.Reliability.Close)
|
|
|
|
|
|
2025-11-18 19:59:37 +01:00
|
|
|
s.processor = NewProcessor(
|
|
|
|
|
identity,
|
|
|
|
|
stack,
|
2026-04-16 13:22:54 +01:00
|
|
|
common.NewSQLiteMessageConfirmationPersistence(db),
|
|
|
|
|
common.NewSQLiteHashRatchetPersistence(db),
|
2025-11-18 19:59:37 +01:00
|
|
|
s.logger,
|
2025-12-08 21:31:32 +01:00
|
|
|
trace.NewNoopTracer(),
|
2025-11-18 19:59:37 +01:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *ProcessorSuite) TestProcessMessage() {
|
|
|
|
|
senderKey, err := crypto.GenerateKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
encodedPayload := s.testPayload
|
|
|
|
|
|
|
|
|
|
message := &types.ReceivedMessage{}
|
|
|
|
|
message.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
message.Payload = s.testPayload
|
|
|
|
|
|
|
|
|
|
response, err := s.processor.ProcessMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
decodedMessages := response.Messages
|
|
|
|
|
|
|
|
|
|
s.Require().Equal(1, len(decodedMessages))
|
|
|
|
|
s.Require().Equal(&senderKey.PublicKey, decodedMessages[0].SigPubKey())
|
|
|
|
|
s.Require().Equal(encodedPayload, decodedMessages[0].EncryptionLayer.Payload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *ProcessorSuite) TestProcessMessageDatasync() {
|
|
|
|
|
senderKey, err := crypto.GenerateKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
dataSyncMessage := mvdsproto.Payload{
|
|
|
|
|
Messages: []*mvdsproto.Message{
|
|
|
|
|
{Body: s.testPayload},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
marshalledDataSyncMessage, err := proto.Marshal(&dataSyncMessage)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
message := &types.ReceivedMessage{}
|
|
|
|
|
message.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
message.Payload = marshalledDataSyncMessage
|
|
|
|
|
|
|
|
|
|
response, err := s.processor.ProcessMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
decodedMessages := response.Messages
|
|
|
|
|
|
|
|
|
|
// We send two messages, the unwrapped one will be attributed to the relayer, while the wrapped one will be attributed to the author
|
|
|
|
|
s.Require().Equal(1, len(decodedMessages))
|
|
|
|
|
s.Require().Equal(&senderKey.PublicKey, decodedMessages[0].SigPubKey())
|
|
|
|
|
s.Require().Equal(s.testPayload, decodedMessages[0].EncryptionLayer.Payload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *ProcessorSuite) TestProcessMessageDatasyncEncrypted() {
|
|
|
|
|
senderKey, err := crypto.GenerateKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
dataSyncMessage := mvdsproto.Payload{
|
|
|
|
|
Messages: []*mvdsproto.Message{
|
|
|
|
|
{Body: s.testPayload},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
marshalledDataSyncMessage, err := proto.Marshal(&dataSyncMessage)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
// Create sender encryption protocol.
|
2025-12-17 15:21:08 +00:00
|
|
|
senderDatabase, err := testutils.SetupTestMemorySQLDB(testutils.NewTestDBInitializer([]*bindata.AssetSource{
|
2025-11-18 19:59:37 +01:00
|
|
|
{
|
|
|
|
|
Names: encryptionmigrations.AssetNames(),
|
|
|
|
|
AssetFunc: encryptionmigrations.Asset,
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
2026-04-16 13:22:54 +01:00
|
|
|
senderEncryptionProtocol := encryption.New(
|
|
|
|
|
encryption.NewSQLitePersistence(senderDatabase),
|
2025-11-18 19:59:37 +01:00
|
|
|
"installation-2",
|
|
|
|
|
s.logger,
|
2025-12-08 21:31:32 +01:00
|
|
|
trace.NewNoopTracer(),
|
2025-11-18 19:59:37 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
messageSpec, err := senderEncryptionProtocol.BuildEncryptedMessage(
|
|
|
|
|
senderKey,
|
|
|
|
|
&s.processor.identity.PublicKey,
|
|
|
|
|
marshalledDataSyncMessage,
|
|
|
|
|
)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
encryptedPayload, err := proto.Marshal(messageSpec.Message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
message := &types.ReceivedMessage{}
|
|
|
|
|
message.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
message.Payload = encryptedPayload
|
|
|
|
|
|
|
|
|
|
response, err := s.processor.ProcessMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
decodedMessages := response.Messages
|
|
|
|
|
|
|
|
|
|
// We send two messages, the unwrapped one will be attributed to the relayer,
|
|
|
|
|
// while the wrapped one will be attributed to the author.
|
|
|
|
|
s.Require().Equal(1, len(decodedMessages))
|
|
|
|
|
s.Require().Equal(&senderKey.PublicKey, decodedMessages[0].SigPubKey())
|
|
|
|
|
s.Require().Equal(s.testPayload, decodedMessages[0].EncryptionLayer.Payload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *ProcessorSuite) TestHandleOutOfOrderHashRatchet() {
|
|
|
|
|
groupID := []byte("group-id")
|
|
|
|
|
senderKey, err := crypto.GenerateKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
// Create sender encryption protocol.
|
2025-12-17 15:21:08 +00:00
|
|
|
senderDatabase, err := testutils.SetupTestMemorySQLDB(testutils.NewTestDBInitializer([]*bindata.AssetSource{
|
2025-11-18 19:59:37 +01:00
|
|
|
{
|
|
|
|
|
Names: encryptionmigrations.AssetNames(),
|
|
|
|
|
AssetFunc: encryptionmigrations.Asset,
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
2026-04-16 13:22:54 +01:00
|
|
|
senderEncryptionProtocol := encryption.New(
|
|
|
|
|
encryption.NewSQLitePersistence(senderDatabase),
|
2025-11-18 19:59:37 +01:00
|
|
|
"installation-2",
|
|
|
|
|
s.logger,
|
2025-12-08 21:31:32 +01:00
|
|
|
trace.NewNoopTracer(),
|
2025-11-18 19:59:37 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ratchet, err := senderEncryptionProtocol.GenerateHashRatchetKey(groupID)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
2026-04-16 13:22:54 +01:00
|
|
|
ratchets := []*encryption.HashRatchetKeyCompatibility{ratchet}
|
2025-11-18 19:59:37 +01:00
|
|
|
|
2025-12-08 21:31:32 +01:00
|
|
|
hashRatchetKeyExchangeMessage, err := senderEncryptionProtocol.BuildHashRatchetKeyExchangeMessage(context.Background(), senderKey, &s.processor.identity.PublicKey, groupID, ratchets)
|
2025-11-18 19:59:37 +01:00
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
encryptedPayload1, err := proto.Marshal(hashRatchetKeyExchangeMessage.Message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
messageSpec2, err := senderEncryptionProtocol.BuildHashRatchetMessage(
|
|
|
|
|
groupID,
|
|
|
|
|
s.testPayload,
|
|
|
|
|
)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
encryptedPayload2, err := proto.Marshal(messageSpec2.Message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
message := &types.ReceivedMessage{}
|
|
|
|
|
message.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
message.Hash = []byte{0x1}
|
|
|
|
|
message.Payload = encryptedPayload2
|
|
|
|
|
|
|
|
|
|
_, err = s.processor.processMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
keyID, err := ratchet.GetKeyID()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
msgs, err := s.processor.hashRatchetStorage.GetMessages(keyID)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
s.Require().Len(msgs, 1)
|
|
|
|
|
|
|
|
|
|
message = &types.ReceivedMessage{}
|
|
|
|
|
message.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
message.Hash = []byte{0x2}
|
|
|
|
|
message.Payload = encryptedPayload1
|
|
|
|
|
|
|
|
|
|
response, err := s.processor.ProcessMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
decodedMessages2 := response.Messages
|
|
|
|
|
s.Require().NotNil(decodedMessages2)
|
|
|
|
|
|
|
|
|
|
// It should have 2 messages, the key exchange and the one from the database
|
|
|
|
|
s.Require().Len(decodedMessages2, 2)
|
|
|
|
|
|
|
|
|
|
// it deletes the messages after being processed
|
|
|
|
|
msgs, err = s.processor.hashRatchetStorage.GetMessages(keyID)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
s.Require().Len(msgs, 0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 15:53:54 +04:00
|
|
|
// A queued hash ratchet message can legitimately fail to complete on replay —
|
|
|
|
|
// here, it is encrypted with a ratchet key that still hasn't arrived, so
|
|
|
|
|
// processing it re-queues it and yields no response. That must not crash the
|
|
|
|
|
// replay and must not lose the message.
|
|
|
|
|
func (s *ProcessorSuite) TestQueuedHashRatchetMessageStillMissingItsKeySurvivesReplay() {
|
|
|
|
|
groupID := []byte("group-id")
|
|
|
|
|
otherGroupID := []byte("group-id-other")
|
|
|
|
|
senderKey, err := crypto.GenerateKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
senderDatabase, err := testutils.SetupTestMemorySQLDB(testutils.NewTestDBInitializer([]*bindata.AssetSource{
|
|
|
|
|
{
|
|
|
|
|
Names: encryptionmigrations.AssetNames(),
|
|
|
|
|
AssetFunc: encryptionmigrations.Asset,
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
senderEncryptionProtocol := encryption.New(
|
|
|
|
|
encryption.NewSQLitePersistence(senderDatabase),
|
|
|
|
|
"installation-2",
|
|
|
|
|
s.logger,
|
|
|
|
|
trace.NewNoopTracer(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ratchet, err := senderEncryptionProtocol.GenerateHashRatchetKey(groupID)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
keyID, err := ratchet.GetKeyID()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
// A second ratchet whose key exchange is never delivered to the receiver.
|
|
|
|
|
otherRatchet, err := senderEncryptionProtocol.GenerateHashRatchetKey(otherGroupID)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
otherKeyID, err := otherRatchet.GetKeyID()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
hashRatchetKeyExchangeMessage, err := senderEncryptionProtocol.BuildHashRatchetKeyExchangeMessage(context.Background(), senderKey, &s.processor.identity.PublicKey, groupID, []*encryption.HashRatchetKeyCompatibility{ratchet})
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
keyExchangePayload, err := proto.Marshal(hashRatchetKeyExchangeMessage.Message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
dataMessageSpec, err := senderEncryptionProtocol.BuildHashRatchetMessage(groupID, s.testPayload)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
dataPayload, err := proto.Marshal(dataMessageSpec.Message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
strayMessageSpec, err := senderEncryptionProtocol.BuildHashRatchetMessage(otherGroupID, s.testPayload)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
strayPayload, err := proto.Marshal(strayMessageSpec.Message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
// The data message arrives before its key and gets queued.
|
|
|
|
|
message := &types.ReceivedMessage{}
|
|
|
|
|
message.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
message.Hash = []byte{0x1}
|
|
|
|
|
message.Payload = dataPayload
|
|
|
|
|
|
|
|
|
|
_, err = s.processor.processMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
// A message for the other, still missing ratchet key sits in the same queue.
|
|
|
|
|
strayMessage := &types.ReceivedMessage{}
|
|
|
|
|
strayMessage.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
strayMessage.Hash = []byte{0x2}
|
|
|
|
|
strayMessage.Payload = strayPayload
|
|
|
|
|
|
|
|
|
|
err = s.processor.hashRatchetStorage.SaveMessage(groupID, keyID, strayMessage)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
// The key arrives: the replay must decode the data message, not crash on the
|
|
|
|
|
// stray one, and re-queue the stray one under its own key.
|
|
|
|
|
message = &types.ReceivedMessage{}
|
|
|
|
|
message.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
message.Hash = []byte{0x3}
|
|
|
|
|
message.Payload = keyExchangePayload
|
|
|
|
|
|
|
|
|
|
response, err := s.processor.ProcessMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().NotNil(response)
|
|
|
|
|
|
|
|
|
|
// The key exchange itself and the queued data message.
|
|
|
|
|
s.Require().Len(response.Messages, 2)
|
|
|
|
|
|
|
|
|
|
// The stray message is not lost: it waits for its own key.
|
|
|
|
|
msgs, err := s.processor.hashRatchetStorage.GetMessages(otherKeyID)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().Len(msgs, 1)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 19:59:37 +01:00
|
|
|
func (s *ProcessorSuite) TestHandleSegmentMessages() {
|
|
|
|
|
senderKey, err := crypto.GenerateKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
segmentedMessages, err := s.processor.stack.Segmentation.Segment(s.testPayload, int(math.Ceil(float64(len(s.testPayload))/2)))
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().Len(segmentedMessages, 2)
|
|
|
|
|
|
|
|
|
|
message := &types.ReceivedMessage{}
|
|
|
|
|
message.Sig = crypto.FromECDSAPub(&senderKey.PublicKey)
|
|
|
|
|
message.Payload = segmentedMessages[0]
|
|
|
|
|
|
|
|
|
|
// First segment is received, no messages are decoded
|
|
|
|
|
response, err := s.processor.ProcessMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().Nil(response)
|
|
|
|
|
|
|
|
|
|
// Second (and final) segment is received, reassembled message is decoded
|
|
|
|
|
message.Payload = segmentedMessages[1]
|
|
|
|
|
response, err = s.processor.ProcessMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
decodedMessages := response.Messages
|
|
|
|
|
s.Require().Len(decodedMessages, 1)
|
|
|
|
|
s.Require().Equal(&senderKey.PublicKey, decodedMessages[0].SigPubKey())
|
|
|
|
|
s.Require().Equal(s.testPayload, decodedMessages[0].EncryptionLayer.Payload)
|
|
|
|
|
|
2026-03-18 10:49:47 -04:00
|
|
|
// Receiving another segment after reassembly should be ignored as a duplicate.
|
|
|
|
|
response, err = s.processor.ProcessMessage(message)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().Nil(response)
|
2025-11-18 19:59:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *ProcessorSuite) TestGetEphemeralKey() {
|
|
|
|
|
keyMap := make(map[string]bool)
|
|
|
|
|
for i := 0; i < maxNumOfEphemeralKeys; i++ {
|
|
|
|
|
key, err := s.processor.GetEphemeralKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().NotNil(key)
|
|
|
|
|
keyMap[crypto.PubkeyToHex(&key.PublicKey)] = true
|
|
|
|
|
}
|
|
|
|
|
s.Require().Len(keyMap, maxNumOfEphemeralKeys)
|
|
|
|
|
// Add one more
|
|
|
|
|
key, err := s.processor.GetEphemeralKey()
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().NotNil(key)
|
|
|
|
|
|
|
|
|
|
s.Require().True(keyMap[crypto.PubkeyToHex(&key.PublicKey)])
|
|
|
|
|
}
|
2025-12-22 19:57:36 +00:00
|
|
|
|
|
|
|
|
func (s *ProcessorSuite) TestSDSWrappedMessages() {
|
|
|
|
|
payload := []byte("hello")
|
2026-07-20 15:51:11 -04:00
|
|
|
sdsChannelID := "community123channel-1"
|
2025-12-22 19:57:36 +00:00
|
|
|
|
2026-07-20 15:51:11 -04:00
|
|
|
wrappedPayload, _, err := s.processor.stack.Reliability.WrapPayloadForSDS(payload, sdsChannelID)
|
2025-12-22 19:57:36 +00:00
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().True(len(wrappedPayload) > 0)
|
|
|
|
|
|
|
|
|
|
receivedMsg := types.Message{
|
|
|
|
|
EncryptionLayer: types.EncryptionLayer{
|
|
|
|
|
Payload: wrappedPayload,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = s.processor.processSDSLayer(&receivedMsg)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().Equal(payload, receivedMsg.EncryptionLayer.Payload)
|
|
|
|
|
|
|
|
|
|
anotherPayload := []byte("another-message")
|
|
|
|
|
receivedMsg2 := types.Message{
|
|
|
|
|
EncryptionLayer: types.EncryptionLayer{
|
|
|
|
|
Payload: anotherPayload,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
err = s.processor.processSDSLayer(&receivedMsg2)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
s.Require().Equal(anotherPayload, receivedMsg2.EncryptionLayer.Payload)
|
|
|
|
|
}
|
2026-03-18 10:49:47 -04:00
|
|
|
|
|
|
|
|
func (s *ProcessorSuite) TestSDSMissingDependencyTriggersFetchHintsAndRecoversPayloads() {
|
|
|
|
|
senderReliability := s.newStandaloneReliability()
|
|
|
|
|
receiverReliability := s.newStandaloneReliability()
|
|
|
|
|
|
2026-07-20 15:51:11 -04:00
|
|
|
channelID := "community123general"
|
2026-03-18 10:49:47 -04:00
|
|
|
oldPayload := []byte("old-community-message")
|
|
|
|
|
newPayload := []byte("new-community-message")
|
|
|
|
|
|
|
|
|
|
expectedOldMessageID := cryptotypes.EncodeHex(crypto.Keccak256(oldPayload))
|
|
|
|
|
expectedNewMessageID := cryptotypes.EncodeHex(crypto.Keccak256(newPayload))
|
|
|
|
|
expectedOldHint := "store-hash-" + expectedOldMessageID
|
|
|
|
|
|
|
|
|
|
senderReliability.SetRetrievalHintProvider(func(messageID string) []byte {
|
2026-07-20 15:51:11 -04:00
|
|
|
hint, err := proto.Marshal(&reliabilitypb.RetrievalHint{
|
|
|
|
|
EnvelopeHashes: [][]byte{[]byte("store-hash-" + messageID)},
|
|
|
|
|
})
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
return hint
|
2026-03-18 10:49:47 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
type missingDepsCall struct {
|
|
|
|
|
messageID string
|
|
|
|
|
deps []string
|
|
|
|
|
channelID string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
callsMu sync.Mutex
|
|
|
|
|
calls []missingDepsCall
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
receiverReliability.SetMissingDependenciesHandler(func(messageID string, missingDeps []string, missingDepsChannelID string) error {
|
|
|
|
|
callsMu.Lock()
|
|
|
|
|
calls = append(calls, missingDepsCall{
|
|
|
|
|
messageID: messageID,
|
|
|
|
|
deps: append([]string(nil), missingDeps...),
|
|
|
|
|
channelID: missingDepsChannelID,
|
|
|
|
|
})
|
|
|
|
|
callsMu.Unlock()
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-20 15:51:11 -04:00
|
|
|
wrappedOldPayload, _, err := senderReliability.WrapPayloadForSDS(oldPayload, channelID)
|
2026-03-18 10:49:47 -04:00
|
|
|
s.Require().NoError(err)
|
2026-07-20 15:51:11 -04:00
|
|
|
wrappedNewPayload, _, err := senderReliability.WrapPayloadForSDS(newPayload, channelID)
|
2026-03-18 10:49:47 -04:00
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
originalReliability := s.processor.stack.Reliability
|
|
|
|
|
s.processor.stack.Reliability = receiverReliability
|
|
|
|
|
defer func() {
|
|
|
|
|
s.processor.stack.Reliability = originalReliability
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
newMessageFirst := types.Message{
|
|
|
|
|
EncryptionLayer: types.EncryptionLayer{Payload: wrappedNewPayload},
|
|
|
|
|
}
|
|
|
|
|
err = s.processor.processSDSLayer(&newMessageFirst)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
err = testutils.RetryWithBackOff(func() error {
|
|
|
|
|
callsMu.Lock()
|
|
|
|
|
defer callsMu.Unlock()
|
|
|
|
|
|
|
|
|
|
if len(calls) == 0 {
|
|
|
|
|
return errors.New("missing dependencies callback not triggered")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
|
|
callsMu.Lock()
|
|
|
|
|
firstCall := calls[0]
|
|
|
|
|
callsMu.Unlock()
|
|
|
|
|
|
|
|
|
|
s.Require().Equal(expectedNewMessageID, firstCall.messageID)
|
|
|
|
|
s.Require().Equal(channelID, firstCall.channelID)
|
|
|
|
|
s.Require().Contains(firstCall.deps, expectedOldHint)
|
|
|
|
|
|
|
|
|
|
recovered := map[string]bool{}
|
|
|
|
|
if bytes.Equal(newMessageFirst.EncryptionLayer.Payload, newPayload) {
|
|
|
|
|
recovered["new"] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oldMessage := types.Message{
|
|
|
|
|
EncryptionLayer: types.EncryptionLayer{Payload: wrappedOldPayload},
|
|
|
|
|
}
|
|
|
|
|
err = s.processor.processSDSLayer(&oldMessage)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
if bytes.Equal(oldMessage.EncryptionLayer.Payload, oldPayload) {
|
|
|
|
|
recovered["old"] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newMessageReplay := types.Message{
|
|
|
|
|
EncryptionLayer: types.EncryptionLayer{Payload: wrappedNewPayload},
|
|
|
|
|
}
|
|
|
|
|
err = s.processor.processSDSLayer(&newMessageReplay)
|
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
if bytes.Equal(newMessageReplay.EncryptionLayer.Payload, newPayload) {
|
|
|
|
|
recovered["new"] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.Require().True(recovered["old"], "old payload should be recoverable after dependency handling")
|
|
|
|
|
s.Require().True(recovered["new"], "new payload should be recoverable after dependency handling")
|
|
|
|
|
}
|