Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: GatewayLdkClient::info() doesn't rely on esplora #6022

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 53 additions & 15 deletions devimint/src/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Duration;

use anyhow::{anyhow, bail, Context, Result};
use bitcoincore_rpc::bitcoin::{Address, BlockHash};
use bitcoincore_rpc::bitcoin::{Address, Block, BlockHash};
use bitcoincore_rpc::bitcoincore_rpc_json::{GetBalancesResult, GetBlockchainInfoResult};
use bitcoincore_rpc::{bitcoin, RpcApi};
use cln_rpc::primitives::{Amount as ClnRpcAmount, AmountOrAny};
Expand All @@ -26,9 +26,7 @@ use tonic_lnd::lnrpc::{ChanInfoRequest, GetInfoRequest, ListChannelsRequest};
use tonic_lnd::Client as LndClient;
use tracing::{debug, info, trace, warn};

use crate::util::{
poll, poll_with_timeout, ClnLightningCli, GatewayClnExtension, ProcessHandle, ProcessManager,
};
use crate::util::{poll, ClnLightningCli, GatewayClnExtension, ProcessHandle, ProcessManager};
use crate::vars::utf8;
use crate::version_constants::VERSION_0_4_0_ALPHA;
use crate::{cmd, poll_eq, Gatewayd};
Expand Down Expand Up @@ -234,6 +232,34 @@ impl Bitcoind {
Ok(proof.encode_hex())
}

pub fn transaction_is_known(&self, txid: &bitcoin::Txid) -> Result<bool> {
if self.get_raw_transaction(txid).is_ok() {
return Ok(true);
}

let block_height = self.get_block_count()? - 1;

for i in 0..block_height {
let block_hash = self.get_block_hash(i)?;
let block = self.get_block(&block_hash)?;
for tx in block.txdata {
if tx.txid() == *txid {
return Ok(true);
}
}
}

Ok(false)
}

fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
Ok(block_in_place(|| self.client.get_block_hash(height))?)
}

fn get_block(&self, hash: &BlockHash) -> Result<Block> {
Ok(block_in_place(|| self.client.get_block(hash))?)
}

