Skip to content

Commit 9a34add

Browse files
authored
Recover cleanly from failed unlocks and release the VSS fence on graceful teardown (#91)
* Recover cleanly from failed unlocks and release the VSS fence on graceful teardown; clear changing-state on any exit; run go_online off the async runtime * Fix VSS remove to use the current object version so channel-monitor deletes converge instead of looping on version conflicts
1 parent 6fe67d0 commit 9a34add

7 files changed

Lines changed: 476 additions & 63 deletions

File tree

src/ldk.rs

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3740,6 +3740,8 @@ pub(crate) async fn start_ldk(
37403740
// before ack); ChannelManager and aux state keep the local-first `kv_store`
37413741
// sync facade. Both share the same local DB and (when configured) VSS store.
37423742
#[cfg(feature = "vss")]
3743+
let mut fence_guard: Option<crate::vss_kv_store::FenceReleaseGuard> = None;
3744+
#[cfg(feature = "vss")]
37433745
let (kv_store, monitor_kv_store) = if let (Some(ref vss_url), Some(ref identity)) =
37443746
(&static_state.vss_url, &vss_identity)
37453747
{
@@ -3760,6 +3762,16 @@ pub(crate) async fn start_ldk(
37603762
.acquire_fence()
37613763
.map_err(|e| APIError::FailedVssInit(e.to_string()))?;
37623764

3765+
// Release the just-acquired fence if the rest of startup fails.
3766+
fence_guard = Some(crate::vss_kv_store::FenceReleaseGuard::new({
3767+
let store = Arc::clone(&vss_kv_store);
3768+
move || {
3769+
if let Err(e) = store.release_fence_if_owned() {
3770+
tracing::warn!(error = %e, "failed to release VSS fence after aborted unlock");
3771+
}
3772+
}
3773+
}));
3774+
37633775
let monitor_kv_store = Arc::new(RemoteFirstKvStore::new(
37643776
Arc::clone(&local_kv_store),
37653777
Some(Arc::clone(&vss_kv_store)),
@@ -4271,8 +4283,20 @@ pub(crate) async fn start_ldk(
42714283
witness_version: WitnessVersion::Taproot,
42724284
};
42734285
let reuse_addresses = static_state.reuse_addresses;
4274-
let mut rgb_wallet = tokio::task::spawn_blocking(move || {
4275-
RgbLibWallet::new(
4286+
let indexer_url_owned = indexer_url.to_string();
4287+
#[cfg(feature = "vss")]
4288+
let rgb_vss_backup = match (&static_state.vss_url, &vss_identity) {
4289+
(Some(vss_url), Some(identity)) => Some((
4290+
vss_url.clone(),
4291+
format!("{}_rgb", identity.pubkey_hex),
4292+
identity.signing_key,
4293+
)),
4294+
_ => None,
4295+
};
4296+
// go_online and configure_vss_backup drive blocking rgb-lib HTTP clients;
4297+
// run them off the async runtime so they don't fail on a single-vCPU host.
4298+
let (rgb_wallet, rgb_online) = tokio::task::spawn_blocking(move || {
4299+
let mut rgb_wallet = RgbLibWallet::new(
42764300
WalletData {
42774301
data_dir,
42784302
bitcoin_network,
@@ -4283,40 +4307,31 @@ pub(crate) async fn start_ldk(
42834307
},
42844308
keys,
42854309
)
4286-
.expect("valid rgb-lib wallet")
4310+
.expect("valid rgb-lib wallet");
4311+
let rgb_online = rgb_wallet.go_online(OnlineOptions {
4312+
indexer_url: indexer_url_owned,
4313+
skip_consistency_check: false,
4314+
vanilla_sync_lookback: 20,
4315+
})?;
4316+
#[cfg(feature = "vss")]
4317+
if let Some((vss_url, rgb_store_id, signing_key)) = rgb_vss_backup {
4318+
let vss_config =
4319+
rgb_lib::wallet::vss::VssBackupConfig::new(vss_url, rgb_store_id, signing_key)
4320+
.with_encryption(true)
4321+
.with_auto_backup(true)
4322+
.with_backup_mode(rgb_lib::wallet::vss::VssBackupMode::Blocking);
4323+
// Fail closed: a misconfigured backup must not silently run local-only.
4324+
rgb_wallet.configure_vss_backup(vss_config).map_err(|e| {
4325+
APIError::FailedVssInit(format!(
4326+
"Failed to configure VSS backup for RGB wallet: {e}"
4327+
))
4328+
})?;
4329+
tracing::info!("VSS auto-backup (blocking) enabled for RGB wallet");
4330+
}
4331+
Ok::<_, APIError>((rgb_wallet, rgb_online))
42874332
})
42884333
.await
4289-
.unwrap();
4290-
let rgb_online = rgb_wallet.go_online(OnlineOptions {
4291-
indexer_url: indexer_url.to_string(),
4292-
skip_consistency_check: false,
4293-
vanilla_sync_lookback: 20,
4294-
})?;
4295-
4296-
// Configure VSS backup for the RGB wallet if VSS is enabled. Reuses the
4297-
// identity derived once at the top of this function — see N3.1 / the
4298-
// `derive_vss_identity` helper.
4299-
#[cfg(feature = "vss")]
4300-
if let (Some(ref vss_url), Some(ref identity)) = (&static_state.vss_url, &vss_identity) {
4301-
let rgb_store_id = format!("{}_rgb", identity.pubkey_hex);
4302-
let vss_config = rgb_lib::wallet::vss::VssBackupConfig::new(
4303-
vss_url.clone(),
4304-
rgb_store_id,
4305-
identity.signing_key,
4306-
)
4307-
.with_encryption(true)
4308-
.with_auto_backup(true)
4309-
// Blocking: each RGB op waits until its VSS backup is durable.
4310-
.with_backup_mode(rgb_lib::wallet::vss::VssBackupMode::Blocking);
4311-
4312-
// Fail closed: a misconfigured backup must not silently run local-only.
4313-
rgb_wallet.configure_vss_backup(vss_config).map_err(|e| {
4314-
APIError::FailedVssInit(format!(
4315-
"Failed to configure VSS backup for RGB wallet: {e}"
4316-
))
4317-
})?;
4318-
tracing::info!("VSS auto-backup (blocking) enabled for RGB wallet");
4319-
}
4334+
.map_err(|e| APIError::Unexpected(format!("rgb-lib wallet setup task failed: {e}")))??;
43204335
save_config(
43214336
&static_state.db(),
43224337
kv_store.as_ref(),
@@ -5094,6 +5109,11 @@ pub(crate) async fn start_ldk(
50945109
tracing::info!("LDK logs are available at <your-supplied-ldk-data-dir-path>/.ldk/logs");
50955110
tracing::info!("Local Node ID is {}", channel_manager.get_our_node_id());
50965111

5112+
#[cfg(feature = "vss")]
5113+
if let Some(guard) = fence_guard.as_mut() {
5114+
guard.disarm();
5115+
}
5116+
50975117
Ok((
50985118
LdkBackgroundServices {
50995119
stop_processing,
@@ -5183,6 +5203,27 @@ pub(crate) async fn stop_ldk(app_state: Arc<AppState>) {
51835203
join_handle.await.unwrap().unwrap();
51845204
}
51855205

5206+
// Graceful teardown (lock, shutdown, signal): release the VSS fence so
5207+
// the next unlock — a fresh instance id — takes over without an explicit
5208+
// /vssclearfence. Hard kills still leave the fence behind by design.
5209+
#[cfg(feature = "vss")]
5210+
{
5211+
let kv_store = app_state
5212+
.get_unlocked_app_state()
5213+
.await
5214+
.as_ref()
5215+
.map(|unlocked| Arc::clone(&unlocked.kv_store));
5216+
if let Some(kv_store) = kv_store {
5217+
match tokio::task::spawn_blocking(move || kv_store.release_vss_fence_if_owned()).await {
5218+
Ok(Ok(())) => {}
5219+
Ok(Err(e)) => {
5220+
tracing::warn!(error = %e, "failed to release VSS fence during shutdown")
5221+
}
5222+
Err(e) => tracing::warn!(error = %e, "VSS fence release task failed"),
5223+
}
5224+
}
5225+
}
5226+
51865227
// connect to the peer port so it can be released
51875228
let peer_port = app_state.static_state.ldk_peer_listening_port;
51885229
let sock_addr = SocketAddr::from(([127, 0, 0, 1], peer_port));

src/routes.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5028,28 +5028,22 @@ pub(crate) async fn unlock(
50285028
}
50295029
}
50305030

5031-
let mnemonic = match check_password_validity(&payload.password, &state.db()) {
5032-
Ok(mnemonic) => mnemonic,
5033-
Err(e) => {
5034-
state.update_changing_state(false);
5035-
return Err(e);
5036-
}
5037-
};
5031+
// Clear the changing-state flag on any exit — including a panic during
5032+
// startup — so a failed unlock can't wedge the node in ChangingState.
5033+
let _changing_state_guard = crate::utils::CallOnDrop::new({
5034+
let state = state.clone();
5035+
move || state.update_changing_state(false)
5036+
});
5037+
5038+
let mnemonic = check_password_validity(&payload.password, &state.db())?;
50385039

50395040
tracing::debug!("Starting LDK...");
5040-
let (new_ldk_background_services, new_unlocked_app_state) = match start_ldk(
5041+
let (new_ldk_background_services, new_unlocked_app_state) = start_ldk(
50415042
state.clone(),
50425043
crate::core_types::NodeKeySource::InternalMnemonic(mnemonic),
50435044
payload.into(),
50445045
)
5045-
.await
5046-
{
5047-
Ok((nlbs, nuap)) => (nlbs, nuap),
5048-
Err(e) => {
5049-
state.update_changing_state(false);
5050-
return Err(e);
5051-
}
5052-
};
5046+
.await?;
50535047
tracing::debug!("LDK started");
50545048

50555049
state
@@ -5058,8 +5052,6 @@ pub(crate) async fn unlock(
50585052

50595053
state.update_ldk_background_services(Some(new_ldk_background_services));
50605054

5061-
state.update_changing_state(false);
5062-
50635055
tracing::info!("Unlock completed");
50645056
Ok(Json(EmptyResponse {}))
50655057
})

src/synced_kv_store.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ impl SyncedKvStore {
6464
}
6565
}
6666

67+
/// Releases the VSS single-writer fence if this instance owns it. No-op
68+
/// without a remote store.
69+
#[cfg(feature = "vss")]
70+
pub(crate) fn release_vss_fence_if_owned(&self) -> Result<(), io::Error> {
71+
match &self.remote {
72+
Some(remote) => remote.release_fence_if_owned(),
73+
None => Ok(()),
74+
}
75+
}
76+
6777
/// Restores all key-value pairs from VSS into the local store.
6878
///
6979
/// `force = false` is the safe default and returns `Ok(0)` if the local

src/test/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,9 @@ async fn start_node_with_vss(
473473
String::new()
474474
} else {
475475
let resp = init(node_address, &password, mnemonic.map(|m| m.to_string())).await;
476-
// Take over our own store_id: a same-seed restart inherits the previous
477-
// incarnation's fence (nothing releases it on shutdown). No-op on a first
478-
// start.
476+
// Take over our own store_id: a same-seed restart may inherit a fence
477+
// from a previous incarnation that exited without a graceful teardown.
478+
// No-op on a first start.
479479
clear_vss_fence(node_address, &password).await;
480480
resp.mnemonic
481481
};

0 commit comments

Comments
 (0)