Skip to content

sweepbatcher: batch change outputs #976

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

hieblmi
Copy link
Collaborator

@hieblmi hieblmi commented Jul 9, 2025

The batcher receives the ability to sign and publish batches with change outputs.

Change is created when the clients choose fractional swap amounts from the input values to be swapped. Usually the change is sent back to client's addresses.

If change is provided to ensurePresigned, presign and publishPresigned, it is expected to be placed within the passed []sweep.
Each sweep optionally holds a pointer to a change output. Only the first sweep in a sweep group called primary deposit will hold a pointer to the respective swap's change output.

  • unit test coverage for change outputs

@hieblmi hieblmi self-assigned this Jul 9, 2025
@hieblmi hieblmi marked this pull request as draft July 9, 2025 13:39
@hieblmi hieblmi requested a review from starius July 9, 2025 13:39
@hieblmi hieblmi force-pushed the batcher-change-outputs branch from 19b13cd to c7091de Compare July 9, 2025 13:46
@hieblmi
Copy link
Collaborator Author

hieblmi commented Jul 10, 2025

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the ability to handle change outputs in sweep batches, which is a significant feature. I've found a critical bug in fee calculation within CheckSignedTx that needs to be addressed. I've also pointed out a minor code simplification and a gap in test coverage for the new logic.

@hieblmi hieblmi force-pushed the batcher-change-outputs branch 4 times, most recently from c20504a to 0567051 Compare July 10, 2025 09:31
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request introduces functionality to handle change outputs in sweep batcher, specifically for presigned transactions. The changes involve modifying the sweep struct, updating transaction construction to include change outputs, and adding a function to retrieve and de-duplicate change outputs. The code also includes unit tests for the new functionality. The changes look good overall, but there are a few areas that could be improved for robustness and clarity.

@hieblmi hieblmi force-pushed the batcher-change-outputs branch from 0567051 to 950fd4e Compare July 10, 2025 13:33
@hieblmi hieblmi marked this pull request as ready for review July 10, 2025 13:41
@hieblmi hieblmi force-pushed the batcher-change-outputs branch 4 times, most recently from 576270f to 35f5810 Compare July 14, 2025 07:04
@starius
Copy link
Collaborator

starius commented Jul 14, 2025

Multiple sweeps holding the same change reference will produce a single change output in the batch transaction.

This seems to be outdated.

@hieblmi hieblmi force-pushed the batcher-change-outputs branch from 35f5810 to 4c58f85 Compare July 15, 2025 18:34
@hieblmi hieblmi force-pushed the batcher-change-outputs branch 4 times, most recently from fe4ec68 to c0418e3 Compare July 16, 2025 10:14
@hieblmi hieblmi requested a review from starius July 16, 2025 11:54
Copy link
Collaborator

@starius starius left a comment

Choose a reason for hiding this comment

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

LGTM! 🥇

Added a couple of proposals.

@hieblmi hieblmi force-pushed the batcher-change-outputs branch from c0418e3 to 1b73395 Compare July 17, 2025 13:09
@hieblmi hieblmi requested a review from bhandras July 17, 2025 13:10
@hieblmi hieblmi force-pushed the batcher-change-outputs branch 2 times, most recently from 4d91489 to 767b9cd Compare July 17, 2025 13:30
Copy link
Collaborator

@starius starius left a comment

Choose a reason for hiding this comment

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

LGTM! 🌴

One comment pending: #976 (comment)

@hieblmi hieblmi force-pushed the batcher-change-outputs branch from 767b9cd to 738ff64 Compare July 18, 2025 09:01
@hieblmi hieblmi requested a review from starius July 18, 2025 09:01
hieblmi added 3 commits July 18, 2025 11:05
This commit adds an optional change output to the sweep struct.
Presigning sweeps takes change outputs into account.
Each primary deposit id of a sweep group points to
an optional change output.
sweepbatcher.presign scans all passed sweeps for
change outputs and passes them to constructUnsignedTx.
Optional change of a swap is encoded in its sweeps
as a pointer to the same change output. This change
is taken into account when constructing the unsigned
batch transaction when it comes to tx weight and
outputs.
@hieblmi hieblmi force-pushed the batcher-change-outputs branch from 738ff64 to 26c8b27 Compare July 18, 2025 09:08
Copy link
Collaborator

@starius starius left a comment

Choose a reason for hiding this comment

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

LGTM pending subtle issues with dust limits and relay fee! 💾

