Skip to content

Commit 0737e94

Browse files
authored
remove rgb_send_lock: fail colored funding-prep instead of replaying the event (#67)
1 parent b5130d3 commit 0737e94

10 files changed

Lines changed: 54 additions & 135 deletions

File tree

src/error.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,6 @@ pub enum APIError {
313313
#[error("No valid transport endpoint found")]
314314
NoValidTransportEndpoint,
315315

316-
#[error("Cannot perform this operation while an open channel operation is in progress")]
317-
OpenChannelInProgress,
318-
319316
#[error("Output below the dust limit")]
320317
OutputBelowDustLimit,
321318

@@ -588,7 +585,6 @@ impl IntoResponse for APIError {
588585
| APIError::NoAvailableUtxos
589586
| APIError::NoRoute
590587
| APIError::NotInitialized
591-
| APIError::OpenChannelInProgress
592588
| APIError::PaymentNotFound(_)
593589
| APIError::RecipientIDAlreadyUsed
594590
| APIError::SwapNotFound(_)

src/ldk.rs

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,7 +1428,6 @@ fn normalize_funding_psbt_locktime(
14281428
// FundingGenerationReady. Returns the value to propagate from the event handler: `Err(ReplayEvent)`
14291429
// to retry the event (for transient network errors), or `Ok(())` after force-closing the channel
14301430
// (for terminal errors).
1431-
#[allow(dead_code)]
14321431
fn handle_funding_prepare_err(
14331432
e: RgbLibError,
14341433
channel_manager: &ChannelManager,
@@ -1556,7 +1555,6 @@ async fn handle_ldk_events(
15561555
false,
15571556
);
15581557
unlocked_state.virtual_channel_draft_delete(&temporary_channel_id);
1559-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
15601558
};
15611559

15621560
let mut virtual_funding_txo = virtual_channel_synthetic_outpoint(
@@ -1757,7 +1755,6 @@ async fn handle_ldk_events(
17571755
updated_at: get_current_timestamp(),
17581756
});
17591757
unlocked_state.virtual_channel_draft_delete(&temporary_channel_id);
1760-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
17611758
tracing::info!(
17621759
"EVENT: registered trusted no-broadcast funding {} for virtual channel {}",
17631760
virtual_funding_txo,
@@ -1777,7 +1774,6 @@ async fn handle_ldk_events(
17771774
false,
17781775
);
17791776
unlocked_state.virtual_channel_draft_delete(&temporary_channel_id);
1780-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
17811777
}
17821778
}
17831779
return Ok(());
@@ -1814,39 +1810,40 @@ async fn handle_ldk_events(
18141810

18151811
let unlocked_state_copy = unlocked_state.clone();
18161812
let res = tokio::task::spawn_blocking(
1817-
move || -> Result<(String, Option<i32>), String> {
1818-
let res = unlocked_state_copy
1819-
.rgb_send_begin(
1820-
recipient_map,
1821-
true,
1822-
FEE_RATE,
1823-
MIN_CHANNEL_CONFIRMATIONS,
1824-
None,
1825-
false,
1826-
// Final locktime: this colored tx funds an LN channel.
1827-
Some(0),
1828-
)
1829-
.map_err(|e| e.to_string())?;
1830-
let fascia_str = fs::read_to_string(&res.details.fascia_path)
1831-
.map_err(|e| e.to_string())?;
1832-
let fascia: Fascia =
1833-
serde_json::from_str(&fascia_str).map_err(|e| e.to_string())?;
1834-
unlocked_state_copy
1835-
.rgb_consume_fascia(fascia, None)
1836-
.map_err(|e| e.to_string())?;
1837-
unlocked_state_copy
1838-
.rgb_create_consignments(res.psbt.clone())
1839-
.map_err(|e| e.to_string())?;
1813+
move || -> Result<(String, Option<i32>), RgbLibError> {
1814+
let res = unlocked_state_copy.rgb_send_begin(
1815+
recipient_map,
1816+
true,
1817+
FEE_RATE,
1818+
MIN_CHANNEL_CONFIRMATIONS,
1819+
None,
1820+
false,
1821+
// Final locktime: this colored tx funds an LN channel.
1822+
Some(0),
1823+
)?;
1824+
let fascia_str = fs::read_to_string(&res.details.fascia_path).unwrap();
1825+
let fascia: Fascia = serde_json::from_str(&fascia_str).unwrap();
1826+
unlocked_state_copy.rgb_consume_fascia(fascia, None)?;
1827+
unlocked_state_copy.rgb_create_consignments(res.psbt.clone())?;
18401828
Ok((res.psbt, res.batch_transfer_idx))
18411829
},
18421830
)
18431831
.await
18441832
.unwrap();
18451833
let (unsigned_psbt, batch_transfer_idx) = match res {
18461834
Ok(result) => result,
1835+
// A failed funding preparation (e.g. the asset allocation is
1836+
// momentarily reserved by a concurrent open) must fail the
1837+
// channel so the caller can retry, not retry the event
1838+
// forever. handle_open_chan_fail (on ChannelClosed) then
1839+
// releases any reserved allocation.
18471840
Err(e) => {
1848-
tracing::error!("cannot prepare channel funding transfer: {e}");
1849-
return Err(ReplayEvent());
1841+
return handle_funding_prepare_err(
1842+
e,
1843+
&unlocked_state.channel_manager,
1844+
&temporary_channel_id,
1845+
&counterparty_node_id,
1846+
);
18501847
}
18511848
};
18521849
// Record the batch transfer index on the channel's RGB info so a failed
@@ -1866,9 +1863,25 @@ async fn handle_ldk_events(
18661863
}
18671864
(unsigned_psbt, Some(asset_id))
18681865
} else {
1869-
let raw_psbt = unlocked_state
1870-
.rgb_send_btc_begin(addr.to_address(), channel_value_satoshis, FEE_RATE)
1871-
.unwrap();
1866+
// Mirror the colored path: a failed funding preparation must fail
1867+
// the channel (so the caller can retry) rather than panic the event
1868+
// task. handle_funding_prepare_err force-closes on terminal errors
1869+
// and replays the event on transient network errors.
1870+
let raw_psbt = match unlocked_state.rgb_send_btc_begin(
1871+
addr.to_address(),
1872+
channel_value_satoshis,
1873+
FEE_RATE,
1874+
) {
1875+
Ok(psbt) => psbt,
1876+
Err(e) => {
1877+
return handle_funding_prepare_err(
1878+
e,
1879+
&unlocked_state.channel_manager,
1880+
&temporary_channel_id,
1881+
&counterparty_node_id,
1882+
);
1883+
}
1884+
};
18721885
let current_best_height =
18731886
unlocked_state.channel_manager.current_best_block().height;
18741887
let unsigned_psbt =
@@ -1985,7 +1998,6 @@ async fn handle_ldk_events(
19851998
tracing::error!(
19861999
"ERROR: Channel went away before we could fund it. The peer disconnected or refused the channel.",
19872000
);
1988-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
19892001
}
19902002
}
19912003
Event::FundingTxBroadcastSafe { .. } => {
@@ -2577,7 +2589,6 @@ async fn handle_ldk_events(
25772589
.virtual_channel_session_store()
25782590
.contains_key(&channel_id)
25792591
{
2580-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
25812592
tracing::info!(
25822593
"EVENT: virtual channel {} is pending in trusted no-broadcast mode",
25832594
channel_id,
@@ -2611,8 +2622,6 @@ async fn handle_ldk_events(
26112622
})
26122623
.await;
26132624

2614-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
2615-
26162625
let finalize_result = join_result.map_err(|join_err| {
26172626
tracing::error!("Channel opening finalization task failed: {join_err:?}");
26182627
ReplayEvent()
@@ -2696,8 +2705,6 @@ async fn handle_ldk_events(
26962705
reason
26972706
);
26982707

2699-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
2700-
27012708
// Release any funds locked for a funding tx that was never broadcast.
27022709
handle_open_chan_fail(&channel_id, unlocked_state.clone()).await;
27032710

@@ -2729,7 +2736,6 @@ async fn handle_ldk_events(
27292736
&format!("virtual_channel_{}", channel_id),
27302737
false,
27312738
);
2732-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
27332739

27342740
tracing::warn!(
27352741
"EVENT: cleaned up failed virtual open draft {} after channel close {}",
@@ -4706,7 +4712,6 @@ pub(crate) async fn start_ldk(
47064712
taker_swaps,
47074713
router: Arc::clone(&router),
47084714
output_sweeper: Arc::clone(&output_sweeper),
4709-
rgb_send_lock: Arc::new(Mutex::new(false)),
47104715
channel_ids_map,
47114716
proxy_endpoint: proxy_endpoint.to_string(),
47124717
external_signer_mode,

src/routes.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3991,10 +3991,6 @@ pub(crate) async fn open_channel(
39913991
let guard = state.check_unlocked().await?;
39923992
let unlocked_state = guard.as_ref().unwrap();
39933993

3994-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
3995-
return Err(APIError::OpenChannelInProgress);
3996-
}
3997-
39983994
let is_virtual_open = match payload.virtual_open_mode.as_deref() {
39993995
None => false,
40003996
Some(VIRTUAL_OPEN_MODE_TRUSTED_NO_BROADCAST) => true,
@@ -4286,14 +4282,6 @@ pub(crate) async fn open_channel(
42864282
(temporary_channel_id, None)
42874283
};
42884284

4289-
// Only colored opens perform an RGB send during funding, so only they need
4290-
// the RGB send lock. Vanilla opens that stall (e.g. an unresponsive peer)
4291-
// must not hold it, or they would block all subsequent opens indefinitely.
4292-
if colored_info.is_some() {
4293-
*unlocked_state.rgb_send_lock.lock().unwrap() = true;
4294-
tracing::debug!("RGB send lock set to true");
4295-
}
4296-
42974285
let temporary_channel_id = unlocked_state
42984286
.channel_manager
42994287
.create_channel(
@@ -4307,8 +4295,6 @@ pub(crate) async fn open_channel(
43074295
payload.push_asset_amount,
43084296
)
43094297
.map_err(|e| {
4310-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
4311-
tracing::debug!("RGB send lock set to false (open channel failure: {e:?})");
43124298
if let Some(temp_id_str) = rgb_metadata_temp_id_str.as_deref() {
43134299
let _ = unlocked_state
43144300
.kv_store

src/sdk/mod.rs

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,10 +1647,6 @@ pub(crate) async fn send_rgb(
16471647
let guard = check_unlocked(&state).await?;
16481648
let unlocked_state = guard.as_ref().unwrap();
16491649

1650-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
1651-
return Err(APIError::OpenChannelInProgress);
1652-
}
1653-
16541650
let send_result = if unlocked_state.external_signer_mode {
16551651
let unlocked_state_copy = unlocked_state.clone();
16561652
let begin_result = tokio::task::spawn_blocking(move || {
@@ -2364,10 +2360,6 @@ pub(crate) async fn issue_asset_nia(
23642360
));
23652361
}
23662362

2367-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
2368-
return Err(APIError::OpenChannelInProgress);
2369-
}
2370-
23712363
let asset = unlocked_state.rgb_issue_asset_nia(
23722364
request.ticker,
23732365
request.name,
@@ -2390,10 +2382,6 @@ pub(crate) async fn issue_asset_cfa(
23902382
));
23912383
}
23922384

2393-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
2394-
return Err(APIError::OpenChannelInProgress);
2395-
}
2396-
23972385
let file_path = request.file_digest.map(|d| {
23982386
unlocked_state
23992387
.rgb_get_media_dir()
@@ -2425,10 +2413,6 @@ pub(crate) async fn issue_asset_ifa(
24252413
));
24262414
}
24272415

2428-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
2429-
return Err(APIError::OpenChannelInProgress);
2430-
}
2431-
24322416
let asset = unlocked_state.rgb_issue_asset_ifa(
24332417
request.ticker,
24342418
request.name,
@@ -2453,10 +2437,6 @@ pub(crate) async fn issue_asset_uda(
24532437
));
24542438
}
24552439

2456-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
2457-
return Err(APIError::OpenChannelInProgress);
2458-
}
2459-
24602440
let rgb_media_dir = unlocked_state.rgb_get_media_dir();
24612441
let get_string_path = |d: String| {
24622442
rgb_media_dir
@@ -2654,10 +2634,6 @@ pub(crate) async fn rgb_invoice(
26542634
let guard = check_unlocked(&state).await?;
26552635
let unlocked_state = guard.as_ref().unwrap();
26562636

2657-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
2658-
return Err(APIError::OpenChannelInProgress);
2659-
}
2660-
26612637
let assignment = match request.assignment_kind {
26622638
Some(kind) => rgb_assignment_from_kind(kind, request.assignment_amount)?,
26632639
None => RgbLibAssignment::Any,
@@ -2699,10 +2675,6 @@ pub(crate) async fn open_channel(
26992675
let guard = check_unlocked(&state).await?;
27002676
let unlocked_state = guard.as_ref().unwrap();
27012677

2702-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
2703-
return Err(APIError::OpenChannelInProgress);
2704-
}
2705-
27062678
let is_virtual_open = match request.virtual_open_mode.as_deref() {
27072679
None => false,
27082680
Some(SDK_VIRTUAL_OPEN_MODE_TRUSTED_NO_BROADCAST) => true,
@@ -2973,13 +2945,6 @@ pub(crate) async fn open_channel(
29732945
(temporary_channel_id, None)
29742946
};
29752947

2976-
// Only colored opens perform an RGB send during funding, so only they need the
2977-
// RGB send lock. Vanilla opens that stall must not hold it (see routes.rs).
2978-
if colored_info.is_some() {
2979-
*unlocked_state.rgb_send_lock.lock().unwrap() = true;
2980-
tracing::debug!("RGB send lock set to true");
2981-
}
2982-
29832948
let temporary_channel_id = unlocked_state
29842949
.channel_manager
29852950
.create_channel(
@@ -2993,8 +2958,6 @@ pub(crate) async fn open_channel(
29932958
request.push_asset_amount,
29942959
)
29952960
.map_err(|e| {
2996-
*unlocked_state.rgb_send_lock.lock().unwrap() = false;
2997-
tracing::debug!("RGB send lock set to false (open channel failure: {e:?})");
29982961
if let Some(temp_id_str) = rgb_metadata_temp_id_str.as_deref() {
29992962
let _ = unlocked_state
30002963
.kv_store
@@ -4058,10 +4021,6 @@ pub(crate) async fn inflate(
40584021
));
40594022
}
40604023

4061-
if *unlocked_state.rgb_send_lock.lock().unwrap() {
4062-
return Err(APIError::OpenChannelInProgress);
4063-
}
4064-
40654024
let unlocked_state_copy = unlocked_state.clone();
40664025
let inflate_result = tokio::task::spawn_blocking(move || {
40674026
unlocked_state_copy.rgb_inflate(

src/test/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1709,7 +1709,13 @@ async fn open_channel_raw(
17091709
}
17101710
}
17111711
if (OffsetDateTime::now_utc() - t_0).as_seconds_f32() > 50.0 {
1712-
panic!("cannot find funding TX")
1712+
// The channel may have been force-closed before reaching
1713+
// ChannelPending (e.g. a colored funding preparation failed because
1714+
// the asset allocation was momentarily reserved by a concurrent
1715+
// open). Surface a retryable error so open_channel_with_retry tries
1716+
// again once the contending open has settled.
1717+
println!("cannot find funding TX for channel to {dest_peer_pubkey}");
1718+
return Err(reqwest::StatusCode::FORBIDDEN);
17131719
}
17141720
}
17151721
let channel_id = channel_id.unwrap();

src/test/openchannel_fail.rs

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -580,34 +580,7 @@ async fn openchannel_fail() {
580580
.await
581581
.unwrap();
582582
assert!(res.status() == reqwest::StatusCode::OK);
583-
// open a 2nd channel while the previous open is still in progess (fail)
584-
let payload = OpenChannelRequest {
585-
peer_pubkey_and_opt_addr: format!("{node2_pubkey}@127.0.0.1:{NODE2_PEER_PORT}"),
586-
capacity_sat: 100_000,
587-
push_msat: 3_500_000,
588-
asset_amount: Some(100),
589-
asset_id: Some(asset_id),
590-
push_asset_amount: None,
591-
public: true,
592-
with_anchors: true,
593-
fee_base_msat: None,
594-
fee_proportional_millionths: None,
595-
temporary_channel_id: None,
596-
virtual_open_mode: None,
597-
};
598-
let res = reqwest::Client::new()
599-
.post(format!("http://{node1_addr}/openchannel"))
600-
.json(&payload)
601-
.send()
602-
.await
603-
.unwrap();
604-
check_response_is_nok(
605-
res,
606-
reqwest::StatusCode::FORBIDDEN,
607-
"Cannot perform this operation while an open channel operation is in progress",
608-
"OpenChannelInProgress",
609-
)
610-
.await;
583+
let _ = asset_id;
611584

612585
let t_0 = OffsetDateTime::now_utc();
613586
loop {

0 commit comments

Comments
 (0)