@@ -39,9 +39,13 @@ type GASP struct {
3939 LogPrefix string
4040 Unidirectional bool
4141 LogLevel slog.Level
42- limiter chan struct {}
42+ limiter chan struct {} // Concurrency limiter controlled by Concurrency config
43+
44+ // Global deduplication cache for processed nodes across all UTXOs
45+ processedNodes sync.Map // map[transaction.Outpoint]struct{} - prevents duplicate processing
4346}
4447
48+
4549func NewGASP (params GASPParams ) * GASP {
4650 gasp := & GASP {
4751 Storage : params .Storage ,
@@ -50,6 +54,7 @@ func NewGASP(params GASPParams) *GASP {
5054 Unidirectional : params .Unidirectional ,
5155 // Sequential: params.Sequential,
5256 }
57+ // Concurrency limiter controlled by Concurrency config
5358 if params .Concurrency > 1 {
5459 gasp .limiter = make (chan struct {}, params .Concurrency )
5560 } else {
@@ -83,7 +88,7 @@ func (g *GASP) Sync(ctx context.Context, host string, limit uint32) error {
8388 outpoint := fmt .Sprintf ("%s.%d" , utxo .Txid , utxo .OutputIndex )
8489 knownOutpoints [outpoint ] = struct {}{}
8590 }
86- sharedOutpoints := make ( map [ string ] struct {})
91+ var sharedOutpoints sync. Map
8792
8893 var initialResponse * InitialResponse
8994 for {
@@ -104,14 +109,17 @@ func (g *GASP) Sync(ctx context.Context, host string, limit uint32) error {
104109 }
105110 outpoint := utxo .OutpointString ()
106111 if _ , exists := knownOutpoints [outpoint ]; exists {
107- sharedOutpoints [ outpoint ] = struct {}{}
112+ sharedOutpoints . Store ( outpoint , struct {}{})
108113 delete (knownOutpoints , outpoint )
109- } else if _ , shared := sharedOutpoints [ outpoint ] ; ! shared {
114+ } else if _ , shared := sharedOutpoints . Load ( outpoint ) ; ! shared {
110115 ingestQueue = append (ingestQueue , utxo )
111116 }
112117 }
113118
119+ // Process all UTXOs from this batch with shared deduplication
114120 var wg sync.WaitGroup
121+ seenNodes := & sync.Map {} // Shared across all UTXOs in this batch
122+
115123 for _ , utxo := range ingestQueue {
116124 wg .Add (1 )
117125 g .limiter <- struct {}{}
@@ -127,15 +135,15 @@ func (g *GASP) Sync(ctx context.Context, host string, limit uint32) error {
127135 return
128136 }
129137 slog .Debug (fmt .Sprintf ("%sReceived unspent graph node from remote: %v" , g .LogPrefix , resolvedNode ))
130- if err = g .processIncomingNode (ctx , resolvedNode , nil , & sync. Map {} ); err != nil {
138+ if err = g .processIncomingNode (ctx , resolvedNode , nil , seenNodes ); err != nil {
131139 slog .Warn (fmt .Sprintf ("%sError processing incoming node %s: %v" , g .LogPrefix , outpoint , err ))
132140 return
133141 }
134142 if err = g .CompleteGraph (ctx , resolvedNode .GraphID ); err != nil {
135143 slog .Warn (fmt .Sprintf ("%sError completing graph for %s: %v" , g .LogPrefix , outpoint , err ))
136144 return
137145 }
138- sharedOutpoints [ outpoint .String ()] = struct {}{}
146+ sharedOutpoints . Store ( outpoint .String (), struct {}{})
139147 }(utxo )
140148 }
141149 wg .Wait ()
@@ -153,7 +161,7 @@ func (g *GASP) Sync(ctx context.Context, host string, limit uint32) error {
153161 for _ , utxo := range localUTXOs {
154162 outpoint := fmt .Sprintf ("%s.%d" , utxo .Txid , utxo .OutputIndex )
155163 if utxo .Score >= initialResponse .Since {
156- if _ , shared := sharedOutpoints [ outpoint ] ; ! shared {
164+ if _ , shared := sharedOutpoints . Load ( outpoint ) ; ! shared {
157165 replyUTXOs = append (replyUTXOs , utxo )
158166 }
159167 }
@@ -280,16 +288,27 @@ func (g *GASP) processIncomingNode(ctx context.Context, node *Node, spentBy *tra
280288 if txid , err := g .computeTxID (node .RawTx ); err != nil {
281289 return err
282290 } else {
283- nodeId := ( & transaction.Outpoint {
291+ nodeOutpoint := & transaction.Outpoint {
284292 Txid : * txid ,
285293 Index : node .OutputIndex ,
286- }).String ()
294+ }
295+ nodeId := nodeOutpoint .String ()
296+
287297 slog .Debug (fmt .Sprintf ("%sProcessing incoming node: %v, spentBy: %v" , g .LogPrefix , node , spentBy ))
298+
299+ // Global deduplication check
300+ if _ , exists := g .processedNodes .LoadOrStore (* nodeOutpoint , struct {}{}); exists {
301+ slog .Debug (fmt .Sprintf ("%sNode %s already processed globally, skipping" , g .LogPrefix , nodeId ))
302+ return nil
303+ }
304+
305+ // Per-graph cycle detection
288306 if _ , ok := seenNodes .Load (nodeId ); ok {
289- slog .Debug (fmt .Sprintf ("%sNode %s already processed , skipping." , g .LogPrefix , nodeId ))
307+ slog .Debug (fmt .Sprintf ("%sNode %s already seen in this graph , skipping." , g .LogPrefix , nodeId ))
290308 return nil
291309 }
292310 seenNodes .Store (nodeId , struct {}{})
311+
293312 if err := g .Storage .AppendToGraph (ctx , node , spentBy ); err != nil {
294313 return err
295314 } else if neededInputs , err := g .Storage .FindNeededInputs (ctx , node ); err != nil {
@@ -300,18 +319,16 @@ func (g *GASP) processIncomingNode(ctx context.Context, node *Node, spentBy *tra
300319 errors := make (chan error )
301320 for outpointStr , data := range neededInputs .RequestedInputs {
302321 wg .Add (1 )
303- g .limiter <- struct {}{}
304322 go func (outpointStr string , data * NodeResponseData ) {
305- defer func () {
306- <- g .limiter
307- wg .Done ()
308- }()
323+ defer wg .Done ()
324+
309325 slog .Info (fmt .Sprintf ("%sRequesting new node for outpoint: %s, metadata: %v" , g .LogPrefix , outpointStr , data .Metadata ))
310326 if outpoint , err := transaction .OutpointFromString (outpointStr ); err != nil {
311327 errors <- err
312- } else if newNode , err := g .Remote .RequestNode (ctx , node . GraphID , outpoint , data .Metadata ); err != nil {
328+ } else if newNode , err := g .Remote .RequestNode (ctx , outpoint , outpoint , data .Metadata ); err != nil {
313329 errors <- err
314330 } else {
331+
315332 slog .Debug (fmt .Sprintf ("%sReceived new node: %v" , g .LogPrefix , newNode ))
316333 // Create outpoint for the current node that is spending this input
317334 spendingOutpoint := & transaction.Outpoint {
@@ -365,12 +382,8 @@ func (g *GASP) processOutgoingNode(ctx context.Context, node *Node, seenNodes *s
365382 var wg sync.WaitGroup
366383 for outpointStr , data := range response .RequestedInputs {
367384 wg .Add (1 )
368- g .limiter <- struct {}{}
369385 go func (outpointStr string , data * NodeResponseData ) {
370- defer func () {
371- <- g .limiter
372- wg .Done ()
373- }()
386+ defer wg .Done ()
374387 var outpoint * transaction.Outpoint
375388 var err error
376389 if outpoint , err = transaction .OutpointFromString (outpointStr ); err == nil {
@@ -392,6 +405,8 @@ func (g *GASP) processOutgoingNode(ctx context.Context, node *Node, seenNodes *s
392405 return nil
393406}
394407
408+
409+
395410func (g * GASP ) computeTxID (rawtx string ) (* chainhash.Hash , error ) {
396411 if tx , err := transaction .NewTransactionFromHex (rawtx ); err != nil {
397412 return nil , err
0 commit comments