Skip to content

add watchtower querier trait#1153

Closed
doitian wants to merge 8 commits intonervosnetwork:developfrom
doitian:add-watchtower-querier-trait
Closed

add watchtower querier trait#1153
doitian wants to merge 8 commits intonervosnetwork:developfrom
doitian:add-watchtower-querier-trait

Conversation

@doitian
Copy link
Copy Markdown
Member

@doitian doitian commented Feb 28, 2026

feat: implement watchtower querier trait and integrate with network actor

Allow network/channel actor to query tlc status from watchtower no matter it is in the same process or runs as a standalone service.

Refs #1077

@doitian doitian force-pushed the add-watchtower-querier-trait branch from fc28803 to 01cae25 Compare February 28, 2026 08:58
Comment thread crates/fiber-lib/src/fiber/network.rs Outdated
@doitian doitian marked this pull request as draft March 2, 2026 04:41
@doitian doitian marked this pull request as ready for review March 2, 2026 08:18
@doitian doitian force-pushed the add-watchtower-querier-trait branch from 0f4d9b6 to 5af3805 Compare March 2, 2026 09:16
@doitian doitian requested a review from jjyr March 3, 2026 03:33
@doitian doitian force-pushed the add-watchtower-querier-trait branch 3 times, most recently from 71cad1a to 7c7d65f Compare March 10, 2026 05:18
@doitian
Copy link
Copy Markdown
Member Author

doitian commented Mar 11, 2026

Need to add e2e test

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements a watchtower query abstraction so the network/channel flows can check TLC settlement/preimage status via either the built-in watchtower store or a standalone watchtower RPC, and adds coverage via new unit/e2e tests.

Changes:

  • Add WatchtowerQuerier + query actor to asynchronously fetch TLC status (preimage + settled) and integrate results into NetworkActor.
  • Expose new watchtower RPC method query_tlc_status (types, server, auth rule, docs).
  • Add a new Bruno e2e scenario for “force-close preimage settled by recipient” and add unit tests for the querier integration.

Reviewed changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/01-node1-connect-node2.bru New e2e step: connect peers (N1↔N2).
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/02-node2-connect-node3.bru New e2e step: connect peers (N2↔N3).
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/03-node1-node2-open-channel.bru New e2e step: open N1→N2 channel.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/04-node2-get-auto-accepted-channel.bru New e2e step: fetch N1-N2 channel id.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/05-ckb-generate-blocks.bru New e2e step: mine epochs for channel confirmation.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/06-node2-node3-open-channel.bru New e2e step: open N2→N3 channel.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/07-node3-get-auto-accepted-channel.bru New e2e step: fetch N2-N3 channel id.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/08-ckb-generate-blocks.bru New e2e step: mine epochs for channel confirmation.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/09-get-node1-funding-script.bru New e2e step: capture node1 default funding lock script.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/10-get-node3-funding-script.bru New e2e step: capture node3 default funding lock script.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/11-get-node1-balance.bru New e2e step: record node1 on-chain balance.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/12-get-node3-balance.bru New e2e step: record node3 on-chain balance.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/13-node3-gen-invoice.bru New e2e step: generate hold invoice + payment hash/preimage vars.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/14-node1-send-payment-with-invoice.bru New e2e step: send payment using invoice.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/15-node2-force-close-channel.bru New e2e step: force-close N2-N3 channel.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/16-node2-disconnect-node3.bru New e2e step: disconnect N2 from N3.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/17-ckb-generate-blocks-for-force-close-tx.bru New e2e step: mine epoch for force-close tx.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/18-node3-remove-tlc.bru New e2e step: node3 reveals preimage on-chain (remove_tlc).
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/19-ckb-generate-blocks-for-settlement-tx-preimage.bru New e2e step: mine epochs for settlement tx (preimage path).
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/20-ckb-generate-blocks-for-final-settlement-tx.bru New e2e step: mine epochs for final settlement.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/21-ckb-generate-blocks-for-final-settlement-tx-commit.bru New e2e step: mine epoch for final settlement commit.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/22-check-channel1-balance.bru New e2e assertion: verify intermediary balance increased.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/23-check-payment-status.bru New e2e assertion: payment status is Success.
tests/bruno/e2e/watchtower/force-close-preimage-settled-by-recipient/24-disconnect.bru New e2e step: disconnect peers after scenario.
docs/biscuit-auth.md Document Biscuit auth rule for query_tlc_status.
crates/fiber-wasm/src/lib.rs Wire optional watchtower querier into WASM network startup.
crates/fiber-types/src/schema.rs Gate WATCHTOWER_TLC_SETTLED_PREFIX behind watchtower feature.
crates/fiber-lib/src/watchtower/store.rs Extend watchtower store with TLC status query helpers.
crates/fiber-lib/src/tests/test_utils.rs Allow injecting a watchtower querier into test NetworkNode config.
crates/fiber-lib/src/store/store_impl/mod.rs Move TLC-settled lookup into WatchtowerStore; implement WatchtowerQuerier for Store.
crates/fiber-lib/src/rpc/watchtower.rs Add query_tlc_status RPC + client-side querier wrapper.
crates/fiber-lib/src/rpc/biscuit.rs Add Biscuit auth rule for query_tlc_status.
crates/fiber-lib/src/rpc/README.md Add method docs for query_tlc_status.
crates/fiber-lib/src/fiber/watchtower_query_actor.rs New actor to perform watchtower queries off the network actor hot path.
crates/fiber-lib/src/fiber/watchtower_query.rs New WatchtowerQuerier trait + status struct.
crates/fiber-lib/src/fiber/tests/watchtower_query.rs New unit tests covering payment success via watchtower-provided preimage.
crates/fiber-lib/src/fiber/tests/settle_tlc_set_command_tests.rs Update mock store for removed is_tlc_settled channel-store API.
crates/fiber-lib/src/fiber/tests/mod.rs Register new watchtower_query tests (non-wasm).
crates/fiber-lib/src/fiber/network.rs Integrate watchtower querying into CheckChannels + handle results via new commands.
crates/fiber-lib/src/fiber/mod.rs Export watchtower query types; include new actor module.
crates/fiber-lib/src/fiber/channel.rs Remove is_tlc_settled from ChannelActorStateStore trait.
crates/fiber-json-types/src/watchtower.rs Add JSON-RPC types for query_tlc_status.
crates/fiber-bin/src/main.rs Construct watchtower querier (prefer store; fallback to standalone watchtower RPC) and pass to start_network.
.github/workflows/e2e.yml Add the new watchtower e2e scenario to CI matrix.

