Skip to content

Commit 6afe507

Browse files
authored
fix: race in Signer::fetch_tx_nonce (#101)
* fix: race in Signer::fetch_tx_nonce * fix: get rid of AtomicU64 behind Mutex * chore: make clippy happy * chore: track_caller * chore: make clippy happy
1 parent 9ef1d85 commit 6afe507

File tree

3 files changed

+15
-28
lines changed

3 files changed

+15
-28
lines changed

api/src/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ pub enum ArgumentValidationError {
160160
#[error("Account creation error: {0}")]
161161
AccountCreationError(#[from] AccountCreationError),
162162
#[error("Multiple errors: {0:?}")]
163-
MultipleErrors(Vec<ArgumentValidationError>),
163+
MultipleErrors(Vec<Self>),
164164
}
165165

166166
impl ArgumentValidationError {

api/src/signer/mod.rs

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ use std::{
110110
collections::HashMap,
111111
path::{Path, PathBuf},
112112
sync::{
113-
atomic::{AtomicU64, AtomicUsize, Ordering},
113+
atomic::{AtomicUsize, Ordering},
114114
Arc,
115115
},
116116
};
@@ -126,7 +126,7 @@ use near_api_types::{
126126
use borsh::{BorshDeserialize, BorshSerialize};
127127
use serde::{Deserialize, Serialize};
128128
use slipped10::BIP32Path;
129-
use tracing::{debug, info, instrument, trace, warn};
129+
use tracing::{debug, instrument, warn};
130130

131131
use crate::{
132132
config::NetworkConfig,
@@ -394,7 +394,7 @@ pub trait SignerTrait {
394394
/// It provides an access key pooling and a nonce caching mechanism to improve transaction throughput.
395395
pub struct Signer {
396396
pool: tokio::sync::RwLock<HashMap<PublicKey, Box<dyn SignerTrait + Send + Sync + 'static>>>,
397-
nonce_cache: tokio::sync::RwLock<HashMap<(AccountId, PublicKey), AtomicU64>>,
397+
nonce_cache: futures::lock::Mutex<HashMap<(AccountId, PublicKey), u64>>,
398398
current_public_key: AtomicUsize,
399399
}
400400

@@ -410,7 +410,7 @@ impl Signer {
410410
public_key,
411411
Box::new(signer) as Box<dyn SignerTrait + Send + Sync + 'static>,
412412
)])),
413-
nonce_cache: tokio::sync::RwLock::new(HashMap::new()),
413+
nonce_cache: futures::lock::Mutex::new(HashMap::new()),
414414
current_public_key: AtomicUsize::new(0),
415415
}))
416416
}
@@ -545,36 +545,20 @@ impl Signer {
545545
network: &NetworkConfig,
546546
) -> Result<(Nonce, CryptoHash, BlockHeight), SignerError> {
547547
debug!(target: SIGNER_TARGET, "Fetching transaction nonce");
548+
548549
let nonce_data = crate::account::Account(account_id.clone())
549550
.access_key(public_key)
550551
.fetch_from(network)
551552
.await
552553
.map_err(|e| SignerError::FetchNonceError(Box::new(e)))?;
553-
let nonce_cache = self.nonce_cache.read().await;
554-
555-
if let Some(nonce) = nonce_cache.get(&(account_id.clone(), public_key)) {
556-
let nonce = nonce.fetch_add(1, Ordering::SeqCst);
557-
drop(nonce_cache);
558-
trace!(target: SIGNER_TARGET, "Nonce fetched from cache");
559-
return Ok((nonce + 1, nonce_data.block_hash, nonce_data.block_height));
560-
} else {
561-
drop(nonce_cache);
562-
}
563-
564-
// It's initialization, so it's better to take write lock, so other will wait
565554

566-
// case where multiple writers end up at the same lock acquisition point and tries
567-
// to overwrite the cached value that a previous writer already wrote.
568-
let nonce = self
569-
.nonce_cache
570-
.write()
571-
.await
572-
.entry((account_id.clone(), public_key))
573-
.or_insert_with(|| AtomicU64::new(nonce_data.data.nonce.0 + 1))
574-
.fetch_max(nonce_data.data.nonce.0 + 1, Ordering::SeqCst)
575-
.max(nonce_data.data.nonce.0 + 1);
555+
let nonce = {
556+
let mut nonce_cache = self.nonce_cache.lock().await;
557+
let nonce = nonce_cache.entry((account_id, public_key)).or_default();
558+
*nonce = (*nonce).max(nonce_data.data.nonce.0) + 1;
559+
*nonce
560+
};
576561

577-
info!(target: SIGNER_TARGET, "Nonce fetched and cached");
578562
Ok((nonce, nonce_data.block_hash, nonce_data.block_height))
579563
}
580564

types/src/transaction/result.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub struct Execution<T> {
3939
}
4040

4141
impl<T> Execution<T> {
42+
#[track_caller]
4243
pub fn assert_success(self) -> T {
4344
#[allow(clippy::unwrap_used)]
4445
self.into_result().unwrap()
@@ -251,11 +252,13 @@ impl ExecutionFinalResult {
251252
///
252253
/// Because this function may panic, its use is generally discouraged. Instead, prefer
253254
/// to call into [`into_result`](ExecutionFinalResult::into_result) then pattern matching and handle the Err case explicitly.
255+
#[track_caller]
254256
pub fn assert_success(self) -> ExecutionSuccess {
255257
#[allow(clippy::unwrap_used)]
256258
self.into_result().unwrap()
257259
}
258260

261+
#[track_caller]
259262
pub fn assert_failure(self) -> ExecutionResult<TxExecutionError> {
260263
#[allow(clippy::unwrap_used)]
261264
self.into_result().unwrap_err()

0 commit comments

Comments
 (0)