Skip to content

Include onchain transactions in events#448

Open
tnull wants to merge 4 commits into
lightningdevkit:mainfrom
tnull:2025-01-include-onchain-transactions-in-events
Open

Include onchain transactions in events#448
tnull wants to merge 4 commits into
lightningdevkit:mainfrom
tnull:2025-01-include-onchain-transactions-in-events

Conversation

@tnull

@tnull tnull commented Jan 27, 2025

Copy link
Copy Markdown
Collaborator

Closes #446.
Based on #432.

In #432 we exposed transactions in store. Here we take a stab at exposing them via events. However, to avoid emitting duplicate events for channel-related transactions, this is in draft until we have lightningdevkit/rust-lightning#3566

@tnull

tnull commented Oct 9, 2025

Copy link
Copy Markdown
Collaborator Author

While the latest BDK 2.2 now shipped event (which would make emitting events easier on our end), we still need lightningdevkit/rust-lightning#3566 to properly classify the transactions in our store. So this remains blocked until the latter ships (likely with LDK 0.3).

@ovitrif

ovitrif commented Oct 29, 2025

Copy link
Copy Markdown

While the latest BDK 2.2 now shipped event (which would make emitting events easier on our end), we still need lightningdevkit/rust-lightning#3566 to properly classify the transactions in our store. So this remains blocked until the latter ships (likely with LDK 0.3).

