@@ -63,8 +63,7 @@ type EVMTXGun struct {
6363 seqNosMu sync.Mutex
6464 sentMsgCh chan SentMessage // Channel for real-time message notifications
6565 closeOnce sync.Once // Ensure channel is closed only once
66- nonceMu sync.Mutex
67- nonce map [NonceKey ]* atomic.Uint64
66+ nonce sync.Map // map[NonceKey]*uint64
6867 messageProfiles []load.MessageProfileConfig
6968 userSelector map [uint64 ]func () * bind.TransactOpts
7069}
@@ -88,7 +87,6 @@ func NewEVMTransactionGun(cfg *ccv.Cfg, e *deployment.Environment, selectors []u
8887 impl : impls ,
8988 sentMsgSet : make (map [SentMessage ]struct {}),
9089 sentMsgCh : make (chan SentMessage , sentMessageChannelBufferSize ),
91- nonce : make (map [NonceKey ]* atomic.Uint64 ),
9290 srcSelectors : srcSelectors ,
9391 destSelectors : destSelectors ,
9492 userSelector : userSelector ,
@@ -123,7 +121,6 @@ func NewEVMTransactionGunFromTestConfig(cfg *ccv.Cfg, testProfile *load.TestProf
123121 impl : impls ,
124122 sentMsgSet : make (map [SentMessage ]struct {}),
125123 sentMsgCh : make (chan SentMessage , sentMessageChannelBufferSize ),
126- nonce : make (map [NonceKey ]* atomic.Uint64 ),
127124 srcSelectors : srcSelectors ,
128125 destSelectors : destSelectors ,
129126 messageProfiles : messageProfiles ,
@@ -132,10 +129,7 @@ func NewEVMTransactionGunFromTestConfig(cfg *ccv.Cfg, testProfile *load.TestProf
132129}
133130
134131func (m * EVMTXGun ) initNonce (key NonceKey , userAddress common.Address ) error {
135- m .nonceMu .Lock ()
136- defer m .nonceMu .Unlock ()
137-
138- if m .nonce [key ] != nil {
132+ if _ , loaded := m .nonce .Load (key ); loaded {
139133 return nil
140134 }
141135
@@ -144,8 +138,12 @@ func (m *EVMTXGun) initNonce(key NonceKey, userAddress common.Address) error {
144138 return fmt .Errorf ("failed to get pending nonce for selector %d: %w" , key .Selector , err )
145139 }
146140
147- m .nonce [key ] = & atomic.Uint64 {}
148- m .nonce [key ].Store (n )
141+ // Allocate a pointer so the stored value can be incremented atomically across
142+ // goroutines without replacing the map entry. LoadOrStore ensures exactly one
143+ // pointer wins even if multiple goroutines race through initialization.
144+ ptr := new (uint64 )
145+ * ptr = n
146+ m .nonce .LoadOrStore (key , ptr )
149147 return nil
150148}
151149
@@ -174,6 +172,15 @@ func (m *EVMTXGun) Call(_ *wasp.Generator) *wasp.Response {
174172 return & wasp.Response {Error : err .Error (), Failed : true }
175173 }
176174
175+ nonceVal , ok := m .nonce .Load (nonceKey )
176+ if ! ok {
177+ return & wasp.Response {Error : fmt .Sprintf ("nonce not initialized for key %+v" , nonceKey ), Failed : true }
178+ }
179+ noncePtr := nonceVal .(* uint64 )
180+ // Atomically claim the next nonce. AddUint64 returns the new value, so
181+ // subtracting 1 gives us the nonce we own exclusively for this send.
182+ currentNonce := atomic .AddUint64 (noncePtr , 1 ) - 1
183+
177184 b := ccv .NewDefaultCLDFBundle (m .e )
178185 m .e .OperationsBundle = b
179186
@@ -182,7 +189,7 @@ func (m *EVMTXGun) Call(_ *wasp.Generator) *wasp.Response {
182189 return & wasp.Response {Error : "impl is not CCIP17EVM" , Failed : true }
183190 }
184191
185- sentEvent , err := c .SendMessageWithNonce (ctx , destSelector , fields , opts , sender , m . nonce [ nonceKey ] , true )
192+ sentEvent , err := c .SendMessageWithNonce (ctx , destSelector , fields , opts , sender , & currentNonce , true )
186193 if err != nil {
187194 return & wasp.Response {Error : fmt .Errorf ("failed to send message: %w" , err ).Error (), Failed : true }
188195 }
0 commit comments