Comment on lines +1651 to +1669
self.store.insert_preimage(payment_hash, preimage);
debug!(
"Found watchtower preimage for channel {:?} tlc {:?}",
channel_id, tlc_id
);
if self
.store
.get_invoice_status(&payment_hash)
.is_some_and(|s| {
!matches!(s, CkbInvoiceStatus::Open | CkbInvoiceStatus::Received)
})
{
return Ok(());
}
// Only send RemoveTlc if the channel still has this received TLC (it may have
// been removed already, e.g. by a concurrent payment winning the race).
let tlc_still_exists =
self.store
.get_channel_actor_state(&channel_id)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

When a watchtower-sourced preimage arrives, it’s inserted into the node’s preimage store before any validation. If the watchtower is misconfigured/malicious and returns a wrong preimage, the node will stop querying (because get_preimage becomes Some) and repeatedly fail TLC settlement with FinalIncorrectPreimage. Recommend validating preimage against the TLC’s hash_algorithm/payment_hash (and only persisting it if it matches), and ideally only persisting if the received TLC still exists.

Copilot uses AI. Check for mistakes.
Comment on lines +1399 to +1405
let _ = wt_actor.send_message(
WatchtowerQueryActorMessage::QueryPreimage {
channel_id,
tlc_id: tlc_id.into(),
payment_hash,
},
);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This send ignores any error from watchtower_query_actor (e.g., mailbox closed/full), which can silently prevent TLC settlement retries via watchtower. Consider handling the send_message result (at least log at debug/warn) so failures are observable.

Suggested change
let _ = wt_actor.send_message(
WatchtowerQueryActorMessage::QueryPreimage {
channel_id,
tlc_id: tlc_id.into(),
payment_hash,
},
);
if let Err(err) = wt_actor.send_message(
WatchtowerQueryActorMessage::QueryPreimage {
channel_id,
tlc_id: tlc_id.into(),
payment_hash,
},
) {
warn!(
"Failed to send QueryPreimage to watchtower for channel {:?}, tlc {:?}, payment_hash {:?}: {}",
channel_id,
tlc_id,
payment_hash,
err
);
}