Is there a way we could get this into ldk-node sooner?
To be honest we (Bitkit team), kind of relied on the expectation that the BDK 2.2 PR #310 Return wallet events when applying updates will be integrated in ldk-node soon enough (ie. this month or November); and we made our roadmapping around this expectation. Now I'm getting feedback that there is no way we can delay our release any further than when it's planned, and the due date is very soon; which leaves us with very few options:

  1. Get help from you to get this into ldk-node very soon (November)
  2. Collaborate with you to have someone from our team help with implementation efforts (for example by fixing lightningdevkit/rust-lightning#3566), but then we will need a commitment that if we fix that, this PR takes priority and it is implemented shortly thereafter)
  3. Do our own implementation for this

The team seems to lean towards favoring option 3, which is not ideal IMHO, but the main argument is that we don't really have the time to go any other route.

The issue that I see from my PoV is that option 2 might even seem faster.

Could 2 be an option?

@tnull

tnull commented Feb 18, 2026

Copy link
Copy Markdown
Collaborator Author

Technically unblocked, but we should first land #791, as the API here will depend on the API choices there.

tnull added 2 commits July 10, 2026 13:01
Let callers obtain the object produced by merge semantics without a
second lookup. Preserve the existing boolean API so unrelated call
sites and their allocation behavior remain unchanged.

Co-Authored-By: HAL 9000
Let wallet sync enqueue on-chain payment events directly.

Co-Authored-By: HAL 9000
@tnull tnull force-pushed the 2025-01-include-onchain-transactions-in-events branch 3 times, most recently from b20b0b5 to b2d7c36 Compare July 10, 2026 12:59
@tnull tnull marked this pull request as ready for review July 10, 2026 13:00
@tnull

tnull commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Should be good for review now.

@tnull tnull force-pushed the 2025-01-include-onchain-transactions-in-events branch from b2d7c36 to 35a60c4 Compare July 10, 2026 13:07
@tnull tnull requested a review from jkczyz July 10, 2026 13:39
tnull added 2 commits July 10, 2026 15:39
Notify users when otherwise unclassified on-chain payments reach the
anti-reorg confirmation depth.

Use the wallet's effective stored payment so classified funding,
splice, close, and sweep transactions stay quiet.

Co-Authored-By: HAL 9000
The in-memory store source is shared by the library test utilities and
integration-test helpers. Keep its unit test in the library-only module
so each integration binary does not register and rerun it.

Co-Authored-By: HAL 9000
@tnull tnull force-pushed the 2025-01-include-onchain-transactions-in-events branch from 35a60c4 to 176ac00 Compare July 10, 2026 13:40
Comment thread src/data_store.rs
Comment on lines +476 to +481
let supplied = MergingTestObject { id, data: [25u8; 3], preserved_data: [26u8; 3] };
let expected = MergingTestObject { data: supplied.data, ..existing };
assert_eq!(Ok((true, expected)), data_store.insert_or_update_and_get(supplied).await);

let unchanged = MergingTestObject { preserved_data: [27u8; 3], ..expected };
assert_eq!(Ok((false, expected)), data_store.insert_or_update_and_get(unchanged).await);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add an assertion for when an insert occurs.

Comment thread src/wallet/mod.rs
if payment_status == PaymentStatus::Pending {
let pending_payment =
self.create_pending_payment_from_tx(payment, Vec::new());
if updated && payment_status == PaymentStatus::Succeeded {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we crash after the payment_store write but before the event queue is persisted, updated will be false on the next sync upon restart and the event will be lost.

Also from Claude:

This updated gate is the only thing preventing re-emission after a restart (the reloaded store merges the re-derived payment as a no-op), and no test covers it: no restart test syncs and asserts next_event() == None. I wrote that test — it passes, so this is coverage-only.

Comment thread src/wallet/mod.rs
Comment on lines +289 to +293
if payment_status == PaymentStatus::Pending {
let pending_payment =
self.create_pending_payment_from_tx(stored_payment, Vec::new());
self.pending_payment_store.insert_or_update(pending_payment).await?;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Directly from Claude:

Direct graduation leaves a pre-existing pending entry stale. Sequence: a tx confirms below ANTI_REORG_DELAY, so a pending entry is written recording its confirmation block; a shallow reorg then moves the tx into a different block; by the next batch sync the tx is ≥ ANTI_REORG_DELAY deep. That sync's TxConfirmed graduates the payment and emits an event with the new (canonical) block, but the pending entry still holds the old, now-orphaned block — and ChainTipChanged then merges that stale entry back in (updated == true), emitting a duplicate event carrying the orphaned block hash and regressing the stored confirmation to the orphaned block. Demonstrated by a failing integration test using a 1-block reorg; no restart needed. Remove/refresh the pending entry when graduating directly — with that fixed, emission is exactly-once for reorgs shallower than ANTI_REORG_DELAY, which is worth documenting on the event variants.

Comment thread src/event.rs
Comment on lines +287 to +288
/// The value, in thousandths of a satoshi, that was sent.
amount_msat: u64,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we include a field for the fees paid?

Comment thread src/event.rs
amount_msat: u64,
/// The hash of the block in which the transaction was confirmed.
block_hash: BlockHash,
/// The height at which the block was confirmed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The height of the block in which the transaction was confirmed.

Comment thread tests/common/mod.rs
Comment on lines +1630 to +1635
if !allow_0conf {
assert_eq!(
node_a.expect_onchain_payment_event(OnchainPaymentEvent::Received).await,
splice_out_txo.txid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmmm... Clause says we aren't using InteractiveFunding for 0-conf splices.

The gate is correct but subtle — worth a comment: with 0conf, LDK re-broadcasts the splice tx as TransactionType::Funding (classified → no event); without, the acceptor only broadcasts InteractiveFunding, which classification skips → event fires. The cross-variant inconsistency in the acceptor's record may be worth flagging upstream.

};

generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
wait_for_tx(&electrsd.client, txid).await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The CI failure is here according to Claude:

This ordering is what failed CI (build (self-hosted, beta)) once the new assertion below started depending on it: send() only queues the broadcast (drained by a background task), and wait_for_tx after mining also passes for a mempool-only tx — so the tx can miss the first of the 6 blocks, sit below ANTI_REORG_DELAY at the sync, and never produce the event (expect_onchain_payment_event can't recover; single 60s next_event_async, tests/common/mod.rs:426-434). Swap these two lines, as onchain_send_receive does.

Comment on lines +832 to +833
assert_eq!(node_a.next_event(), None);
assert_eq!(node_b.next_event(), None);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude:

After the reorg sync below, also assert no event, then re-mine to ANTI_REORG_DELAY and assert exactly one event fires. Extended that way, it currently fails due to the stale-pending-entry duplicate (src/wallet/mod.rs:289).


// Restart node_a with the same config
println!("\nRestarting node A...");
let restarted_node_a = setup_node(&chain_source, config_a);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude:

Nothing tests that a restarted node doesn't re-emit. I wrote the test — it passes, so coverage-only.

Comment on lines +232 to +235
is Event.OnchainPaymentReceived -> {
assertEquals(txid1, onchainPaymentReceivedEvent1.txid)
assertEquals(100000000uL, onchainPaymentReceivedEvent1.amountMsat)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How about testing OnchainPaymentSuccessful?

@jkczyz

jkczyz commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Tests mentioned in claude feedback can be found in jkczyz@a7c0269

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.

Expose on-chain transactions in events

3 participants