pub fn get_raw_transaction(&self, txid: &bitcoin::Txid) -> Result<String> {
let tx = block_in_place(|| self.client.get_raw_transaction(txid, None))?;
let bytes = tx.consensus_encode_to_vec();
Expand Down Expand Up @@ -869,23 +895,25 @@ pub async fn open_channels_between_gateways(
bitcoind: &Bitcoind,
gateways: &[NamedGateway<'_>],
) -> Result<()> {
debug!(target: LOG_DEVIMINT, "Syncing gateway lightning nodes to chain tip...");
bitcoind.mine_blocks(20).await?;

info!(target: LOG_DEVIMINT, "Syncing gateway lightning nodes to chain tip...");
futures::future::try_join_all(
gateways
.iter()
.map(|(gw, _gw_name)| gw.wait_for_chain_sync(bitcoind)),
)
.await?;

debug!(target: LOG_DEVIMINT, "Funding all gateway lightning nodes...");
info!(target: LOG_DEVIMINT, "Funding all gateway lightning nodes...");
for (gw, _gw_name) in gateways {
let funding_addr = gw.get_ln_onchain_address().await?;
bitcoind.send_to(funding_addr, 100_000_000).await?;
}

bitcoind.mine_blocks(10).await?;
bitcoind.mine_blocks(20).await?;

debug!(target: LOG_DEVIMINT, "Syncing gateway lightning nodes to chain tip...");
info!(target: LOG_DEVIMINT, "Syncing gateway lightning nodes to chain tip...");
futures::future::try_join_all(
gateways
.iter()
Expand All @@ -911,11 +939,17 @@ pub async fn open_channels_between_gateways(

let push_amount = 5_000_000;
info!(target: LOG_DEVIMINT, "Opening channel between {gw_a_name} and {gw_b_name} gateway lightning nodes with {push_amount} on each side...");
let gw_a_name = (*gw_a_name).to_string();
let gw_b_name = (*gw_b_name).to_string();
tokio::task::spawn(async move {
// Sometimes channel openings just after funding the lightning nodes don't work right away.
// This resolves itself after a few seconds, so we don't need to poll for very long.
poll_with_timeout("Open channel", Duration::from_secs(10), || async {
gw_a.open_channel(&gw_b, 10_000_000, Some(push_amount)).await.map_err(ControlFlow::Continue)
poll("Open channel", || async {
let gw_a_name = gw_a_name.clone();
let gw_b_name = gw_b_name.clone();
let txid = gw_a.open_channel(&gw_b, 10_000_000, Some(push_amount)).await.map_err(ControlFlow::Continue)?;
info!(target: LOG_DEVIMINT, "Opened channel between {gw_a_name} and {gw_b_name} with txid: {txid}");
Ok((gw_a_name, gw_b_name, txid))
})
.await
})
Expand All @@ -940,35 +974,39 @@ pub async fn open_channels_between_gateways(
}

// Wait for all channel funding transaction to be known by bitcoind.
for txid in &channel_funding_txids {
for (gw_a_name, gw_b_name, txid) in &channel_funding_txids {
loop {
info!(target: LOG_DEVIMINT, "Checking if channel funding transaction between {gw_a_name} and {gw_b_name} is known by bitcoind...");

// Bitcoind's getrawtransaction RPC call will return an error if the transaction
// is not known.
if bitcoind.get_raw_transaction(txid).is_ok() {
if bitcoind.transaction_is_known(txid).is_ok() {
break;
}

// Wait for a bit, then restart the check.
fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
}
info!(target: LOG_DEVIMINT, "Channel funding transaction between {gw_a_name} and {gw_b_name} is known by bitcoind");
}

bitcoind.mine_blocks(10).await?;
bitcoind.mine_blocks(20).await?;

debug!(target: LOG_DEVIMINT, "Syncing gateway lightning nodes to chain tip...");
info!(target: LOG_DEVIMINT, "Syncing gateway lightning nodes to chain tip...");
futures::future::try_join_all(
gateways
.iter()
.map(|(gw, _gw_name)| gw.wait_for_chain_sync(bitcoind)),
)
.await?;

for ((gw_a, _gw_a_name), (gw_b, _gw_b_name)) in &gateway_pairs {
for ((gw_a, gw_a_name), (gw_b, gw_b_name)) in &gateway_pairs {
let gw_a_node_pubkey = gw_a.lightning_pubkey().await?;
let gw_b_node_pubkey = gw_b.lightning_pubkey().await?;

wait_for_ready_channel_on_gateway_with_counterparty(gw_b, gw_a_node_pubkey).await?;
wait_for_ready_channel_on_gateway_with_counterparty(gw_a, gw_b_node_pubkey).await?;
info!(target: LOG_DEVIMINT, "Channels between {gw_a_name} and {gw_b_name} are ready");
}

Ok(())
Expand Down
52 changes: 17 additions & 35 deletions gateway/ln-gateway/src/lightning/ldk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ impl GatewayLdkClient {
addr: [0, 0, 0, 0],
port: lightning_port,
}]),
// TODO: Remove these and rely on the default values.
// See here for details: https://github.com/lightningdevkit/ldk-node/issues/339#issuecomment-2344230472
onchain_wallet_sync_interval_secs: 10,
wallet_sync_interval_secs: 10,
..Default::default()
});
node_builder.set_entropy_bip39_mnemonic(mnemonic, None);
Expand Down Expand Up @@ -197,41 +193,13 @@ impl Drop for GatewayLdkClient {
impl ILnRpcClient for GatewayLdkClient {
async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
let node_status = self.node.status();

let Some(chain_tip_block_summary) = self
.esplora_client
.get_blocks(None)
.await
.map_err(|e| LightningRpcError::FailedToGetNodeInfo {
failure_reason: format!("Failed get chain tip block summary: {e:?}"),
})?
.into_iter()
.next()
else {
return Err(LightningRpcError::FailedToGetNodeInfo {
failure_reason:
"Failed to get chain tip block summary (empty block list was returned)"
.to_string(),
});
};

let esplora_chain_tip_timestamp = chain_tip_block_summary.time.timestamp;
let block_height: u32 = chain_tip_block_summary.time.height;

let synced_to_chain = node_status.latest_wallet_sync_timestamp.unwrap_or_default()
> esplora_chain_tip_timestamp
&& node_status
.latest_onchain_wallet_sync_timestamp
.unwrap_or_default()
> esplora_chain_tip_timestamp;

Ok(GetNodeInfoResponse {
pub_key: self.node.node_id().serialize().to_vec(),
// TODO: This is a placeholder. We need to get the actual alias from the LDK node.
alias: format!("LDK Fedimint Gateway Node {}", self.node.node_id()),
network: self.node.config().network.to_string(),
block_height,
synced_to_chain,
block_height: node_status.current_best_block.height,
synced_to_chain: node_status.is_running && node_status.is_listening,
})
}

Expand Down Expand Up @@ -426,6 +394,20 @@ impl ILnRpcClient for GatewayLdkClient {
channel_size_sats: u64,
push_amount_sats: u64,
) -> Result<OpenChannelResponse, LightningRpcError> {
let funding_txid_or = self
.node
.list_channels()
.iter()
.find(|channel| channel.counterparty_node_id == pubkey)
.and_then(|channel| channel.funding_txo)
.map(|funding_txo| funding_txo.txid);

if let Some(funding_txid) = funding_txid_or {
return Ok(OpenChannelResponse {
funding_txid: funding_txid.to_string(),
});
}

let push_amount_msats_or = if push_amount_sats == 0 {
None
} else {
Expand All @@ -451,7 +433,7 @@ impl ILnRpcClient for GatewayLdkClient {
})?;

// The channel isn't always visible immediately, so we need to poll for it.
for _ in 0..10 {
for _ in 0..100 {
let funding_txid_or = self
.node
.list_channels()
Expand Down
7 changes: 2 additions & 5 deletions gateway/ln-gateway/src/lightning/lnd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,7 @@ impl ILnRpcClient for GatewayLndClient {
let mut client = self.connect().await?;

// Connect to the peer first
client
let _ = client
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this being swallowed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still trying to figure out why CI is failing, this was one of the things I tweaked that I thought might fix it. The intention here was to make this function idempotent, since we retry channel opens in devimint:

// Sometimes channel openings just after funding the lightning nodes don't work right away.
// This resolves itself after a few seconds, so we don't need to poll for very long.
poll_with_timeout("Open channel", Duration::from_secs(10), || async {
gw_a.open_channel(&gw_b, 10_000_000, Some(push_amount)).await.map_err(ControlFlow::Continue)
})

I believe calling ConnectPeer on a peer that's already connected returns an error.

.lightning()
.connect_peer(ConnectPeerRequest {
addr: Some(LightningAddress {
Expand All @@ -1144,10 +1144,7 @@ impl ILnRpcClient for GatewayLndClient {
perm: false,
timeout: 10,
})
.await
.map_err(|e| LightningRpcError::FailedToConnectToPeer {
failure_reason: format!("Failed to connect to peer {e:?}"),
})?;
.await;

// Open the channel
match client
Expand Down
Loading