• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 25903914664-1

15 May 2026 06:28AM UTC coverage: 47.122% (-38.8%) from 85.959%
25903914664-1

Pull #7199

github

94e391
web-flow
Merge 109f2828c into 1c7b8e6ac
Pull Request #7199: Feat: L1 and L2 early unlocks, updating signer

103343 of 219309 relevant lines covered (47.12%)

12880462.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.02
/stackslib/src/core/nonce_cache.rs
1
// Copyright (C) 2025 Stacks Open Internet Foundation
2
//
3
// This program is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// This program is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

16
use std::thread;
17
use std::time::Duration;
18

19
use clarity::types::chainstate::StacksAddress;
20
use clarity::util::lru_cache::{FlushError, LruCache};
21
use clarity::vm::clarity::ClarityConnection;
22
use rand::Rng;
23
use rusqlite::params;
24

25
use crate::chainstate::stacks::db::StacksChainState;
26
use crate::util_lib::db::{query_row, u64_to_sql, DBConn, Error as db_error};
27

28
/// Used to cache nonces in memory and in the mempool database.
29
/// 1. MARF - source of truth for nonces
30
/// 2. Nonce DB - table in mempool sqlite database
31
/// 3. HashMap - in-memory cache for nonces
32
/// The in-memory cache is restricted to a maximum size to avoid memory
33
/// exhaustion. When the cache is full, it should be flushed to the database
34
/// and cleared. It is recommended to do this in between batches of candidate
35
/// transactions from the mempool.
36
pub struct NonceCache {
37
    /// In-memory LRU cache of nonces.
38
    cache: LruCache<StacksAddress, u64>,
39
    max_size: usize,
40
}
41

