-
Notifications
You must be signed in to change notification settings - Fork 411
Rescan dependent transactions in ChainMonitor #840
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
Changes from all commits
d70fdd3
02b85fa
6b14ade
80e81fb
d8d9eaf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,7 @@ | |
use bitcoin::blockdata::block::{Block, BlockHeader}; | ||
|
||
use chain; | ||
use chain::Filter; | ||
use chain::{Filter, WatchedOutput}; | ||
use chain::chaininterface::{BroadcasterInterface, FeeEstimator}; | ||
use chain::channelmonitor; | ||
use chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateErr, MonitorEvent, Persist}; | ||
|
@@ -82,18 +82,40 @@ where C::Target: chain::Filter, | |
/// descendants of such transactions. It is not necessary to re-fetch the block to obtain | ||
/// updated `txdata`. | ||
pub fn block_connected(&self, header: &BlockHeader, txdata: &TransactionData, height: u32) { | ||
let mut dependent_txdata = Vec::new(); | ||
let monitors = self.monitors.read().unwrap(); | ||
for monitor in monitors.values() { | ||
let mut txn_outputs = monitor.block_connected(header, txdata, height, &*self.broadcaster, &*self.fee_estimator, &*self.logger); | ||
|
||
// Register any new outputs with the chain source for filtering, storing any dependent | ||
// transactions from within the block that previously had not been included in txdata. | ||
if let Some(ref chain_source) = self.chain_source { | ||
let block_hash = header.block_hash(); | ||
for (txid, outputs) in txn_outputs.drain(..) { | ||
for (idx, output) in outputs.iter() { | ||
chain_source.register_output(&OutPoint { txid, index: *idx as u16 }, &output.script_pubkey); | ||
// Register any new outputs with the chain source for filtering and recurse | ||
// if it indicates that there are dependent transactions within the block | ||
// that had not been previously included in txdata. | ||
let output = WatchedOutput { | ||
block_hash: Some(block_hash), | ||
outpoint: OutPoint { txid, index: *idx as u16 }, | ||
script_pubkey: output.script_pubkey.clone(), | ||
}; | ||
if let Some(tx) = chain_source.register_output(output) { | ||
dependent_txdata.push(tx); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Recursively call for any dependent transactions that were identified by the chain source. | ||
if !dependent_txdata.is_empty() { | ||
dependent_txdata.sort_unstable_by_key(|(index, _tx)| *index); | ||
dependent_txdata.dedup_by_key(|(index, _tx)| *index); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, From where duplication could have been introduced ? Though I agree to keep it as belt-and-suspender. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow. Could you explain why it will not do so? If fact, doesn't my test demonstrate that it does? (i.e., a commitment transaction is processed and its outputs are registered)
The duplication may occur across channel monitors. For instance, two channel monitors register two different outputs which happen to be spent by the same in-block descendent transaction. So each call to Would this ever happen in practice? Maybe not? But it is not an impossible scenario, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I meant a channel monitor won't ask to register outputs spent by the processed commitment transactions, only outputs for the next stage of spending ? Those outputs spent are already registered, otherwise we won't have received a transaction spending them.
I don't believe this can happen right now because we don't coalesce justice transactions across channels. Although thought about doing it for fee optimizations a while back but not worthy the complexity. Beyond, I'm also pretty sure that dual-funding/splicing will enable that kind of scenario where channel monitors register different outputs which happen to be spent by the same in-block descendent transaction. A splicing transaction could lawfully spend two funding outpoints. AFAICT, I think those scenarios aren't possible for now, but really likely in the future so better to keep the dedup ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, right. Sorry, I misread your message.
Gotcha, will keep as is. Though note that these transactions are published by the counterparty, which may be another implementation. For a de-dup test, I suppose if I set up two channels then I may be able to form a transaction that spends from both commitment transaction's outputs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Actually, I think one channel would be sufficient here. A transaction spending the commitment transaction's Is there an easy way to form such a transaction using our API? |
||
let txdata: Vec<_> = dependent_txdata.iter().map(|(index, tx)| (*index, tx)).collect(); | ||
self.block_connected(header, &txdata, height); | ||
jkczyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/// Dispatches to per-channel monitors, which are responsible for updating their on-chain view | ||
|
@@ -245,3 +267,56 @@ impl<ChannelSigner: Sign, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref> even | |
pending_events | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use ::{check_added_monitors, get_local_commitment_txn}; | ||
use ln::features::InitFeatures; | ||
use ln::functional_test_utils::*; | ||
use util::events::EventsProvider; | ||
use util::events::MessageSendEventsProvider; | ||
use util::test_utils::{OnRegisterOutput, TxOutReference}; | ||
|
||
/// Tests that in-block dependent transactions are processed by `block_connected` when not | ||
/// included in `txdata` but returned by [`chain::Filter::register_output`]. For instance, | ||
/// a (non-anchor) commitment transaction's HTLC output may be spent in the same block as the | ||
/// commitment transaction itself. An Electrum client may filter the commitment transaction but | ||
/// needs to return the HTLC transaction so it can be processed. | ||
#[test] | ||
fn connect_block_checks_dependent_transactions() { | ||
let chanmon_cfgs = create_chanmon_cfgs(2); | ||
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); | ||
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); | ||
let nodes = create_network(2, &node_cfgs, &node_chanmgrs); | ||
let channel = create_announced_chan_between_nodes( | ||
&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()); | ||
|
||
// Send a payment, saving nodes[0]'s revoked commitment and HTLC-Timeout transactions. | ||
let (commitment_tx, htlc_tx) = { | ||
let payment_preimage = route_payment(&nodes[0], &vec!(&nodes[1])[..], 5_000_000).0; | ||
let mut txn = get_local_commitment_txn!(nodes[0], channel.2); | ||
claim_payment(&nodes[0], &vec!(&nodes[1])[..], payment_preimage, 5_000_000); | ||
|
||
assert_eq!(txn.len(), 2); | ||
(txn.remove(0), txn.remove(0)) | ||
}; | ||
|
||
// Set expectations on nodes[1]'s chain source to return dependent transactions. | ||
let htlc_output = TxOutReference(commitment_tx.clone(), 0); | ||
let to_local_output = TxOutReference(commitment_tx.clone(), 1); | ||
let htlc_timeout_output = TxOutReference(htlc_tx.clone(), 0); | ||
nodes[1].chain_source | ||
.expect(OnRegisterOutput { with: htlc_output, returns: Some((1, htlc_tx)) }) | ||
.expect(OnRegisterOutput { with: to_local_output, returns: None }) | ||
.expect(OnRegisterOutput { with: htlc_timeout_output, returns: None }); | ||
|
||
// Notify nodes[1] that nodes[0]'s revoked commitment transaction was mined. The chain | ||
// source should return the dependent HTLC transaction when the HTLC output is registered. | ||
mine_transaction(&nodes[1], &commitment_tx); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I double checked that "expectations" are registered and then cleared before and after this tx is mined |
||
|
||
// Clean up so uninteresting assertions don't fail. | ||
check_added_monitors!(nodes[1], 1); | ||
nodes[1].node.get_and_clear_pending_msg_events(); | ||
nodes[1].node.get_and_clear_pending_events(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ | |
|
||
use bitcoin::blockdata::block::{Block, BlockHeader}; | ||
use bitcoin::blockdata::script::Script; | ||
use bitcoin::blockdata::transaction::TxOut; | ||
use bitcoin::blockdata::transaction::{Transaction, TxOut}; | ||
use bitcoin::hash_types::{BlockHash, Txid}; | ||
|
||
use chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateErr, MonitorEvent}; | ||
|
@@ -129,9 +129,38 @@ pub trait Filter: Send + Sync { | |
/// a spending condition. | ||
fn register_tx(&self, txid: &Txid, script_pubkey: &Script); | ||
|
||
/// Registers interest in spends of a transaction output identified by `outpoint` having | ||
/// `script_pubkey` as the spending condition. | ||
fn register_output(&self, outpoint: &OutPoint, script_pubkey: &Script); | ||
/// Registers interest in spends of a transaction output. | ||
/// | ||
/// Optionally, when `output.block_hash` is set, should return any transaction spending the | ||
/// output that is found in the corresponding block along with its index. | ||
/// | ||
/// This return value is useful for Electrum clients in order to supply in-block descendant | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand how the Electrum client is leveraging block hash presence. Do you assume it stores the in-block descendant, even before Otherwise, I don't think you need block hash to register already-confirmed scriptpubkeys to an Electrum server API, see https://electrumx.readthedocs.io/en/latest/protocol-methods.html There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The purpose of the
It should return |
||
/// transactions which otherwise were not included. This is not necessary for other clients if | ||
/// such descendant transactions were already included (e.g., when a BIP 157 client provides the | ||
/// full block). | ||
fn register_output(&self, output: WatchedOutput) -> Option<(usize, Transaction)>; | ||
} | ||
|
||
/// A transaction output watched by a [`ChannelMonitor`] for spends on-chain. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the persistent confusion all of us seem to have, would be good if this mentioned "spends" more prominently, or maybe explicitly - "Indicates any transaction spending the outpoint described here..." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it is a bit vague. Rewrote to be more explicit on how this is used. |
||
/// | ||
/// Used to convey to a [`Filter`] such an output with a given spending condition. Any transaction | ||
/// spending the output must be given to [`ChannelMonitor::block_connected`] either directly or via | ||
/// the return value of [`Filter::register_output`]. | ||
/// | ||
/// If `block_hash` is `Some`, this indicates the output was created in the corresponding block and | ||
/// may have been spent there. See [`Filter::register_output`] for details. | ||
/// | ||
/// [`ChannelMonitor`]: channelmonitor::ChannelMonitor | ||
/// [`ChannelMonitor::block_connected`]: channelmonitor::ChannelMonitor::block_connected | ||
pub struct WatchedOutput { | ||
/// First block where the transaction output may have been spent. | ||
pub block_hash: Option<BlockHash>, | ||
|
||
/// Outpoint identifying the transaction output. | ||
pub outpoint: OutPoint, | ||
|
||
/// Spending condition of the transaction output. | ||
pub script_pubkey: Script, | ||
} | ||
|
||
impl<T: Listen> Listen for std::ops::Deref<Target = T> { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where I've a doubt about the operations of an Electrum client. At this point, if you don't have locally the in-block descendants what should you do ?
register_output
caller ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Electrum client needs to query the server for dependent transactions synchronously and block until it gets a response back. Otherwise, the user would need to call
block_connected
again for the same header, which we'd like to avoid.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, option c) emit new queries and block until it gets a response back. I think this query-and-block requirement should be documented around
chain::Filter::register_output
?Also, I think documentation should note that query to a trusted-but-not-too-much server should be on a time-out, otherwise that's a vector to stale operations of an Electrum client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... currently the return value is
Option<Transaction>
, so there's no mechanism to timeout. What do you propose should happen in that scenario?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest to return a None, but I think the timeout should be implemented by the Electrum local client, not by us. We should just make this point clear in
chainFilter
implementation documentation imo.I think <30s is enough to consider the output spent isn't known by the Electrum server and that we should return.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that is shouldn't be us. But returning
None
doesn't allow us to differentiate between "Electrum doesn't know" and "Electrum is unresponsive". What should the user do in case of the latter?