@@ -413,6 +413,95 @@ func TestNotifyMempoolConfirmed_EmptyTxidsIsNoOp(t *testing.T) {
413413 }
414414}
415415
416+ // fakePeer is a minimal query.Peer used to assert handleInv's getdata
417+ // routing. It records every QueueMessageWithEncoding invocation; the
418+ // channels and Addr exist only so the type satisfies the interface.
419+ type fakePeer struct {
420+ addr string
421+ sent []wire.Message
422+ disconCh chan struct {}
423+ mu sync.Mutex
424+ }
425+
426+ func newFakePeer (addr string ) * fakePeer {
427+ return & fakePeer {addr : addr , disconCh : make (chan struct {})}
428+ }
429+
430+ func (p * fakePeer ) Addr () string { return p .addr }
431+ func (p * fakePeer ) OnDisconnect () <- chan struct {} { return p .disconCh }
432+ func (p * fakePeer ) SubscribeRecvMsg () (<- chan wire.Message , func ()) {
433+ ch := make (chan wire.Message )
434+ return ch , func () { close (ch ) }
435+ }
436+ func (p * fakePeer ) QueueMessageWithEncoding (m wire.Message , _ chan <- struct {},
437+ _ wire.MessageEncoding ,
438+ ) {
439+ p .mu .Lock ()
440+ p .sent = append (p .sent , m )
441+ p .mu .Unlock ()
442+ }
443+
444+ // TestHandleInv_GetDataGoesOnlyToAnnouncingPeer locks in the privacy
445+ // contract documented at the top of mempool.go: getdata for a tx inv is
446+ // issued only to the peer that announced the tx, never broadcast. If we
447+ // fanned out we would leak nothing about *which* txs we care about, but we
448+ // would still expand bandwidth quadratically and give a network-wide
449+ // observer extra fingerprintable behaviour.
450+ func TestHandleInv_GetDataGoesOnlyToAnnouncingPeer (t * testing.T ) {
451+ mgr := newTestRescanMgr (t , watchedAddrMain , nil )
452+ tr := newTracker (mgr )
453+
454+ announcer := newFakePeer ("announcer:8333" )
455+ other := newFakePeer ("other:8333" )
456+
457+ inv := wire .NewMsgInv ()
458+ hash := mustHash (t , "0000000000000000000000000000000000000000000000000000000000000ccc" )
459+ if err := inv .AddInvVect (& wire.InvVect {Type : wire .InvTypeTx , Hash : hash }); err != nil {
460+ t .Fatalf ("AddInvVect: %v" , err )
461+ }
462+
463+ tr .handleInv (announcer , inv )
464+
465+ if got := len (announcer .sent ); got != 1 {
466+ t .Fatalf ("announcer should have received 1 getdata, got %d" , got )
467+ }
468+ if got := len (other .sent ); got != 0 {
469+ t .Fatalf ("non-announcing peer must not receive getdata, got %d" , got )
470+ }
471+ gd , ok := announcer .sent [0 ].(* wire.MsgGetData )
472+ if ! ok {
473+ t .Fatalf ("expected MsgGetData, got %T" , announcer .sent [0 ])
474+ }
475+ if len (gd .InvList ) != 1 || gd .InvList [0 ].Hash != hash {
476+ t .Errorf ("unexpected getdata payload: %+v" , gd .InvList )
477+ }
478+ // Witness-stripped form is required so we receive the full tx.
479+ if gd .InvList [0 ].Type != wire .InvTypeWitnessTx {
480+ t .Errorf ("expected witness-tx inv type, got %v" , gd .InvList [0 ].Type )
481+ }
482+ }
483+
484+ // TestHandleInv_NoWatchedAddressesSkipsGetData verifies the early-exit in
485+ // handleInv: when zero addresses are watched, no getdata is issued (no
486+ // point fetching txs we can't match).
487+ func TestHandleInv_NoWatchedAddressesSkipsGetData (t * testing.T ) {
488+ mgr := newTestRescanMgr (t , "" , nil )
489+ tr := newTracker (mgr )
490+
491+ announcer := newFakePeer ("announcer:8333" )
492+ inv := wire .NewMsgInv ()
493+ hash := mustHash (t , "0000000000000000000000000000000000000000000000000000000000000ddd" )
494+ if err := inv .AddInvVect (& wire.InvVect {Type : wire .InvTypeTx , Hash : hash }); err != nil {
495+ t .Fatalf ("AddInvVect: %v" , err )
496+ }
497+
498+ tr .handleInv (announcer , inv )
499+
500+ if got := len (announcer .sent ); got != 0 {
501+ t .Fatalf ("no getdata expected with zero watched addrs, got %d" , got )
502+ }
503+ }
504+
416505// TestEvictExpired_DropsStaleInflight verifies the TTL sweep also clears
417506// abandoned getdata requests so a never-delivered tx (e.g. announcing peer
418507// disconnected before responding) stops blocking re-fetch from other peers.
0 commit comments