`PersistedWallet::create` empty-store check and initial write are not atomic
Los mantenedores suelen responder en 2 días
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 48/100
Línea de trabajo
Start with PersistedWallet::create and create_async in src/wallet/persisted.rs, then review the WalletPersister initialize and persist contract and the rusqlite implementation. Run the concurrent reproduction from the issue under tests/ and determine how creation should behave when both callers race; done means the two calls cannot leave a PersistedWallet inconsistent with the store.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Describe the bug
PersistedWallet::create (src/wallet/persisted.rs) guards against overwriting an existing store by calling P::initialize and returning DataAlreadyExists if the changeset is non-empty, then writes the initial changeset with a separate P::persist call. The two steps are not atomic. With the rusqlite::Connection persister each step commits its own transaction, so two callers creating a wallet on the same database at the same time can both observe an empty store and both get Ok.
The store itself does not get corrupted in the descriptor sense: the SQLite ON CONFLICT ... COALESCE upsert and ChangeSet::merge keep the first descriptor. But the second PersistedWallet is then inconsistent with its store: it holds a different descriptor in memory, its later persist calls write keychain/tx data for that descriptor into the first wallet's database, and a subsequent load returns the first wallet. create_async has the same structure.
This issue was found by AI.
To Reproduce
// tests/, requires the `test-utils` and `rusqlite` features
use std::sync::{Arc, Barrier};
use std::thread;
use bdk_chain::rusqlite;
use bdk_wallet::test_utils::{get_test_tr_single_sig_xprv, get_test_wpkh};
use bdk_wallet::{ChangeSet, KeychainKind, Wallet, WalletPersister};
use bitcoin::Network;
/// Wraps the SQLite persister only to force the interleaving:
/// both `initialize`s complete before either `persist`.
struct RacingConnection {
conn: rusqlite::Connection,
initialized: Arc<Barrier>,
persist_order: Arc<Barrier>,
persist_first: bool,
}
impl WalletPersister for RacingConnection {
type Error = rusqlite::Error;
fn initialize(p: &mut Self) -> Result<ChangeSet, Self::Error> {
let changeset = <rusqlite::Connection as WalletPersister>::initialize(&mut p.conn)?;
p.initialized.wait();
Ok(changeset)
}
fn persist(p: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> {
if p.persist_first {
let res = <rusqlite::Connection as WalletPersister>::persist(&mut p.conn, changeset);
p.persist_order.wait();
res
} else {
p.persist_order.wait();
<rusqlite::Connection as WalletPersister>::persist(&mut p.conn, changeset)
}
}
}
#[test]
fn concurrent_create_on_same_store() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("wallet.db");
let mut bootstrap = rusqlite::Connection::open(&db_path).unwrap();
<rusqlite::Connection as WalletPersister>::initialize(&mut bootstrap).unwrap();
drop(bootstrap);
let initialized = Arc::new(Barrier::new(2));
let persist_order = Arc::new(Barrier::new(2));
let mut first_store = RacingConnection {
conn: rusqlite::Connection::open(&db_path).unwrap(),
initialized: initialized.clone(),
persist_order: persist_order.clone(),
persist_first: true,
};
let mut second_store = RacingConnection {
conn: rusqlite::Connection::open(&db_path).unwrap(),
initialized,
persist_order,
persist_first: false,
};
let first = thread::spawn(move || {
Wallet::create_single(get_test_tr_single_sig_xprv())
.network(Network::Testnet)
.create_wallet(&mut first_store)
});
let second = thread::spawn(move || {
Wallet::create_single(get_test_wpkh())
.network(Network::Testnet)
.create_wallet(&mut second_store)
});
let first = first.join().unwrap();
let second = second.join().unwrap();
// Both succeed. The store keeps the `tr(..)` descriptor while `second`
// holds the `wpkh(..)` one in memory.
assert!(first.is_ok());
let second = second.unwrap();
let mut conn = rusqlite::Connection::open(&db_path).unwrap();
let loaded = Wallet::load().load_wallet(&mut conn).unwrap().unwrap();
assert_ne!(
second.public_descriptor(KeychainKind::External),
loaded.public_descriptor(KeychainKind::External)
);
}
Expected behavior
Only one of two concurrent create calls on the same store should succeed, or the resulting PersistedWallet should always match what is in the store. Whether that is solved in PersistedWallet::create, in the persister trait contract, or by documenting that callers must serialize first-time creation (e.g. via the rusqlite::Transaction persister) is up to the maintainers.
Build environment
- BDK tag/commit:
6fc68462(master)
Which backend(s) are relevant (if any)?
- None / not backend-related (e.g.
bdk_chain,bdk_core)
Is this blocking production use?
- No
- Lenguaje dominante
- Rust
- Estrellas
- 61
- Forks
- 116
- Merge medio
- 7 h 29 min
- PR fusionados (30 d)
- 6
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de bitcoindevkit/bdk_wallet
-
Change output crossing a compactsize boundary leaves the fee slightly below the requested feeratePosiblemente ocupada @artofbitcoin la tomó hace 3 días. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
bitcoindevkit/bdk_wallet#578 · 2 comentarios ·
Los mantenedores suelen responder en 2 días
-
Change address `mark_used` marker is not persisted; same change address reissued after restartPosiblemente ocupada @NaitikVerma6776 la tomó hace 3 días. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
bitcoindevkit/bdk_wallet#577 · 3 comentarios ·
Los mantenedores suelen responder en 2 días
-
bdk_wallet uses `std` items without feature gatePosiblemente ocupada @Chibey-max la tomó hace 7 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
bitcoindevkit/bdk_wallet#574 · 1 comentario ·
Los mantenedores suelen responder en 2 días
-
`CaravanExport::to_descriptors` accepts duplicate xpubsPosiblemente ocupada @chukwudiikeh la tomó hace 7 días. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
bitcoindevkit/bdk_wallet#560 · 1 comentario ·
Los mantenedores suelen responder en 2 días
-
`PsbtUtils::fee_amount` panics when PSBT outputs exceed inputsPosiblemente ocupada @shubham5080 la tomó hace 30 días. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
bitcoindevkit/bdk_wallet#558 · 1 comentario ·
Los mantenedores suelen responder en 2 días
Todos los issues de bitcoindevkit/bdk_wallet
Issues similares
-
[1.x Bug] run-operation --sql reports success but rolls back writes on transaction-capable adaptersAbiertotriage
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
dbt-labs/dbt#16784 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
area:cli bug good first issue priority:medium
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
rtk-ai/rtk#4615 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bnv-manager-v2: accept_invite creates the Mailcow domain admin before the commitPosiblemente ocupada @myOmikron la tomó hoy. Abiertobug ready-for-agent
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
-
Windows: mio 1.2.3 named-pipe use-after-free panics the native IPC broker and hangs every toolAbiertoplatform:windows tool triaged wontfix
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
Los mantenedores suelen responder en 1 día
-
resources: add a regression test confirming admin mint still works after the #16 SAC handoffPosiblemente ocupada @Daniexie la tomó hoy. Abiertocontracts good first issue Stellar Wave
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
OlaGreat/starbound-realms#137 · 1 comentario ·