@@ -110,6 +110,40 @@ pub const ALREADY_SUBMITTED_PATTERNS: &[&str] = &[
110110 "same hash was already imported" ,
111111] ;
112112
113+ /// Error message patterns indicating the transaction nonce is ahead of the expected on-chain nonce.
114+ /// This can be transient (burst ordering: tx N+1 arrives before N) or persistent (counter drift).
115+ ///
116+ /// Checked **after** `ALREADY_SUBMITTED_PATTERNS` in `classify_submission_error` to avoid
117+ /// ambiguity. Each entry is a lowercased substring to match against the RPC error message.
118+ pub const NONCE_TOO_HIGH_PATTERNS : & [ & str ] = & [
119+ "nonce too high" , // Geth, Erigon, Hardhat, Anvil
120+ "nonce is too high" , // Geth, Erigon, Hardhat, Anvil
121+ "nonce too far in the future" , // Besu
122+ "exceeds next nonce" , // Nethermind
123+ "nonce out of range" , // Arbitrum, Optimism, specialized RPCs
124+ "tx-nonce-too-high" , // Certain SaaS RPC providers (e.g. Alchemy/Infura internal)
125+ ] ;
126+
127+ /// Maximum number of "nonce too high" retries before escalating to a nonce health job.
128+ /// With ~25s between retries (driven by status checker resend timeout), this means
129+ /// escalation happens within ~75s — enough time for transient burst ordering to resolve.
130+ pub const MAX_NONCE_TOO_HIGH_RETRIES : u32 = 3 ;
131+
132+ /// Maximum number of nonces to scan when detecting gaps between on-chain and local counter.
133+ /// Gaps beyond this range are logged for operator investigation rather than automated recovery.
134+ pub const MAX_GAP_SCAN_RANGE : u64 = 100 ;
135+
136+ /// Metadata key used in `RelayerHealthCheck` to indicate a targeted health action.
137+ pub const HEALTH_CHECK_ACTION_KEY : & str = "health_check_action" ;
138+
139+ /// Value for `HEALTH_CHECK_ACTION_KEY` that triggers nonce gap detection and resolution.
140+ pub const HEALTH_CHECK_ACTION_NONCE_HEALTH : & str = "nonce_health" ;
141+
142+ /// Optional metadata key carrying a nonce hint for the health action.
143+ /// When present, `resolve_nonce_gaps` ensures the counter covers at least `hint + 1`
144+ /// so the scan range includes the hinted nonce. This handles the case where the
145+ /// counter was reset (e.g., after a restart) but a tx at a higher nonce still exists.
146+ pub const HEALTH_CHECK_NONCE_HINT_KEY : & str = "nonce_hint" ;
113147/// Checks if a lowercased message matches "known transaction" without matching
114148/// "unknown transaction" (substring false positive).
115149pub fn matches_known_transaction ( msg_lower : & str ) -> bool {
@@ -122,3 +156,57 @@ pub fn matches_known_transaction(msg_lower: &str) -> bool {
122156 }
123157 false
124158}
159+
160+ #[ cfg( test) ]
161+ mod tests {
162+ use super :: * ;
163+
164+ #[ test]
165+ fn test_nonce_too_high_patterns_match_expected_strings ( ) {
166+ let cases = [
167+ "nonce too high" ,
168+ "nonce is too high" ,
169+ "nonce too far in the future" ,
170+ "exceeds next nonce" ,
171+ "nonce out of range" ,
172+ ] ;
173+ for case in & cases {
174+ let msg_lower = case. to_lowercase ( ) ;
175+ assert ! (
176+ NONCE_TOO_HIGH_PATTERNS
177+ . iter( )
178+ . any( |p| msg_lower. contains( p) ) ,
179+ "Expected NONCE_TOO_HIGH_PATTERNS to match: {case}"
180+ ) ;
181+ }
182+ }
183+
184+ #[ test]
185+ fn test_matches_known_transaction_does_not_match_nonce_too_high ( ) {
186+ let nonce_too_high_msgs = [
187+ "nonce too high" ,
188+ "nonce is too high" ,
189+ "nonce too far in the future" ,
190+ "exceeds next nonce" ,
191+ "nonce out of range" ,
192+ ] ;
193+ for msg in & nonce_too_high_msgs {
194+ assert ! (
195+ !matches_known_transaction( & msg. to_lowercase( ) ) ,
196+ "matches_known_transaction should NOT match nonce-too-high message: {msg}"
197+ ) ;
198+ }
199+ }
200+
201+ #[ test]
202+ fn test_matches_known_transaction_matches_known_transaction ( ) {
203+ assert ! ( matches_known_transaction( "known transaction" ) ) ;
204+ assert ! ( matches_known_transaction( "already known transaction here" ) ) ;
205+ }
206+
207+ #[ test]
208+ fn test_matches_known_transaction_does_not_match_unknown_transaction ( ) {
209+ assert ! ( !matches_known_transaction( "unknown transaction" ) ) ;
210+ assert ! ( !matches_known_transaction( "unknown transaction status" ) ) ;
211+ }
212+ }
0 commit comments