Copilot uses AI. Check for mistakes.
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
pub struct QueryTlcStatusResult {
/// Found preimage when the TLC has been settled on chain with the preimage path.
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

preimage is documented as only being present when the TLC has been settled on-chain with the preimage path, but the implementation returns whatever preimage the watchtower knows (e.g., inserted via create_preimage), independent of is_settled. Please adjust the wording to match actual behavior (e.g., “preimage if known”).

Suggested change
/// Found preimage when the TLC has been settled on chain with the preimage path.
/// Preimage if known (independent of `is_settled`).

Copilot uses AI. Check for mistakes.

##### Returns

* `preimage` - <em>Option<[Hash256](#type-hash256)></em>, Found preimage when the TLC has been settled on chain with the preimage path.
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The return docs for query_tlc_status say the preimage is only found when the TLC has been settled on-chain with the preimage path, but the RPC returns any stored/known preimage regardless of is_settled (e.g., preimages created via create_preimage). Update this description to avoid implying preimage => on-chain settlement.

Suggested change
* `preimage` - <em>Option<[Hash256](#type-hash256)></em>, Found preimage when the TLC has been settled on chain with the preimage path.
* `preimage` - <em>Option<[Hash256](#type-hash256)></em>, Known preimage for this payment hash, if any. May be present regardless of whether the TLC has been settled on chain.

Copilot uses AI. Check for mistakes.
Comment on lines +211 to +229
// Construct watchtower querier for WASM (standalone watchtower only)
let watchtower_querier: Option<std::sync::Arc<dyn fnn::fiber::WatchtowerQuerier>> =
if let Some(url) = fiber_config.standalone_watchtower_rpc_url.clone() {
let querier_client =
WasmClientBuilder::default()
.build(url)
.await
.map_err(|err| {
ExitMessage(format!(
"failed to create watchtower rpc client: {}",
err
))
})?;
Some(std::sync::Arc::new(
fnn::rpc::watchtower::WatchtowerRpcQuerier::new(querier_client),
))
} else {
None
};
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

In WASM builds, the standalone watchtower RPC client is created without using fiber_config.standalone_watchtower_token, so query_tlc_status (and later event forwarding) will fail against an authenticated watchtower endpoint. Please either add token/header support for the wasm client builder (if supported by jsonrpsee wasm-client) or document/guard that auth tokens aren’t supported in wasm mode.

Copilot uses AI. Check for mistakes.
Comment on lines +211 to +226
// Construct watchtower querier for WASM (standalone watchtower only)
let watchtower_querier: Option<std::sync::Arc<dyn fnn::fiber::WatchtowerQuerier>> =
if let Some(url) = fiber_config.standalone_watchtower_rpc_url.clone() {
let querier_client =
WasmClientBuilder::default()
.build(url)
.await
.map_err(|err| {
ExitMessage(format!(
"failed to create watchtower rpc client: {}",
err
))
})?;
Some(std::sync::Arc::new(
fnn::rpc::watchtower::WatchtowerRpcQuerier::new(querier_client),
))
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This block builds a dedicated wasm watchtower client for the querier, but the file later constructs another client for forwarding events to the watchtower. If they’re targeting the same URL, consider reusing a single client instance (or a clonable handle) to avoid duplicate connections/config drift (e.g., headers/timeouts).

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +156
b.with_rule(
"query_tlc_status",
AuthRule::new(r#"allow if read("watchtower");"#).with_require_rpc_context(true),
);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The new query_tlc_status Biscuit rule only checks read("watchtower"), but the Biscuit auth doc example adds a right({channel_id}, "watchtower") constraint. Either update the documentation to match the actual enforcement, or extend the auth/middleware to inject channel_id facts and enforce channel-scoped rights for this method (and other watchtower methods, if intended).

Copilot uses AI. Check for mistakes.
Comment thread docs/biscuit-auth.md
Comment on lines +121 to +127
rule(
"query_tlc_status",
r#"
allow if read("watchtower");
allow if right({channel_id}, "watchtower");
"#,
);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The example rule for query_tlc_status includes right({channel_id}, "watchtower"), but the current Biscuit middleware/rule set doesn’t appear to inject channel_id into the authorizer context for RPC calls (it only injects node_id). As written, this example is not enforceable and may mislead users into thinking channel-scoped authorization is active for this method.

Copilot uses AI. Check for mistakes.
ian and others added 8 commits March 24, 2026 11:09
…ctor

Allow network/channel actor to query tlc status from watchtower no matter it is in the same process or runs as a standalone service.
- Add WatchtowerQueryActor to run query_tlc_status without blocking Network Actor
- Queue QueryPreimage and QuerySettled jobs from CheckChannels; handle results
  via WatchtowerPreimageResult and WatchtowerSettledResult commands
- Pass tlc_id through preimage flow so result handler can send RemoveTlc
  when preimage is found; only send if channel still has that received TLC

Made-with: Cursor
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: doitian <35768+doitian@users.noreply.github.com>
Verify that in a 3-node multi-hop payment (N1->N2->N3) using hold
invoice, when N2 force-closes the N2-N3 channel, N3 reveals the
preimage on-chain and N2's watchtower picks it up to settle the
TLC on the still-open N1-N2 channel.
The openrpc spec generator requires all RPC parameter and result
types to implement schemars::JsonSchema. QueryTlcStatusParams and
QueryTlcStatusResult were missing it, breaking the CI job.
@doitian doitian force-pushed the add-watchtower-querier-trait branch from 2372735 to cb5a69c Compare March 24, 2026 03:09
@doitian
Copy link
Copy Markdown
Member Author

doitian commented Mar 24, 2026

Troubleshooting #1222

@doitian doitian marked this pull request as draft March 24, 2026 08:25
@doitian
Copy link
Copy Markdown
Member Author

doitian commented Mar 25, 2026

After discussion with Quake, we prioritize fixing the issue when watchtower is running together with fiber node. I'll close this PR and create a new one based on that assumption.

@doitian doitian closed this Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants