Skip to content

Commit 618dde2

Browse files
committed
multi: make message re-send idempotent
To allow a sender to re-try sending a message easily, we return the same response for a message that uses the same proof and receiver key as if we saw that message for the first time. We can't actually check the content of the message, since it's encrypted, and might differ each time it is re-encrypted, even if the cleartext remains the same. Since with the address v2 scheme each outpoint can only have a single recipient, we can make the assumption that a message that claims the same outpoint and is for the same recipient also is the same message.
1 parent 20fa381 commit 618dde2

File tree

13 files changed

+310
-228
lines changed

13 files changed

+310
-228
lines changed

authmailbox/client_test.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ type serverHarness struct {
3333
clientCfg *ClientConfig
3434
mockSigner *test.MockSigner
3535
mockMsgStore *MockMsgStore
36-
mockTxStore proof.TxProofStore
3736
srv *Server
3837
grpcServer *grpc.Server
3938
cleanup func()
@@ -45,7 +44,6 @@ func newServerHarness(t *testing.T) *serverHarness {
4544
signer.Signature = test.RandBytes(64)
4645

4746
inMemMsgStore := NewMockStore()
48-
inMemTxStore := proof.NewMockTxProofStore()
4947

5048
nextPort := port.NextAvailablePort()
5149
listenAddr := fmt.Sprintf(test.ListenAddrTemplate, nextPort)
@@ -56,7 +54,6 @@ func newServerHarness(t *testing.T) *serverHarness {
5654
HeaderVerifier: proof.MockHeaderVerifier,
5755
MerkleVerifier: proof.DefaultMerkleVerifier,
5856
MsgStore: inMemMsgStore,
59-
TxProofStore: inMemTxStore,
6057
}
6158
h := &serverHarness{
6259
listenAddr: listenAddr,
@@ -70,7 +67,6 @@ func newServerHarness(t *testing.T) *serverHarness {
7067
},
7168
mockSigner: signer,
7269
mockMsgStore: inMemMsgStore,
73-
mockTxStore: inMemTxStore,
7470
}
7571
h.start(t)
7672

@@ -217,6 +213,15 @@ func (h *clientHarness) stop(t *testing.T) {
217213
require.NoError(t, err)
218214
}
219215

216+
func randProof(t *testing.T) proof.TxProof {
217+
t.Helper()
218+
219+
randOp := test.RandOp(t)
220+
return proof.TxProof{
221+
ClaimedOutPoint: randOp,
222+
}
223+
}
224+
220225
// TestServerClientAuthAndRestart tests the server and client authentication
221226
// process, and that the client can re-connect to the server after it has
222227
// restarted. It also tests that the client can receive messages from the
@@ -226,7 +231,6 @@ func TestServerClientAuthAndRestart(t *testing.T) {
226231
harness := newServerHarness(t)
227232
clientCfg := harness.clientCfg
228233

229-
randOp := test.RandOp(t)
230234
clientKey1, _ := test.RandKeyDesc(t)
231235
clientKey2, _ := test.RandKeyDesc(t)
232236
filter := MessageFilter{}
@@ -279,7 +283,7 @@ func TestServerClientAuthAndRestart(t *testing.T) {
279283
}
280284

281285
// We also store the message in the store, so we can retrieve it later.
282-
_, err = harness.mockMsgStore.StoreMessage(ctx, randOp, msg1)
286+
_, err = harness.mockMsgStore.StoreMessage(ctx, randProof(t), msg1)
283287
require.NoError(t, err)
284288

285289
harness.srv.publishMessage(msg1)
@@ -310,7 +314,7 @@ func TestServerClientAuthAndRestart(t *testing.T) {
310314
ReceiverKey: *clientKey1.PubKey,
311315
ArrivalTimestamp: time.Now(),
312316
}
313-
_, err = harness.mockMsgStore.StoreMessage(ctx, randOp, msg2)
317+
_, err = harness.mockMsgStore.StoreMessage(ctx, randProof(t), msg2)
314318
require.NoError(t, err)
315319

316320
harness.srv.publishMessage(msg2)
@@ -379,6 +383,7 @@ func TestSendMessage(t *testing.T) {
379383

380384
clientKey1, _ := test.RandKeyDesc(t)
381385
clientKey2, _ := test.RandKeyDesc(t)
386+
clientKey3, _ := test.RandKeyDesc(t)
382387

383388
proofWithHeight := func(p proof.TxProof, h uint32) proof.TxProof {
384389
p.BlockHeight = h
@@ -391,7 +396,7 @@ func TestSendMessage(t *testing.T) {
391396
testCases := []struct {
392397
name string
393398
txProofs []proof.TxProof
394-
recvKey keychain.KeyDescriptor
399+
recvKeys []keychain.KeyDescriptor
395400
sendKey keychain.KeyDescriptor
396401
msgs [][]byte
397402
expiryHeight uint32
@@ -400,15 +405,15 @@ func TestSendMessage(t *testing.T) {
400405
{
401406
name: "empty payload",
402407
txProofs: []proof.TxProof{*txProof1},
403-
recvKey: clientKey2,
408+
recvKeys: []keychain.KeyDescriptor{clientKey2},
404409
sendKey: clientKey1,
405410
msgs: [][]byte{nil},
406411
expectedErrs: []string{"empty payload"},
407412
},
408413
{
409414
name: "long payload",
410415
txProofs: []proof.TxProof{*txProof1},
411-
recvKey: clientKey2,
416+
recvKeys: []keychain.KeyDescriptor{clientKey2},
412417
sendKey: clientKey1,
413418
msgs: [][]byte{
414419
bytes.Repeat([]byte("foo"), MsgMaxSize),
@@ -418,7 +423,7 @@ func TestSendMessage(t *testing.T) {
418423
{
419424
name: "missing expiry height",
420425
txProofs: []proof.TxProof{*txProof1},
421-
recvKey: clientKey2,
426+
recvKeys: []keychain.KeyDescriptor{clientKey2},
422427
sendKey: clientKey1,
423428
msgs: [][]byte{[]byte("yoooo")},
424429
expectedErrs: []string{"missing expiry block height"},
@@ -428,7 +433,7 @@ func TestSendMessage(t *testing.T) {
428433
txProofs: []proof.TxProof{
429434
proofWithHeight(*txProof1, 100002),
430435
},
431-
recvKey: clientKey2,
436+
recvKeys: []keychain.KeyDescriptor{clientKey2},
432437
sendKey: clientKey1,
433438
msgs: [][]byte{[]byte("yoooo")},
434439
expiryHeight: 123,
@@ -442,7 +447,7 @@ func TestSendMessage(t *testing.T) {
442447
txProofs: []proof.TxProof{
443448
proofWithHeight(*txProof1, 100002),
444449
},
445-
recvKey: clientKey2,
450+
recvKeys: []keychain.KeyDescriptor{clientKey2},
446451
sendKey: clientKey1,
447452
msgs: [][]byte{[]byte("yoooo")},
448453
expiryHeight: 100002 + 123,
@@ -453,7 +458,10 @@ func TestSendMessage(t *testing.T) {
453458
proofWithHeight(*txProof1, 100002),
454459
proofWithHeight(*txProof1, 100002),
455460
},
456-
recvKey: clientKey2,
461+
recvKeys: []keychain.KeyDescriptor{
462+
clientKey2,
463+
clientKey3,
464+
},
457465
sendKey: clientKey1,
458466
msgs: [][]byte{
459467
[]byte("yoooo"),
@@ -472,7 +480,11 @@ func TestSendMessage(t *testing.T) {
472480
proofWithHeight(*txProof2, 100002),
473481
proofWithHeight(*txProof3, 100002),
474482
},
475-
recvKey: clientKey2,
483+
recvKeys: []keychain.KeyDescriptor{
484+
clientKey2,
485+
clientKey2,
486+
clientKey2,
487+
},
476488
sendKey: clientKey1,
477489
msgs: [][]byte{
478490
[]byte("yoooo"),
@@ -507,9 +519,10 @@ func TestSendMessage(t *testing.T) {
507519
for idx := range tc.msgs {
508520
msg := tc.msgs[idx]
509521
txProof := tc.txProofs[idx]
522+
recvKey := tc.recvKeys[idx]
510523

511524
msgID, err := client1.client.SendMessage(
512-
ctx, *tc.recvKey.PubKey, msg, txProof,
525+
ctx, *recvKey.PubKey, msg, txProof,
513526
tc.expiryHeight,
514527
)
515528

@@ -528,6 +541,18 @@ func TestSendMessage(t *testing.T) {
528541
// We should be able to read the message if
529542
// there was no error sending it.
530543
client2.readMessages(t, msgID)
544+
545+
// Sending the same message again should result
546+
// in the same message ID, but should not cause
547+
// another message to be sent to any recipients.
548+
msgIDReSend, err := client1.client.SendMessage(
549+
ctx, *recvKey.PubKey, msg, txProof,
550+
tc.expiryHeight,
551+
)
552+
require.NoError(t, err)
553+
554+
require.Equal(t, msgID, msgIDReSend)
555+
client2.expectNoMessage(t)
531556
}
532557
})
533558
}
@@ -569,7 +594,6 @@ func TestReceiveBacklog(t *testing.T) {
569594
harness := newServerHarness(t)
570595

571596
ctx := context.Background()
572-
randOp := test.RandOp(t)
573597
receiver1, _ := test.RandKeyDesc(t)
574598
receiver2, _ := test.RandKeyDesc(t)
575599
receiver3, _ := test.RandKeyDesc(t)
@@ -611,7 +635,9 @@ func TestReceiveBacklog(t *testing.T) {
611635
}
612636

613637
for _, msg := range messages {
614-
_, err := harness.mockMsgStore.StoreMessage(ctx, randOp, msg)
638+
_, err := harness.mockMsgStore.StoreMessage(
639+
ctx, randProof(t), msg,
640+
)
615641
require.NoError(t, err)
616642
}
617643

authmailbox/message.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/btcsuite/btcd/btcec/v2"
99
"github.com/btcsuite/btcd/wire"
10+
"github.com/lightninglabs/taproot-assets/proof"
1011
)
1112

1213
const (
@@ -19,6 +20,10 @@ var (
1920
// allowed length.
2021
ErrMessageTooLong = fmt.Errorf("message too long, max %d bytes",
2122
MsgMaxSize)
23+
24+
// ErrMessageNotFound is returned when a message with the given ID or
25+
// outpoint cannot be found in the mailbox.
26+
ErrMessageNotFound = fmt.Errorf("message not found")
2227
)
2328

2429
// Message represents a message in the mailbox.
@@ -90,12 +95,19 @@ type MsgStore interface {
9095
// outpoint of the transaction that was used to prove the message's
9196
// authenticity. If a message with the same outpoint already exists,
9297
// it returns proof.ErrTxMerkleProofExists.
93-
StoreMessage(ctx context.Context, claimedOp wire.OutPoint,
98+
StoreMessage(ctx context.Context, proof proof.TxProof,
9499
msg *Message) (uint64, error)
95100

96101
// FetchMessage retrieves a message from the mailbox by its ID.
97102
FetchMessage(ctx context.Context, id uint64) (*Message, error)
98103

104+
// FetchMessageByOutPoint retrieves a message from the mailbox by its
105+
// claimed outpoint of the TX proof that was used to send it. If no
106+
// message with the given outpoint exists, it returns
107+
// ErrMessageNotFound.
108+
FetchMessageByOutPoint(ctx context.Context,
109+
claimedOp wire.OutPoint) (*Message, error)
110+
99111
// QueryMessages retrieves messages based on a query.
100112
QueryMessages(ctx context.Context, filter MessageFilter) ([]*Message,
101113
error)

authmailbox/mock.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,42 @@ import (
77
"sync/atomic"
88

99
"github.com/btcsuite/btcd/wire"
10+
"github.com/lightninglabs/taproot-assets/proof"
1011
)
1112

1213
type MockMsgStore struct {
13-
messages map[uint64]*Message
14-
nextMessageID atomic.Uint64
15-
mu sync.RWMutex
14+
messages map[uint64]*Message
15+
outpointToMessage map[wire.OutPoint]uint64
16+
nextMessageID atomic.Uint64
17+
proofs map[wire.OutPoint]struct{}
18+
mu sync.RWMutex
1619
}
1720

1821
var _ MsgStore = (*MockMsgStore)(nil)
1922

2023
func NewMockStore() *MockMsgStore {
2124
return &MockMsgStore{
22-
messages: make(map[uint64]*Message),
25+
messages: make(map[uint64]*Message),
26+
outpointToMessage: make(map[wire.OutPoint]uint64),
27+
proofs: make(map[wire.OutPoint]struct{}),
2328
}
2429
}
2530

26-
func (s *MockMsgStore) StoreMessage(_ context.Context, _ wire.OutPoint,
31+
func (s *MockMsgStore) StoreMessage(_ context.Context, txProof proof.TxProof,
2732
msg *Message) (uint64, error) {
2833

2934
s.mu.Lock()
3035
defer s.mu.Unlock()
3136

37+
if _, exists := s.proofs[txProof.ClaimedOutPoint]; exists {
38+
return 0, proof.ErrTxMerkleProofExists
39+
}
40+
41+
s.proofs[txProof.ClaimedOutPoint] = struct{}{}
42+
3243
id := s.nextMessageID.Add(1)
3344
s.messages[id] = msg
45+
s.outpointToMessage[txProof.ClaimedOutPoint] = id
3446

3547
return id, nil
3648
}
@@ -49,6 +61,20 @@ func (s *MockMsgStore) FetchMessage(_ context.Context,
4961
return msg, nil
5062
}
5163

64+
func (s *MockMsgStore) FetchMessageByOutPoint(ctx context.Context,
65+
claimedOp wire.OutPoint) (*Message, error) {
66+
67+
s.mu.RLock()
68+
defer s.mu.RUnlock()
69+
70+
msgID, exists := s.outpointToMessage[claimedOp]
71+
if !exists {
72+
return nil, ErrMessageNotFound
73+
}
74+
75+
return s.FetchMessage(ctx, msgID)
76+
}
77+
5278
func (s *MockMsgStore) QueryMessages(_ context.Context,
5379
filter MessageFilter) ([]*Message, error) {
5480

0 commit comments

Comments
 (0)