Please add min relay fee to constructUnsignedTx in a separate commit, since it is a separate factor.

},
sweep: feeDetails{
FeeRate: highFeeRate,
Weight: coopInputWeight + changeOutputWeight,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test case passes even if changeOutputWeight is not added. Also its name is confusing, because the sweep is not placed in "existing medium batch", but it also added to a new batch.

I propose to replace this test case which another one which works as control for "low fee change sweep, placed in new batch":

diff --git a/sweepbatcher/greedy_batch_selection_test.go b/sweepbatcher/greedy_batch_selection_test.go
index 33b0d5a..0a819f9 100644
--- a/sweepbatcher/greedy_batch_selection_test.go
+++ b/sweepbatcher/greedy_batch_selection_test.go
@@ -835,8 +835,8 @@ func TestSelectBatches(t *testing.T) {
                },
 
                {
-                       name: "high fee change sweep, placed in existing " +
-                               "medium batch",
+                       name: "low fee sweep without change, placed in "+
+                               "existing batch",
                        batches: []feeDetails{
                                {
                                        BatchId: 1,
@@ -845,14 +845,14 @@ func TestSelectBatches(t *testing.T) {
                                },
                        },
                        sweep: feeDetails{
-                               FeeRate: highFeeRate,
-                               Weight:  coopInputWeight + changeOutputWeight,
+                               FeeRate: lowFeeRate,
+                               Weight:  coopInputWeight,
                        },
                        oneSweepBatch: feeDetails{
-                               FeeRate: highFeeRate,
+                               FeeRate: lowFeeRate,
                                Weight:  coopNewBatchWeight,
                        },
-                       wantBestBatchesIds: []int32{newBatchSignal, 1},
+                       wantBestBatchesIds: []int32{1, newBatchSignal},
                },
        }
 

// Clamp the calculated fee to the max allowed fee amount for the batch.
fee := clampBatchFee(feeForWeight, batchAmt)
fee := clampBatchFee(feeForWeight, batchAmt-btcutil.Amount(sumChange))
Copy link
Collaborator

Choose a reason for hiding this comment

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

In test TestPresigned/dust_main_output I saw:

fee 0.00000065 BTC

Such a fee is too low (below relay fee).

I think we should fail here if the fee is below relay fee rate. The function should accept min relay fee rate as an argument, we already have the value in many places, where the function is called.

Feerate can be found with chainfee.NewSatPerKWeight(fee, weight).

Please add a unit test for constructUnsignedTx for this error and adjust test TestPresigned/dust_main_output not to crash on this error. Additionally, you can add another test case for TestPresigned to reproduce this new error (fee below relay fee).

@@ -70,6 +97,8 @@ func TestConstructUnsignedTx(t *testing.T) {
return fmt.Errorf("weight estimator test failure")
}

dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
Copy link
Collaborator

Choose a reason for hiding this comment

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

destAddr and batchPkScript used as batch destination address in most cases are a p2wkh address, not taproot.

I propose to use our new function utils.DustLimitForPkScript(batchPkScript) here instead of lnwallet.DustLimitForSize(input.P2TRSize) to get the same values. The numeric values are different. Taproot is 330, p2wkh is 294.

I discovered this by changing change amounts by 2 satoshis in the test cases:

diff --git a/sweepbatcher/sweep_batcher_presigned_test.go b/sweepbatcher/sweep_batcher_presigned_test.go
index fd28bc7..2ad90ab 100644
--- a/sweepbatcher/sweep_batcher_presigned_test.go
+++ b/sweepbatcher/sweep_batcher_presigned_test.go
@@ -1318,7 +1318,7 @@ func testPresigned_presigned_group_with_dust_main_output(t *testing.T,
        }
        dustLimit := int64(lnwallet.DustLimitForSize(input.P2TRSize))
        change := &wire.TxOut{
-               Value:    inputValue - dustLimit + 1,
+               Value:    inputValue - dustLimit - 1,
                PkScript: []byte{0xaf, 0xfe},
        }
 
@@ -1401,7 +1401,7 @@ func testPresigned_presigned_group_with_dust_change(t *testing.T,
        }
        dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
        change := &wire.TxOut{
-               Value:    int64(dustLimit - 1),
+               Value:    int64(dustLimit + 1),
                PkScript: []byte{0xaf, 0xfe},
        }
 

My expectation was that inputs would be added successfully (and the tests would fail, as they test failures). However it was not the case, because these dustLimit variables used in sweep_batch_test.go are higher than the actual limit.

Please use utils.DustLimitForPkScript(batchPkScript) here below in this file.

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.

2 participants