@@ -30,7 +30,7 @@ import (
3030
3131// MsgChannelCapacity specifies the capacity of the message channel used to publish messages released from the accountant.
3232// This channel should not back up, but if it does, the accountant will start dropping messages, which would require reobservations.
33- const MsgChannelCapacity = 5 * batchSize
33+ const MsgChannelCapacity = 5 * DefaultSubmitObservationBatchSize
3434
3535type (
3636 AccountantWormchainConn interface {
@@ -75,23 +75,24 @@ type (
7575
7676// Accountant is the object that manages the interface to the wormchain accountant smart contract.
7777type Accountant struct {
78- ctx context.Context
79- logger * zap.Logger
80- db guardianDB.AccountantDB
81- obsvReqWriteC chan <- * gossipv1.ObservationRequest
82- contract string
83- wsUrl string
84- wormchainConn AccountantWormchainConn
85- enforceFlag bool
86- guardianSigner guardiansigner.GuardianSigner
87- gst * common.GuardianSetState
88- guardianAddr ethCommon.Address
89- msgChan chan <- * common.MessagePublication
90- tokenBridges validEmitters
91- pendingTransfersLock sync.Mutex
92- pendingTransfers map [string ]* pendingEntry // Key is the message ID (emitterChain/emitterAddr/seqNo)
93- subChan chan * common.MessagePublication
94- env common.Environment
78+ ctx context.Context
79+ logger * zap.Logger
80+ db guardianDB.AccountantDB
81+ obsvReqWriteC chan <- * gossipv1.ObservationRequest
82+ contract string
83+ wsUrl string
84+ wormchainConn AccountantWormchainConn
85+ enforceFlag bool
86+ guardianSigner guardiansigner.GuardianSigner
87+ gst * common.GuardianSetState
88+ guardianAddr ethCommon.Address
89+ msgChan chan <- * common.MessagePublication
90+ tokenBridges validEmitters
91+ pendingTransfersLock sync.Mutex
92+ pendingTransfers map [string ]* pendingEntry // Key is the message ID (emitterChain/emitterAddr/seqNo)
93+ subChan chan * common.MessagePublication
94+ submitObservationBatchSize int
95+ env common.Environment
9596
9697 nttContract string
9798 nttWormchainConn AccountantWormchainConn
@@ -101,7 +102,10 @@ type Accountant struct {
101102}
102103
103104// On startup, there can be a large number of re-submission requests.
104- const subChanSize = 500
105+ const subChanSize = 5000
106+
107+ // auditSubmitTimeout is the timeout for blocking channel writes during audit.
108+ const auditSubmitTimeout = 30 * time .Second
105109
106110// baseEnabled returns true if the base accountant is enabled, false if not.
107111func (acct * Accountant ) baseEnabled () bool {
@@ -123,25 +127,31 @@ func NewAccountant(
123127 guardianSigner guardiansigner.GuardianSigner , // the guardian signer used for signing observation requests
124128 gst * common.GuardianSetState , // used to get the current guardian set index when sending observation requests
125129 msgChan chan <- * common.MessagePublication , // the channel where transfers received by the accountant runnable should be published
130+ submitObservationBatchSize int , // maximum number of observations to submit to the contract in one transaction
126131 env common.Environment , // Controls the set of token bridges to be monitored
127132) * Accountant {
133+ if submitObservationBatchSize <= 0 {
134+ submitObservationBatchSize = DefaultSubmitObservationBatchSize
135+ }
136+
128137 return & Accountant {
129- ctx : ctx ,
130- logger : logger .With (zap .String ("component" , "gacct" )),
131- db : db ,
132- obsvReqWriteC : obsvReqWriteC ,
133- contract : contract ,
134- wsUrl : wsUrl ,
135- wormchainConn : wormchainConn ,
136- enforceFlag : enforceFlag ,
137- guardianSigner : guardianSigner ,
138- gst : gst ,
139- guardianAddr : ethCrypto .PubkeyToAddress (guardianSigner .PublicKey (ctx )),
140- msgChan : msgChan ,
141- tokenBridges : make (validEmitters ),
142- pendingTransfers : make (map [string ]* pendingEntry ),
143- subChan : make (chan * common.MessagePublication , subChanSize ),
144- env : env ,
138+ ctx : ctx ,
139+ logger : logger .With (zap .String ("component" , "gacct" )),
140+ db : db ,
141+ obsvReqWriteC : obsvReqWriteC ,
142+ contract : contract ,
143+ wsUrl : wsUrl ,
144+ wormchainConn : wormchainConn ,
145+ enforceFlag : enforceFlag ,
146+ guardianSigner : guardianSigner ,
147+ gst : gst ,
148+ guardianAddr : ethCrypto .PubkeyToAddress (guardianSigner .PublicKey (ctx )),
149+ msgChan : msgChan ,
150+ tokenBridges : make (validEmitters ),
151+ pendingTransfers : make (map [string ]* pendingEntry ),
152+ subChan : make (chan * common.MessagePublication , subChanSize ),
153+ submitObservationBatchSize : submitObservationBatchSize ,
154+ env : env ,
145155
146156 nttContract : nttContract ,
147157 nttWormchainConn : nttWormchainConn ,
@@ -153,7 +163,7 @@ func NewAccountant(
153163
154164// Start initializes the accountant and starts the worker and watcher runnables.
155165func (acct * Accountant ) Start (ctx context.Context ) error {
156- acct .logger .Debug ("entering Start" , zap .Bool ("enforceFlag" , acct .enforceFlag ), zap .Bool ("baseEnabled" , acct .baseEnabled ()), zap .Bool ("nttEnabled" , acct .nttEnabled ()))
166+ acct .logger .Debug ("entering Start" , zap .Bool ("enforceFlag" , acct .enforceFlag ), zap .Bool ("baseEnabled" , acct .baseEnabled ()), zap .Bool ("nttEnabled" , acct .nttEnabled ()), zap . Int ( "submitObservationBatchSize" , acct . submitObservationBatchSize ) )
157167 acct .pendingTransfersLock .Lock ()
158168 defer acct .pendingTransfersLock .Unlock ()
159169
@@ -215,9 +225,13 @@ func (acct *Accountant) Start(ctx context.Context) error {
215225 return fmt .Errorf ("failed to start watcher: %w" , err )
216226 }
217227
218- if err := supervisor .Run (ctx , "acctaudit" , common .WrapWithScissors (acct .audit , "acctaudit" )); err != nil {
219- return fmt .Errorf ("failed to start audit worker: %w" , err )
220- }
228+ }
229+ }
230+
231+ // Start the audit worker if not mocking/testing and either Global or NTT accountant are enabled
232+ if acct .env != common .AccountantMock && acct .env != common .GoTest && (acct .baseEnabled () || acct .nttEnabled ()) {
233+ if err := supervisor .Run (ctx , "acctaudit" , common .WrapWithScissors (acct .audit , "acctaudit" )); err != nil {
234+ return fmt .Errorf ("failed to start audit worker: %w" , err )
221235 }
222236 }
223237
@@ -315,7 +329,11 @@ func (acct *Accountant) SubmitObservation(msg *common.MessagePublication) (bool,
315329 acct .pendingTransfersLock .Lock ()
316330 defer acct .pendingTransfersLock .Unlock ()
317331
318- // If this is already pending, don't send it again.
332+ var pe * pendingEntry
333+
334+ // If there is a digest mismatch, don't send it again.
335+ // Otherwise resubmit it and rely on the submitPending flag to prevent duplicate submissions to the contract.
336+ // This allows manual reobservations to proceed.
319337 if oldEntry , exists := acct .pendingTransfers [msgId ]; exists {
320338 if oldEntry .digest != digest {
321339 digestMismatches .Inc ()
@@ -325,17 +343,18 @@ func (acct *Accountant) SubmitObservation(msg *common.MessagePublication) (bool,
325343 zap .String ("newDigest" , digest ),
326344 zap .Bool ("enforcing" , enforceFlag ),
327345 )
328- } else {
329- acct .logger .Info ("blocking transfer because it is already outstanding" , zap .String ("msgID" , msgId ), zap .Bool ("enforcing" , enforceFlag ))
330- }
331- return ! enforceFlag , nil
332- }
333346
334- // Add it to the pending map and the database.
335- pe := & pendingEntry {msg : msg , msgId : msgId , digest : digest , isNTT : isNTT , enforceFlag : enforceFlag }
336- if err := acct .addPendingTransferAlreadyLocked (pe ); err != nil {
337- acct .logger .Error ("failed to persist pending transfer, blocking publishing" , zap .String ("msgID" , msgId ), zap .Error (err ))
338- return false , err
347+ return ! enforceFlag , nil
348+ }
349+ pe = oldEntry
350+ } else {
351+ // Add it to the pending map and the database.
352+ // We only add it if it is not already present.
353+ pe = & pendingEntry {msg : msg , msgId : msgId , digest : digest , isNTT : isNTT , enforceFlag : enforceFlag }
354+ if err := acct .addPendingTransferAlreadyLocked (pe ); err != nil {
355+ acct .logger .Error ("failed to persist pending transfer, blocking publishing" , zap .String ("msgID" , msgId ), zap .Error (err ))
356+ return false , err
357+ }
339358 }
340359
341360 // This transaction may take a while. Pass it off to the worker so we don't block the processor.
@@ -345,7 +364,7 @@ func (acct *Accountant) SubmitObservation(msg *common.MessagePublication) (bool,
345364 tag = "ntt-accountant"
346365 }
347366 acct .logger .Info (fmt .Sprintf ("submitting transfer to %s for approval" , tag ), zap .String ("msgID" , msgId ), zap .Bool ("canPublish" , ! enforceFlag ))
348- _ = acct .submitObservation (pe )
367+ _ = acct .submitObservation (acct . ctx , pe , false ) // Non-blocking from processor
349368 }
350369
351370 // If we are not enforcing accountant, the event can be published. Otherwise we have to wait to hear back from the contract.
@@ -436,37 +455,68 @@ func (acct *Accountant) loadPendingTransfers() error {
436455
437456// submitObservation sends an observation request to the worker so it can be submitted to the contract. If the transfer is already
438457// marked as "submit pending", this function returns false without doing anything. Otherwise it returns true. The return value can
439- // be used to avoid unnecessary error logging. If writing to the channel would block, this function returns without doing anything,
440- // assuming the pending transfer will be handled on the next audit interval. This function grabs the state lock.
441- func (acct * Accountant ) submitObservation (pe * pendingEntry ) bool {
458+ // be used to avoid unnecessary error logging. If blocking is false and writing to the channel would block, this function returns
459+ // without doing anything, assuming the pending transfer will be handled on the next audit interval. If blocking is true, it will
460+ // block until the channel has space, a timeout occurs, or the context is cancelled. This function grabs the state lock.
461+ func (acct * Accountant ) submitObservation (ctx context.Context , pe * pendingEntry , blocking bool ) bool {
442462 pe .stateLock .Lock ()
443- defer pe .stateLock .Unlock ()
444463
445464 if pe .state .submitPending {
465+ pe .stateLock .Unlock ()
446466 return false
447467 }
448468
449469 pe .state .submitPending = true
450470 pe .state .updTime = time .Now ()
471+ pe .stateLock .Unlock ()
472+
473+ timeout := time .Duration (0 )
474+ if blocking {
475+ timeout = auditSubmitTimeout
476+ }
451477
452478 if pe .isNTT {
453- acct .submitToChannel (pe , acct .nttSubChan , "ntt-accountant" )
479+ acct .submitToChannel (ctx , pe , acct .nttSubChan , "ntt-accountant" , blocking , timeout )
454480 } else {
455- acct .submitToChannel (pe , acct .subChan , "accountant" )
481+ acct .submitToChannel (ctx , pe , acct .subChan , "accountant" , blocking , timeout )
456482 }
457483
458484 return true
459485}
460486
461- // submitToChannel submits an observation to the specified channel. If the submission fails because the channel is full,
462- // it marks the transfer as pending so it will be resubmitted by the audit.
463- func (acct * Accountant ) submitToChannel (pe * pendingEntry , subChan chan * common.MessagePublication , tag string ) {
464- select {
465- case subChan <- pe .msg :
466- acct .logger .Debug (fmt .Sprintf ("submitted observation to channel for %s" , tag ), zap .String ("msgId" , pe .msgId ))
467- default :
468- acct .logger .Error (fmt .Sprintf ("unable to submit observation to %s because the channel is full, will try next interval" , tag ), zap .String ("msgId" , pe .msgId ))
469- pe .state .submitPending = false
487+ // submitToChannel submits an observation to the specified channel. If blocking is false and the channel is full,
488+ // it marks the transfer as no longer pending so it will be resubmitted by the audit. If blocking is true, it will
489+ // block until the channel has space, a timeout occurs, or the context is cancelled.
490+ func (acct * Accountant ) submitToChannel (ctx context.Context , pe * pendingEntry , subChan chan * common.MessagePublication , tag string , blocking bool , timeout time.Duration ) {
491+ if blocking {
492+ select {
493+ case subChan <- pe .msg :
494+ acct .logger .Debug (fmt .Sprintf ("submitted observation to channel for %s" , tag ), zap .String ("msgId" , pe .msgId ))
495+ case <- time .After (timeout ):
496+ channelSubmitTimeouts .Inc ()
497+ acct .logger .Warn (fmt .Sprintf ("timeout submitting observation to %s channel, will retry next audit" , tag ),
498+ zap .String ("msgId" , pe .msgId ),
499+ zap .Duration ("timeout" , timeout ))
500+ pe .stateLock .Lock ()
501+ pe .state .submitPending = false
502+ pe .stateLock .Unlock ()
503+ case <- ctx .Done ():
504+ acct .logger .Warn (fmt .Sprintf ("context cancelled while submitting to %s channel" , tag ), zap .String ("msgId" , pe .msgId ))
505+ pe .stateLock .Lock ()
506+ pe .state .submitPending = false
507+ pe .stateLock .Unlock ()
508+ }
509+ } else {
510+ // Non-blocking write
511+ select {
512+ case subChan <- pe .msg :
513+ acct .logger .Debug (fmt .Sprintf ("submitted observation to channel for %s" , tag ), zap .String ("msgId" , pe .msgId ))
514+ default :
515+ acct .logger .Error (fmt .Sprintf ("unable to submit observation to %s because the channel is full, will try next interval" , tag ), zap .String ("msgId" , pe .msgId ))
516+ pe .stateLock .Lock ()
517+ pe .state .submitPending = false
518+ pe .stateLock .Unlock ()
519+ }
470520 }
471521}
472522
0 commit comments