42
impl NonceCache {
43
    pub fn new(max_size: usize) -> Self {
347,904✔
44
        Self {
347,904✔
45
            cache: LruCache::new(max_size),
347,904✔
46
            max_size,
347,904✔
47
        }
347,904✔
48
    }
347,904✔
49

50
    /// Reset the cache to an empty state and clear the nonce DB.
51
    /// This should only be called when the cache is corrupted.
52
    fn reset_cache(&mut self, conn: &mut DBConn) {
×
53
        self.cache = LruCache::new(self.max_size);
×
54
        if let Err(e) = conn.execute("DELETE FROM nonces", []) {
×
55
            warn!("error clearing nonces table: {e}");
×
56
        }
×
57
    }
×
58

59
    /// Get a nonce.
60
    /// First, the RAM cache will be checked for this address.
61
    /// If absent, then the `nonces` table will be queried for this address.
62
    /// If absent, then the MARF will be queried for this address.
63
    ///
64
    /// If not in RAM, the nonce will be opportunistically stored to the `nonces` table.  If that
65
    /// fails due to lock contention, then the method will return `true` for its second tuple argument.
66
    ///
67
    /// Returns (nonce, should-try-store-again?)
68
    pub fn get<C>(
725,452,866✔
69
        &mut self,
725,452,866✔
70
        address: &StacksAddress,
725,452,866✔
71
        clarity_tx: &mut C,
725,452,866✔
72
        mempool_db: &mut DBConn,
725,452,866✔
73
    ) -> u64
725,452,866✔
74
    where
725,452,866✔
75
        C: ClarityConnection,
725,452,866✔
76
    {
77
        // Check in-memory cache
78
        match self.cache.get(address) {
725,452,866✔
79
            Ok(Some(nonce)) => return nonce,
723,914,676✔
80
            Ok(None) => {}
1,538,190✔
81
            Err(_) => {
×
82
                // The cache is corrupt, reset it
×
83
                self.reset_cache(mempool_db);
×
84
            }
×
85
        }
86

87
        // Check sqlite cache
88
        let db_nonce_opt = db_get_nonce(mempool_db, address).unwrap_or_else(|e| {
1,538,190✔
89
            warn!("error retrieving nonce from mempool db: {e}");
×
90
            None
×
91
        });
×
92
        if let Some(db_nonce) = db_nonce_opt {
1,538,190✔
93
            // Insert into in-memory cache, but it is not dirty,
94
            // since we just got it from the database.
95
            let evicted = match self.cache.insert_clean(address.clone(), db_nonce) {
537,174✔
96
                Ok(evicted) => evicted,
537,174✔
97
                Err(_) => {
98
                    // The cache is corrupt, reset it
99
                    self.reset_cache(mempool_db);
×
100
                    None
×
101
                }
102
            };
103
            if evicted.is_some() {
537,174✔
104
                // If we evicted something, we need to flush the cache.
×
105
                self.flush_with_evicted(mempool_db, evicted);
×
106
            }
537,174✔
107
            return db_nonce;
537,174✔
108
        }
1,001,016✔
109

110
        // Check the chainstate
111
        let nonce = StacksChainState::get_nonce(clarity_tx, &address.clone().into());
1,001,016✔
112

113
        self.set(address.clone(), nonce, mempool_db);
1,001,016✔
114
        nonce
1,001,016✔
115
    }
725,452,866✔
116

117
    /// Set the nonce for `address` to `value` in the in-memory cache.
118
    /// If this causes an eviction, flush the in-memory cache to the DB.
119
    pub fn set(&mut self, address: StacksAddress, value: u64, conn: &mut DBConn) {
1,633,383✔
120
        let evicted = match self.cache.insert(address.clone(), value) {
1,633,383✔
121
            Ok(evicted) => evicted,
1,633,383✔
122
            Err(_) => {
123
                // The cache is corrupt, reset it
124
                self.reset_cache(conn);
×
125
                Some((address, value))
×
126
            }
127
        };
128
        if evicted.is_some() {
1,633,383✔
129
            // If we evicted something, we need to flush the cache.
×
130
            self.flush_with_evicted(conn, evicted);
×
131
        }
1,633,383✔
132
    }
1,633,383✔
133

134
    /// Flush the in-memory cache the the DB, including `evicted`.
135
    /// Do not return until successful.
136
    pub fn flush_with_evicted(&mut self, conn: &mut DBConn, evicted: Option<(StacksAddress, u64)>) {
479,421✔
137
        const MAX_BACKOFF: Duration = Duration::from_secs(30);
138
        let mut backoff = Duration::from_millis(rand::thread_rng().gen_range(50..200));
479,421✔
139

140
        loop {
141
            let result = self.try_flush_with_evicted(conn, evicted.clone());
479,421✔
142

143
            match result {
479,421✔
144
                Ok(_) => return, // Success: exit the loop
479,421✔
145
                Err(e) => {
×
146
                    // Calculate a backoff duration
147
                    warn!("Nonce cache flush failed: {e}. Retrying in {backoff:?}");
×
148

149
                    // Sleep for the backoff duration
150
                    thread::sleep(backoff);
×
151

152
                    if backoff < MAX_BACKOFF {
×
153
                        // Exponential backoff
×
154
                        backoff = backoff * 2
×
155
                            + Duration::from_millis(rand::thread_rng().gen_range(50..200));
×
156
                    }
×
157
                }
158
            }
159
        }
160
    }
479,421✔
161

162
    /// Try to flush the in-memory cache the the DB, including `evicted`.
163
    pub fn try_flush_with_evicted(
479,421✔
164
        &mut self,
479,421✔
165
        conn: &mut DBConn,
479,421✔
166
        evicted: Option<(StacksAddress, u64)>,
479,421✔
167
    ) -> Result<(), db_error> {
479,421✔
168
        // Flush the cache to the database
169
        let sql = "INSERT OR REPLACE INTO nonces (address, nonce) VALUES (?1, ?2)";
479,421✔
170

171
        let tx = conn.transaction()?;
479,421✔
172

173
        if let Some((addr, nonce)) = evicted {
479,421✔
174
            tx.execute(sql, params![addr, nonce])?;
×
175
        }
479,421✔
176

177
        match self.cache.flush(|addr, nonce| {
1,234,251✔
178
            tx.execute(sql, params![addr, nonce])?;
1,036,080✔
179
            Ok::<(), db_error>(())
1,036,080✔
180
        }) {
1,036,080✔
181
            Ok(_) => {}
479,421✔
182
            Err(FlushError::LruCacheCorrupted) => {
183
                drop(tx);
×
184
                // The cache is corrupt, reset it and return
185
                self.reset_cache(conn);
×
186
                return Ok(());
×
187
            }
188
            Err(FlushError::FlushError(e)) => return Err(e),
×
189
        };
190

191
        tx.commit()?;
479,421✔
192

193
        Ok(())
479,421✔
194
    }
479,421✔
195

196
    /// Flush the in-memory cache to the DB.
197
    /// Do not return until successful.
198
    pub fn flush(&mut self, conn: &mut DBConn) {
479,421✔
199
        self.flush_with_evicted(conn, None)
479,421✔
200
    }
479,421✔
201
}
202

