datahaven-xyz/datahaven · error · Error
InvalidNonce
InvalidNonce
Error message
InvalidNonce
What it means
`process_message` throws `Error::InvalidNonce` when `Nonce::<T>::get(nonce)` is already set, meaning a message with this nonce was previously processed. The pallet uses nonces for exactly-once delivery; replaying an already-handled message is rejected to prevent double-crediting of assets or double-execution.
Solutions
- Check `Nonce::<T>::get(nonce)` (or the public nonce query) before submitting; skip the message if it is already marked processed.
- Configure the relayer to track successfully delivered nonces persistently and deduplicate retries.
- In tests, use fresh nonces per run or reset pallet storage between runs.
Example fix
// before: blind resubmit causes InvalidNonce
pallet.process_message(relayer, message.clone())?;
pallet.process_message(relayer, message)?; // replay
// after: dedupe by nonce
if Nonce::<Runtime>::get(message.nonce) { return Ok(()); } // already processed
pallet.process_message(relayer, message)?; Defensive patterns
Strategy: validation
Validate before calling
if (await api.query.inboundQueueV2.nonce(message.nonce)) { console.log('already processed'); return; } Type guard
function isUnprocessed(nonce, processedSet) { return !processedSet.has(nonce.toString()); } Try / catch
try { pallet.process_message(relayer, message); } catch (e) { if (matches!(e, Error::InvalidNonce)) { markDelivered(message.nonce); return Ok(()); } else { throw e; } } Prevention
- Persist delivered nonces in relayer state across restarts
- Treat InvalidNonce as idempotent-success, not failure
- Use fresh nonces in test fixtures
- Deduplicate events before dispatch
When it happens
Trigger: Calling `process_message` with a message whose nonce already exists in the `Nonce` storage map — i.e. a replayed or duplicated message after the original was successfully processed.
Common situations: A relayer resubmitting the same message after a timeout without knowing the first submission succeeded; replaying historical Gateway events after a node resync; test harnesses reusing fixture messages across runs without resetting state.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
AI-assisted analysis of datahaven-xyz/datahaven@edcb13dbbc (2026-09-13).
Data as JSON: /api/errors/1aac9d74cad343f7.
Report an issue: GitHub.
Appendix: source
Thrown at operator/pallets/inbound-queue-v2/src/lib.rs:230
ensure_root(origin)?;
OperatingMode::<T>::set(mode);
Self::deposit_event(Event::OperatingModeChanged { mode });
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub fn process_message(relayer: T::AccountId, message: Message) -> DispatchResult {
// Verify that the message was submitted from the known Gateway contract
ensure!(
T::GatewayAddress::get() == message.gateway,
Error::<T>::InvalidGateway
);
let (nonce, relayer_fee) = (message.nonce, message.relayer_fee);
// Verify the message has not been processed
ensure!(!Nonce::<T>::get(nonce.into()), Error::<T>::InvalidNonce);
// Process message
let message_id = T::MessageProcessor::process_message(relayer.clone(), message)?;
// Pay relayer reward if needed
if !relayer_fee.is_zero() {
T::RewardPayment::register_reward(
&relayer,
T::DefaultRewardKind::get(),
relayer_fee,
);
}
// Mark message as received
Nonce::<T>::set(nonce.into());
// Emit event with the message_id
Self::deposit_event(Event::MessageReceived { nonce, message_id });View on GitHub (pinned to edcb13dbbc)