203
fn db_set_nonce(conn: &DBConn, address: &StacksAddress, nonce: u64) -> Result<(), db_error> {
×
204
    let addr_str = address.to_string();
×
205
    let nonce_i64 = u64_to_sql(nonce)?;
×
206

207
    let sql = "INSERT OR REPLACE INTO nonces (address, nonce) VALUES (?1, ?2)";
×
208
    conn.execute(sql, params![addr_str, nonce_i64])?;
×
209
    Ok(())
×
210
}
×
211

212
fn db_get_nonce(conn: &DBConn, address: &StacksAddress) -> Result<Option<u64>, db_error> {
1,538,190✔
213
    let addr_str = address.to_string();
1,538,190✔
214

215
    let sql = "SELECT nonce FROM nonces WHERE address = ?";
1,538,190✔
216
    query_row(conn, sql, params![addr_str])
1,538,190✔
217
}
1,538,190✔
218

219
#[cfg(test)]
220
mod tests {
221
    use clarity::consts::CHAIN_ID_TESTNET;
222
    use clarity::types::chainstate::StacksBlockId;
223
    use clarity::types::Address;
224
    use clarity::vm::tests::{TEST_BURN_STATE_DB, TEST_HEADER_DB};
225

226
    use super::*;
227
    use crate::chainstate::stacks::db::test::{chainstate_path, instantiate_chainstate};
228
    use crate::chainstate::stacks::index::ClarityMarfTrieId;
229
    use crate::clarity_vm::clarity::ClarityInstance;
230
    use crate::clarity_vm::database::marf::MarfedKV;
231
    use crate::core::MemPoolDB;
232

233
    #[test]
234
    fn test_nonce_cache() {
×
235
        let _chainstate = instantiate_chainstate(false, 0x80000000, function_name!());
×
236
        let chainstate_path = chainstate_path(function_name!());
×
237
        let mut mempool = MemPoolDB::open_test(false, CHAIN_ID_TESTNET, &chainstate_path).unwrap();
×
238
        let mut cache = NonceCache::new(2);
×
239

240
        let addr1 =
×
241
            StacksAddress::from_string("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM").unwrap();
×
242
        let addr2 =
×
243
            StacksAddress::from_string("ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5").unwrap();
×
244
        let addr3 =
×
245
            StacksAddress::from_string("ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG").unwrap();
×
246

247
        let conn = &mut mempool.db;
×
248
        cache.set(addr1.clone(), 1, conn);
×
249
        cache.set(addr2.clone(), 2, conn);
×
250

251
        let marf = MarfedKV::temporary();
×
252
        let mut clarity_instance = ClarityInstance::new(false, CHAIN_ID_TESTNET, marf);
×
253
        clarity_instance
×
254
            .begin_test_genesis_block(
×
255
                &StacksBlockId::sentinel(),
×
256
                &StacksBlockId([0u8; 32]),
×
257
                &TEST_HEADER_DB,
×
258
                &TEST_BURN_STATE_DB,
×
259
            )
260
            .commit_block();
×
261
        let mut clarity_conn = clarity_instance.begin_block(
×
262
            &StacksBlockId([0 as u8; 32]),
×
263
            &StacksBlockId([1 as u8; 32]),
×
264
            &TEST_HEADER_DB,
×
265
            &TEST_BURN_STATE_DB,
×
266
        );
267

268
        clarity_conn.as_transaction(|clarity_tx| {
×
269
            assert_eq!(cache.get(&addr1, clarity_tx, conn), 1);
×
270
            assert_eq!(cache.get(&addr2, clarity_tx, conn), 2);
×
271
            // addr3 is not in the cache, so it should be fetched from the
272
            // clarity instance (and get 0)
273
            assert_eq!(cache.get(&addr3, clarity_tx, conn), 0);
×
274
        });
×
275
    }
×
276

277
    #[test]
278
    fn test_db_set_nonce() {
×
279
        let _chainstate = instantiate_chainstate(false, 0x80000000, function_name!());
×
280
        let chainstate_path = chainstate_path(function_name!());
×
281
        let mut mempool = MemPoolDB::open_test(false, CHAIN_ID_TESTNET, &chainstate_path).unwrap();
×
282
        let conn = &mut mempool.db;
×
283
        let addr = StacksAddress::from_string("ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC").unwrap();
×
284
        db_set_nonce(&conn, &addr, 123).unwrap();
×
285
        assert_eq!(db_get_nonce(&conn, &addr).unwrap().unwrap(), 123);
×
286
    }
×
287

288
    #[test]
289
    fn test_nonce_cache_eviction() {
×
290
        let _chainstate = instantiate_chainstate(false, 0x80000000, function_name!());
×
291
        let chainstate_path = chainstate_path(function_name!());
×
292
        let mut mempool = MemPoolDB::open_test(false, CHAIN_ID_TESTNET, &chainstate_path).unwrap();
×
293
        let mut cache = NonceCache::new(2); // Cache size of 2
×
294

295
        let addr1 =
×
296
            StacksAddress::from_string("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM").unwrap();
×
297
        let addr2 =
×
298
            StacksAddress::from_string("ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5").unwrap();
×
299
        let addr3 =
×
300
            StacksAddress::from_string("ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG").unwrap();
×
301

302
        let conn = &mut mempool.db;
×
303

304
        // Fill cache to capacity
305
        cache.set(addr1.clone(), 1, conn);
×
306
        cache.set(addr2.clone(), 2, conn);
×
307

308
        // This should cause addr1 to be evicted
309
        cache.set(addr3.clone(), 3, conn);
×
310

311
        // Verify addr1 was written to DB during eviction
312
        assert_eq!(db_get_nonce(&conn, &addr1).unwrap().unwrap(), 1);
×
313
    }
×
314

315
    #[test]
316
    fn test_nonce_cache_flush() {
×
317
        let _chainstate = instantiate_chainstate(false, 0x80000000, function_name!());
×
318
        let chainstate_path = chainstate_path(function_name!());
×
319
        let mut mempool = MemPoolDB::open_test(false, CHAIN_ID_TESTNET, &chainstate_path).unwrap();
×
320
        let mut cache = NonceCache::new(3);
×
321

322
        let addr1 =
×
323
            StacksAddress::from_string("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM").unwrap();
×
324
        let addr2 =
×
325
            StacksAddress::from_string("ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5").unwrap();
×
326

327
        let conn = &mut mempool.db;
×
328

329
        cache.set(addr1.clone(), 5, conn);
×
330
        cache.set(addr2.clone(), 10, conn);
×
331

332
        // Explicitly flush cache
333
        cache.flush(conn);
×
334

335
        // Verify both entries were written to DB
336
        assert_eq!(db_get_nonce(&conn, &addr1).unwrap().unwrap(), 5);
×
337
        assert_eq!(db_get_nonce(&conn, &addr2).unwrap().unwrap(), 10);
×
338
    }
×
339

340
    #[test]
341
    fn test_db_nonce_overwrite() {
×
342
        let _chainstate = instantiate_chainstate(false, 0x80000000, function_name!());
×
343
        let chainstate_path = chainstate_path(function_name!());
×
344
        let mut mempool = MemPoolDB::open_test(false, CHAIN_ID_TESTNET, &chainstate_path).unwrap();
×
345
        let conn = &mut mempool.db;
×
346

347
        let addr = StacksAddress::from_string("ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC").unwrap();
×
348

349
        // Set initial nonce
350
        db_set_nonce(&conn, &addr, 1).unwrap();
×
351
        assert_eq!(db_get_nonce(&conn, &addr).unwrap().unwrap(), 1);
×
352

353
        // Overwrite with new nonce
354
        db_set_nonce(&conn, &addr, 2).unwrap();
×
355
        assert_eq!(db_get_nonce(&conn, &addr).unwrap().unwrap(), 2);
×
356
    }
×
357
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc