From d05819cc2b4a2ca5bb24e3ed1ec1eca7d75c296e Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 9 Jul 2025 15:21:12 +0200 Subject: [PATCH 1/4] sweepbatcher: consider change in presigning and batch tx Presigning sweeps takes change outputs into account. Each sweep belonging to the same sweep group points to the same change output, if existent. 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. --- sweepbatcher/presigned.go | 108 +++++++++++++++++++----- sweepbatcher/presigned_test.go | 128 ++++++++++++++++++++++++++++- sweepbatcher/sweep_batch.go | 54 ++++++++++-- sweepbatcher/sweep_batch_test.go | 137 ++++++++++++++++++++++++++++++- sweepbatcher/sweep_batcher.go | 2 +- 5 files changed, 396 insertions(+), 33 deletions(-) diff --git a/sweepbatcher/presigned.go b/sweepbatcher/presigned.go index 385f34cf9..6ef3b0c94 100644 --- a/sweepbatcher/presigned.go +++ b/sweepbatcher/presigned.go @@ -51,6 +51,7 @@ func ensurePresigned(ctx context.Context, newSweeps []*sweep, outpoint: s.outpoint, value: s.value, presigned: s.presigned, + change: s.change, } } @@ -66,6 +67,12 @@ func ensurePresigned(ctx context.Context, newSweeps []*sweep, return fmt.Errorf("failed to find destination address: %w", err) } + // Get the change outputs for each sweep group. + changeOutputs, err := getChangeOutputs(sweeps, chainParams) + if err != nil { + return fmt.Errorf("failed to get change outputs: %w", err) + } + // Set LockTime to 0. It is not critical. const currentHeight = 0 @@ -73,7 +80,7 @@ func ensurePresigned(ctx context.Context, newSweeps []*sweep, const feeRate = chainfee.FeePerKwFloor tx, _, _, _, err := constructUnsignedTx( - sweeps, destAddr, currentHeight, feeRate, + sweeps, destAddr, changeOutputs, currentHeight, feeRate, ) if err != nil { return fmt.Errorf("failed to construct unsigned tx "+ @@ -257,7 +264,7 @@ func (b *batch) presign(ctx context.Context, newSweeps []*sweep) error { err = presign( ctx, b.cfg.presignedHelper, destAddr, primarySweepID, - sweeps, nextBlockFeeRate, + sweeps, nextBlockFeeRate, b.cfg.chainParams, ) if err != nil { return fmt.Errorf("failed to presign a transaction "+ @@ -299,7 +306,8 @@ type presigner interface { // 10x of the current next block feerate. func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address, primarySweepID wire.OutPoint, sweeps []sweep, - nextBlockFeeRate chainfee.SatPerKWeight) error { + nextBlockFeeRate chainfee.SatPerKWeight, + chainParams *chaincfg.Params) error { if presigner == nil { return fmt.Errorf("presigner is not installed") @@ -328,6 +336,12 @@ func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address, return fmt.Errorf("timeout is invalid: %d", timeout) } + // Get the change outputs of each sweep group. + changeOutputs, err := getChangeOutputs(sweeps, chainParams) + if err != nil { + return fmt.Errorf("failed to get change outputs: %w", err) + } + // Go from the floor (1.01 sat/vbyte) to 2k sat/vbyte with step of 1.2x. const ( start = chainfee.FeePerKwFloor @@ -353,7 +367,7 @@ func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address, for fr := start; fr <= stop; fr = (fr * factorPPM) / 1_000_000 { // Construct an unsigned transaction for this fee rate. tx, _, feeForWeight, fee, err := constructUnsignedTx( - sweeps, destAddr, currentHeight, fr, + sweeps, destAddr, changeOutputs, currentHeight, fr, ) if err != nil { return fmt.Errorf("failed to construct unsigned tx "+ @@ -438,9 +452,15 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error, err), false } + changeOutputs, err := getChangeOutputs(sweeps, b.cfg.chainParams) + if err != nil { + return 0, fmt.Errorf("failed to get change outputs: %w", err), + false + } + // Construct unsigned batch transaction. tx, weight, _, fee, err := constructUnsignedTx( - sweeps, address, currentHeight, feeRate, + sweeps, address, changeOutputs, currentHeight, feeRate, ) if err != nil { return 0, fmt.Errorf("failed to construct tx: %w", err), @@ -493,10 +513,12 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error, signedFeeRate := chainfee.NewSatPerKWeight(fee, realWeight) numSweeps := len(tx.TxIn) + numChange := len(tx.TxOut) - 1 b.Infof("attempting to publish custom signed tx=%v, desiredFeerate=%v,"+ - " signedFeeRate=%v, weight=%v, fee=%v, sweeps=%d, destAddr=%s", + " signedFeeRate=%v, weight=%v, fee=%v, sweeps=%d, "+ + "changeOutputs=%d, destAddr=%s", txHash, feeRate, signedFeeRate, realWeight, fee, numSweeps, - address) + numChange, address) b.debugLogTx("serialized batch", tx) // Publish the transaction. @@ -557,6 +579,46 @@ func getPresignedSweepsDestAddr(ctx context.Context, helper destPkScripter, return address, nil } +// getChangeOutputs retrieves the change output references of each sweep and +// de-duplicates them. The function must be used in presigned mode only. +func getChangeOutputs(sweeps []sweep, chainParams *chaincfg.Params) ( + map[*wire.TxOut]btcutil.Address, error) { + + changeOutputs := make(map[*wire.TxOut]btcutil.Address) + for _, sweep := range sweeps { + // If the sweep has a change output, add it to the changeOutputs + // map to avoid duplicates. + if sweep.change == nil { + continue + } + + // If the change output is already in the map, skip it. + if _, exists := changeOutputs[sweep.change]; exists { + continue + } + + // Convert the change output's pkScript to an address. + changePkScript, err := txscript.ParsePkScript( + sweep.change.PkScript, + ) + if err != nil { + return nil, fmt.Errorf("failed to parse change "+ + "output pkScript: %w", err) + } + + address, err := changePkScript.Address(chainParams) + if err != nil { + return nil, fmt.Errorf("pkScript.Address failed for "+ + "pkScript %x returned for change output: %w", + sweep.change.PkScript, err) + } + + changeOutputs[sweep.change] = address + } + + return changeOutputs, nil +} + // CheckSignedTx makes sure that signedTx matches the unsignedTx. It checks // according to criteria specified in the description of PresignedHelper.SignTx. func CheckSignedTx(unsignedTx, signedTx *wire.MsgTx, inputAmt btcutil.Amount, @@ -593,23 +655,31 @@ func CheckSignedTx(unsignedTx, signedTx *wire.MsgTx, inputAmt btcutil.Amount, } // Compare outputs. - if len(unsignedTx.TxOut) != 1 { - return fmt.Errorf("unsigned tx has %d outputs, want 1", - len(unsignedTx.TxOut)) - } - if len(signedTx.TxOut) != 1 { - return fmt.Errorf("the signed tx has %d outputs, want 1", + if len(unsignedTx.TxOut) != len(signedTx.TxOut) { + return fmt.Errorf("unsigned tx has %d outputs, signed tx has "+ + "%d outputs, should be equal", len(unsignedTx.TxOut), len(signedTx.TxOut)) } - unsignedOut := unsignedTx.TxOut[0] - signedOut := signedTx.TxOut[0] - if !bytes.Equal(unsignedOut.PkScript, signedOut.PkScript) { - return fmt.Errorf("mismatch of output pkScript: %x, %x", - unsignedOut.PkScript, signedOut.PkScript) + for i, o := range unsignedTx.TxOut { + if !bytes.Equal(o.PkScript, signedTx.TxOut[i].PkScript) { + return fmt.Errorf("mismatch of output pkScript: %x, %x", + o.PkScript, signedTx.TxOut[i].PkScript) + } + } + + // The first output is always the batch output. + // TODO(hieblmi): ensure this. + batchOutput := signedTx.TxOut[0] + + // Calculate the total value of change outputs to help determine the + // transaction fee. + totalChangeValue := btcutil.Amount(0) + for i := 1; i < len(signedTx.TxOut); i++ { + totalChangeValue += btcutil.Amount(signedTx.TxOut[i].Value) } // Find the feerate of signedTx. - fee := inputAmt - btcutil.Amount(signedOut.Value) + fee := inputAmt - btcutil.Amount(batchOutput.Value) - totalChangeValue weight := lntypes.WeightUnit( blockchain.GetTransactionWeight(btcutil.NewTx(signedTx)), ) diff --git a/sweepbatcher/presigned_test.go b/sweepbatcher/presigned_test.go index 629ff1de4..cb84d49a5 100644 --- a/sweepbatcher/presigned_test.go +++ b/sweepbatcher/presigned_test.go @@ -1011,6 +1011,7 @@ func TestPresign(t *testing.T) { ctx, tc.presigner, tc.destAddr, tc.primarySweepID, tc.sweeps, tc.nextBlockFeeRate, + &chaincfg.RegressionNetParams, ) if tc.wantErr != "" { require.Error(t, err) @@ -1460,7 +1461,8 @@ func TestCheckSignedTx(t *testing.T) { }, inputAmt: 3_000_000, minRelayFee: 253, - wantErr: "unsigned tx has 2 outputs, want 1", + wantErr: "unsigned tx has 2 outputs, signed tx " + + "has 1 outputs, should be equal", }, { @@ -1517,7 +1519,8 @@ func TestCheckSignedTx(t *testing.T) { }, inputAmt: 3_000_000, minRelayFee: 253, - wantErr: "the signed tx has 2 outputs, want 1", + wantErr: "unsigned tx has 1 outputs, signed tx " + + "has 2 outputs, should be equal", }, { @@ -1642,3 +1645,124 @@ func TestCheckSignedTx(t *testing.T) { }) } } + +// TestGetChangeOutputs tests that the change aggregation across sweeps works as +// intended. Each sweep of a sweep group should have a pointer to the same +// change output which is aggregated in getChangeOutput. +func TestGetChangeOutputs(t *testing.T) { + // Prepare the necessary data for test cases. + op1 := wire.OutPoint{ + Hash: chainhash.Hash{1, 1, 1}, + Index: 1, + } + op2 := wire.OutPoint{ + Hash: chainhash.Hash{2, 2, 2}, + Index: 2, + } + op3 := wire.OutPoint{ + Hash: chainhash.Hash{3, 3, 3}, + Index: 3, + } + + batchPkScript, err := txscript.PayToAddrScript(destAddr) + require.NoError(t, err) + + changeOutput1 := &wire.TxOut{ + Value: 100_000, + PkScript: batchPkScript, + } + changeOutput2 := &wire.TxOut{ + Value: 200_000, + PkScript: batchPkScript, + } + + cases := []struct { + name string + sweeps []sweep + wantOutputs map[*wire.TxOut]btcutil.Address + wantErr string + }{ + { + name: "no change", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + change: nil, + }, + }, + wantOutputs: map[*wire.TxOut]btcutil.Address{}, + }, + { + name: "single sweep, single change", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + change: changeOutput1, + }, + }, + wantOutputs: map[*wire.TxOut]btcutil.Address{ + changeOutput1: destAddr, + }, + }, + { + name: "double sweep, single change", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + change: changeOutput1, + }, + { + outpoint: op2, + value: 1_000_000, + change: changeOutput1, + }, + }, + wantOutputs: map[*wire.TxOut]btcutil.Address{ + changeOutput1: destAddr, + }, + }, + { + name: "double sweep, double change", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + change: changeOutput1, + }, + { + outpoint: op2, + value: 1_000_000, + change: changeOutput1, + }, + { + outpoint: op3, + value: 1_000_000, + change: changeOutput2, + }, + }, + wantOutputs: map[*wire.TxOut]btcutil.Address{ + changeOutput1: destAddr, + changeOutput2: destAddr, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + changeOutputs, err := getChangeOutputs( + tc.sweeps, &chaincfg.RegressionNetParams, + ) + if tc.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + + require.Equal(t, tc.wantOutputs, changeOutputs) + }) + } +} diff --git a/sweepbatcher/sweep_batch.go b/sweepbatcher/sweep_batch.go index 933077fcf..1a561d1a9 100644 --- a/sweepbatcher/sweep_batch.go +++ b/sweepbatcher/sweep_batch.go @@ -125,6 +125,9 @@ type sweep struct { // presigned is set, if the sweep should be handled in presigned mode. presigned bool + + // change is the optional change output of the sweep. + change *wire.TxOut } // batchState is the state of the batch. @@ -1238,6 +1241,7 @@ func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte, // constructUnsignedTx creates unsigned tx from the sweeps, paying to the addr. // It also returns absolute fee (from weight and clamped). func constructUnsignedTx(sweeps []sweep, address btcutil.Address, + changeOutputs map[*wire.TxOut]btcutil.Address, currentHeight int32, feeRate chainfee.SatPerKWeight) (*wire.MsgTx, lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) { @@ -1297,6 +1301,20 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address, "failed: %w", err) } + // Add the optional change outputs to weight estimates. + if len(changeOutputs) > 0 { + for _, addr := range changeOutputs { + // Add the output to weight estimates. + err = sweeppkg.AddOutputEstimate( + &weightEstimate, addr, + ) + if err != nil { + return nil, 0, 0, 0, fmt.Errorf("sweep."+ + "AddOutputEstimate failed: %w", err) + } + } + } + // Keep track of the total amount this batch is sweeping back. batchAmt := btcutil.Amount(0) for _, sweep := range sweeps { @@ -1318,11 +1336,29 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address, fee := clampBatchFee(feeForWeight, batchAmt) // Add the batch transaction output, which excludes the fees paid to - // miners. - batchTx.AddTxOut(&wire.TxOut{ - PkScript: batchPkScript, - Value: int64(batchAmt - fee), - }) + // miners. Reduce the amount by the sum of change outputs, if any. + if len(changeOutputs) == 0 { + batchTx.AddTxOut(&wire.TxOut{ + PkScript: batchPkScript, + Value: int64(batchAmt - fee), + }) + } else { + // Reduce the batch output by the sum of change outputs. + var sumChange int64 + for change := range changeOutputs { + sumChange += change.Value + } + batchTx.AddTxOut(&wire.TxOut{ + PkScript: batchPkScript, + Value: int64(batchAmt-fee) - sumChange, + }) + for txOut := range changeOutputs { + batchTx.AddTxOut(&wire.TxOut{ + PkScript: txOut.PkScript, + Value: txOut.Value, + }) + } + } return batchTx, weight, feeForWeight, fee, nil } @@ -1396,9 +1432,13 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error, attempt) // Construct unsigned batch transaction. - var err error + var ( + err error + changeOutputs = make(map[*wire.TxOut]btcutil.Address) + ) tx, weight, feeForWeight, fee, err = constructUnsignedTx( - sweeps, address, b.currentHeight, b.rbfCache.FeeRate, + sweeps, address, changeOutputs, b.currentHeight, + b.rbfCache.FeeRate, ) if err != nil { return 0, fmt.Errorf("failed to construct tx: %w", err), diff --git a/sweepbatcher/sweep_batch_test.go b/sweepbatcher/sweep_batch_test.go index 570565b0d..22bc2efa2 100644 --- a/sweepbatcher/sweep_batch_test.go +++ b/sweepbatcher/sweep_batch_test.go @@ -29,6 +29,14 @@ func TestConstructUnsignedTx(t *testing.T) { Hash: chainhash.Hash{2, 2, 2}, Index: 2, } + op3 := wire.OutPoint{ + Hash: chainhash.Hash{3, 3, 3}, + Index: 3, + } + op4 := wire.OutPoint{ + Hash: chainhash.Hash{4, 4, 4}, + Index: 4, + } batchPkScript, err := txscript.PayToAddrScript(destAddr) require.NoError(t, err) @@ -40,6 +48,15 @@ func TestConstructUnsignedTx(t *testing.T) { p2trPkScript, err := txscript.PayToAddrScript(p2trAddress) require.NoError(t, err) + change1 := &wire.TxOut{ + Value: 100_000, + PkScript: p2trPkScript, + } + change2 := &wire.TxOut{ + Value: 200_000, + PkScript: p2trPkScript, + } + serializedPubKey := []byte{ 0x02, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, @@ -223,7 +240,7 @@ func TestConstructUnsignedTx(t *testing.T) { }, TxOut: []*wire.TxOut{ { - Value: 2400000, + Value: 2_400_000, PkScript: batchPkScript, }, }, @@ -265,7 +282,7 @@ func TestConstructUnsignedTx(t *testing.T) { }, TxOut: []*wire.TxOut{ { - Value: 2999211, + Value: 2_999_211, PkScript: batchPkScript, }, }, @@ -275,6 +292,113 @@ func TestConstructUnsignedTx(t *testing.T) { wantFee: 789, }, + { + name: "all sweeps same change output", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + change: change1, + }, + { + outpoint: op2, + value: 2_000_000, + change: change1, + }, + }, + address: p2trAddress, + currentHeight: 800_000, + feeRate: 1000, + wantTx: &wire.MsgTx{ + Version: 2, + LockTime: 800_000, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op1, + }, + { + PreviousOutPoint: op2, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 2_899_154, + PkScript: p2trPkScript, + }, + { + Value: change1.Value, + PkScript: change1.PkScript, + }, + }, + }, + wantWeight: 846, + wantFeeForWeight: 846, + wantFee: 846, + }, + + { + name: "all sweeps different change outputs", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + }, + { + outpoint: op2, + value: 2_000_000, + change: change1, + }, + { + outpoint: op3, + value: 3_000_000, + change: change1, + }, + { + outpoint: op4, + value: 4_000_000, + change: change2, + }, + }, + address: p2trAddress, + currentHeight: 800_000, + feeRate: 1000, + wantTx: &wire.MsgTx{ + Version: 2, + LockTime: 800_000, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op1, + }, + { + PreviousOutPoint: op2, + }, + { + PreviousOutPoint: op3, + }, + { + PreviousOutPoint: op4, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 9_698_522, + PkScript: p2trPkScript, + }, + { + Value: change1.Value, + PkScript: change1.PkScript, + }, + { + Value: change2.Value, + PkScript: change2.PkScript, + }, + }, + }, + wantWeight: 1478, + wantFeeForWeight: 1478, + wantFee: 1478, + }, + { name: "weight estimator fails", sweeps: []sweep{ @@ -338,9 +462,14 @@ func TestConstructUnsignedTx(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + changeOutputs, err := getChangeOutputs( + tc.sweeps, &chaincfg.RegressionNetParams, + ) + require.NoError(t, err) + tx, weight, feeForW, fee, err := constructUnsignedTx( - tc.sweeps, tc.address, tc.currentHeight, - tc.feeRate, + tc.sweeps, tc.address, changeOutputs, + tc.currentHeight, tc.feeRate, ) if tc.wantErr != "" { require.Error(t, err) diff --git a/sweepbatcher/sweep_batcher.go b/sweepbatcher/sweep_batcher.go index 6bab035e7..e4c30041c 100644 --- a/sweepbatcher/sweep_batcher.go +++ b/sweepbatcher/sweep_batcher.go @@ -751,7 +751,7 @@ func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input, return presign( ctx, b.presignedHelper, destAddress, primarySweepID, sweeps, - nextBlockFeeRate, + nextBlockFeeRate, b.chainParams, ) } From 950fd4ee492bdbf860923ed66f5a34ae81833ee8 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 9 Jul 2025 15:30:07 +0200 Subject: [PATCH 2/4] sweepbatcher: add change to PresignSweepsGroup The batcher now presigns sweep groups including an optional change output. Change is usually leftover from the client's swap amount which is returned to the client. --- sweepbatcher/sweep_batcher.go | 8 ++- sweepbatcher/sweep_batcher_presigned_test.go | 60 +++++++++++--------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/sweepbatcher/sweep_batcher.go b/sweepbatcher/sweep_batcher.go index e4c30041c..7633c20b8 100644 --- a/sweepbatcher/sweep_batcher.go +++ b/sweepbatcher/sweep_batcher.go @@ -125,6 +125,9 @@ type SweepInfo struct { // value should be stable for a sweep. Currently presigned and // non-presigned sweeps never appear in the same batch. IsPresigned bool + + // Change is an optional change output of the sweep. + Change *wire.TxOut } // SweepFetcher is used to get details of a sweep. @@ -713,7 +716,8 @@ func (b *Batcher) Run(ctx context.Context) error { // swap. The order of sweeps is important. The first sweep serves as // primarySweepID if the group starts a new batch. func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input, - sweepTimeout int32, destAddress btcutil.Address) error { + sweepTimeout int32, destAddress btcutil.Address, + changeOutput *wire.TxOut) error { if len(inputs) == 0 { return fmt.Errorf("no inputs passed to PresignSweepsGroup") @@ -742,6 +746,7 @@ func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input, outpoint: input.Outpoint, value: input.Value, timeout: sweepTimeout, + change: changeOutput, } } @@ -1564,6 +1569,7 @@ func (b *Batcher) loadSweep(ctx context.Context, swapHash lntypes.Hash, minFeeRate: minFeeRate, nonCoopHint: s.NonCoopHint, presigned: s.IsPresigned, + change: s.Change, }, nil } diff --git a/sweepbatcher/sweep_batcher_presigned_test.go b/sweepbatcher/sweep_batcher_presigned_test.go index 36f11f750..76d14b2b2 100644 --- a/sweepbatcher/sweep_batcher_presigned_test.go +++ b/sweepbatcher/sweep_batcher_presigned_test.go @@ -113,7 +113,7 @@ func (h *mockPresignedHelper) DestPkScript(ctx context.Context, } // SignTx tries to sign the transaction. If all the inputs are online, it signs -// the exact transaction passed and adds it to presignedBatches. Otherwise it +// the exact transaction passed and adds it to presignedBatches. Otherwise, it // looks for a transaction in presignedBatches satisfying the criteria. func (h *mockPresignedHelper) SignTx(ctx context.Context, primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount, @@ -273,7 +273,7 @@ func testPresigned_forgotten_presign(t *testing.T, presignedHelper.SetOutpointOnline(op1, false) err := batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.Error(t, err) require.ErrorContains(t, err, "offline") @@ -350,7 +350,7 @@ func testPresigned_input1_offline_then_input2(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) @@ -413,7 +413,7 @@ func testPresigned_input1_offline_then_input2(t *testing.T, presignedHelper.SetOutpointOnline(op2, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op2, Value: 2_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) @@ -520,7 +520,7 @@ func testPresigned_min_relay_fee(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err := batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: inputAmt}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) @@ -644,7 +644,7 @@ func testPresigned_two_inputs_one_goes_offline(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) require.NoError(t, batcher.AddSweep(ctx, &sweepReq1)) @@ -670,7 +670,7 @@ func testPresigned_two_inputs_one_goes_offline(t *testing.T, presignedHelper.SetOutpointOnline(op2, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op2, Value: 2_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) require.NoError(t, batcher.AddSweep(ctx, &sweepReq2)) @@ -780,7 +780,7 @@ func testPresigned_first_publish_fails(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) presignedHelper.SetOutpointOnline(op1, false) @@ -903,7 +903,7 @@ func testPresigned_locktime(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) presignedHelper.SetOutpointOnline(op1, false) @@ -994,14 +994,18 @@ func testPresigned_presigned_group(t *testing.T, presignedHelper.SetOutpointOnline(op2, false) // An attempt to presign must fail. - err = batcher.PresignSweepsGroup(ctx, group1, sweepTimeout, destAddr) + err = batcher.PresignSweepsGroup( + ctx, group1, sweepTimeout, destAddr, nil, + ) require.ErrorContains(t, err, "some outpoint is offline") // Enable both outpoints. presignedHelper.SetOutpointOnline(op2, true) // An attempt to presign must succeed. - err = batcher.PresignSweepsGroup(ctx, group1, sweepTimeout, destAddr) + err = batcher.PresignSweepsGroup( + ctx, group1, sweepTimeout, destAddr, nil, + ) require.NoError(t, err) // Add the sweep, triggering the publish attempt. @@ -1053,7 +1057,9 @@ func testPresigned_presigned_group(t *testing.T, presignedHelper.SetOutpointOnline(op4, true) // An attempt to presign must succeed. - err = batcher.PresignSweepsGroup(ctx, group2, sweepTimeout, destAddr) + err = batcher.PresignSweepsGroup( + ctx, group2, sweepTimeout, destAddr, nil, + ) require.NoError(t, err) // Add the sweep. It should go to the same batch. @@ -1107,7 +1113,9 @@ func testPresigned_presigned_group(t *testing.T, presignedHelper.SetOutpointOnline(op6, true) // An attempt to presign must succeed. - err = batcher.PresignSweepsGroup(ctx, group3, sweepTimeout, destAddr) + err = batcher.PresignSweepsGroup( + ctx, group3, sweepTimeout, destAddr, nil, + ) require.NoError(t, err) // Add the sweep. It should go to the same batch. @@ -1215,9 +1223,9 @@ func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore, setFeeRate(feeRateLow) - ///////////////////////////////////// + // /////////////////////////////////// // Create the first regular sweep. // - ///////////////////////////////////// + // /////////////////////////////////// swapHash1 := lntypes.Hash{1, 1, 1} op1 := wire.OutPoint{ Hash: chainhash.Hash{1, 1}, @@ -1264,9 +1272,9 @@ func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore, require.Len(t, tx1.TxIn, 1) require.Len(t, tx1.TxOut, 1) - /////////////////////////////////////// + // ///////////////////////////////////// // Create the first presigned sweep. // - /////////////////////////////////////// + // ///////////////////////////////////// swapHash2 := lntypes.Hash{2, 2, 2} op2 := wire.OutPoint{ Hash: chainhash.Hash{2, 2}, @@ -1304,7 +1312,7 @@ func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore, presignedHelper.SetOutpointOnline(op2, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op2, Value: 2_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) require.NoError(t, batcher.AddSweep(ctx, &sweepReq2)) @@ -1319,9 +1327,9 @@ func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore, require.Len(t, tx2.TxOut, 1) require.Equal(t, op2, tx2.TxIn[0].PreviousOutPoint) - ////////////////////////////////////// + // //////////////////////////////////// // Create the second regular sweep. // - ////////////////////////////////////// + // //////////////////////////////////// swapHash3 := lntypes.Hash{3, 3, 3} op3 := wire.OutPoint{ Hash: chainhash.Hash{3, 3}, @@ -1359,9 +1367,9 @@ func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore, // Deliver sweep request to batcher. require.NoError(t, batcher.AddSweep(ctx, &sweepReq3)) - //////////////////////////////////////// + // ////////////////////////////////////// // Create the second presigned sweep. // - //////////////////////////////////////// + // ////////////////////////////////////// swapHash4 := lntypes.Hash{4, 4, 4} op4 := wire.OutPoint{ Hash: chainhash.Hash{4, 4}, @@ -1399,7 +1407,7 @@ func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore, presignedHelper.SetOutpointOnline(op4, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op4, Value: 3_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) require.NoError(t, batcher.AddSweep(ctx, &sweepReq4)) @@ -1520,7 +1528,7 @@ func testPresigned_purging(t *testing.T, numSwaps, numConfirmedSwaps int, // An attempt to presign must succeed. err := batcher.PresignSweepsGroup( - ctx, group, sweepTimeout, destAddr, + ctx, group, sweepTimeout, destAddr, nil, ) require.NoError(t, err) @@ -1584,11 +1592,11 @@ func testPresigned_purging(t *testing.T, numSwaps, numConfirmedSwaps int, // An attempt to presign must succeed. err := batcher.PresignSweepsGroup( - ctx, group, sweepTimeout, destAddr, + ctx, group, sweepTimeout, destAddr, nil, ) require.NoError(t, err) - // Add the sweep, triggering the publish attempt. + // Add the sweep, triggering the publishing attempt. require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{ SwapHash: swapHash, Inputs: group, From 1ba51cac643280d9599446c53e73040c2a34dc35 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 11 Jun 2025 14:56:04 +0200 Subject: [PATCH 3/4] swapserverrpc: arbitrary static swap amount --- swapserverrpc/staticaddr.pb.go | 264 ++++++++++++++++++--------------- swapserverrpc/staticaddr.proto | 15 +- 2 files changed, 155 insertions(+), 124 deletions(-) diff --git a/swapserverrpc/staticaddr.pb.go b/swapserverrpc/staticaddr.pb.go index 4f5cd4134..0deaf1252 100644 --- a/swapserverrpc/staticaddr.pb.go +++ b/swapserverrpc/staticaddr.pb.go @@ -398,7 +398,9 @@ type ServerStaticAddressLoopInRequest struct { // The hashed swap invoice preimage of the swap. SwapHash []byte `protobuf:"bytes,2,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"` // The deposit outpoints the client wishes to loop in. They implicitly state - // the swap amount. + // the swap amount if the amount field is not specified. If the amount field + // is specified, the server will use the total amount of the deposit + // outpoints minus the amount as the change amount. DepositOutpoints []string `protobuf:"bytes,3,rep,name=deposit_outpoints,json=depositOutpoints,proto3" json:"deposit_outpoints,omitempty"` // The swap invoice that the client wants the server to pay. SwapInvoice string `protobuf:"bytes,4,opt,name=swap_invoice,json=swapInvoice,proto3" json:"swap_invoice,omitempty"` @@ -424,6 +426,14 @@ type ServerStaticAddressLoopInRequest struct { // swap payment. If the timeout is reached the swap will be aborted on the // server side and the client can retry the swap with different parameters. PaymentTimeoutSeconds uint32 `protobuf:"varint,8,opt,name=payment_timeout_seconds,json=paymentTimeoutSeconds,proto3" json:"payment_timeout_seconds,omitempty"` + // The optional swap amount the client is attempting to swap. If specified the + // server will take out this amount from the total value of provided + // deposit_outpoints and will send the change back to the static address. If + // this results in dust change the server will reject the swap request. If the + // amount is not specified the server will use the total amount of the + // deposit_outpoints as swap amount without providing an additional flag - this + // is to maintain backwards compatibility. + Amount uint64 `protobuf:"varint,9,opt,name=amount,proto3" json:"amount,omitempty"` } func (x *ServerStaticAddressLoopInRequest) Reset() { @@ -514,6 +524,13 @@ func (x *ServerStaticAddressLoopInRequest) GetPaymentTimeoutSeconds() uint32 { return 0 } +func (x *ServerStaticAddressLoopInRequest) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + type ServerStaticAddressLoopInResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1050,7 +1067,7 @@ var file_staticaddr_proto_rawDesc = []byte{ 0x52, 0x0f, 0x6d, 0x75, 0x73, 0x69, 0x67, 0x32, 0x53, 0x77, 0x65, 0x65, 0x70, 0x53, 0x69, 0x67, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x82, 0x03, 0x0a, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x9a, 0x03, 0x0a, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x13, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, @@ -1074,129 +1091,130 @@ var file_staticaddr_proto_rawDesc = []byte{ 0x65, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x69, 0x6d, - 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x22, 0xe1, 0x02, 0x0a, 0x21, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2d, 0x0a, 0x13, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, - 0x68, 0x74, 0x6c, 0x63, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x68, 0x74, 0x6c, 0x63, 0x45, 0x78, 0x70, 0x69, 0x72, - 0x79, 0x12, 0x4c, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x5f, 0x68, 0x74, - 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, - 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x74, - 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x10, 0x73, - 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x12, - 0x4b, 0x0a, 0x12, 0x68, 0x69, 0x67, 0x68, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x68, 0x74, 0x6c, 0x63, - 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, - 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x74, 0x6c, 0x63, - 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x68, 0x69, 0x67, - 0x68, 0x46, 0x65, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x51, 0x0a, 0x15, - 0x65, 0x78, 0x74, 0x72, 0x65, 0x6d, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x68, 0x74, 0x6c, 0x63, - 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, - 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x74, 0x6c, 0x63, - 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x12, 0x65, 0x78, 0x74, - 0x72, 0x65, 0x6d, 0x65, 0x46, 0x65, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x22, - 0x4a, 0x0a, 0x15, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, - 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x6e, 0x63, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x06, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, - 0x12, 0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x07, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x22, 0xad, 0x02, 0x0a, 0x20, + 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x22, 0xe1, 0x02, 0x0a, 0x21, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x6f, 0x70, 0x49, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x13, 0x68, 0x74, 0x6c, + 0x63, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x68, 0x74, 0x6c, 0x63, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x74, 0x6c, 0x63, + 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x68, + 0x74, 0x6c, 0x63, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x4c, 0x0a, 0x12, 0x73, 0x74, 0x61, + 0x6e, 0x64, 0x61, 0x72, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, + 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x10, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x48, + 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x4b, 0x0a, 0x12, 0x68, 0x69, 0x67, 0x68, 0x5f, + 0x66, 0x65, 0x65, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x68, 0x69, 0x67, 0x68, 0x46, 0x65, 0x65, 0x48, 0x74, 0x6c, 0x63, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x51, 0x0a, 0x15, 0x65, 0x78, 0x74, 0x72, 0x65, 0x6d, 0x65, 0x5f, + 0x66, 0x65, 0x65, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x12, 0x65, 0x78, 0x74, 0x72, 0x65, 0x6d, 0x65, 0x46, 0x65, 0x65, 0x48, + 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x4a, 0x0a, 0x15, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, + 0x52, 0x06, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, + 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x66, 0x65, 0x65, 0x52, + 0x61, 0x74, 0x65, 0x22, 0xad, 0x02, 0x0a, 0x20, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, + 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, + 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, + 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, 0x4c, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, + 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x10, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x4b, 0x0a, 0x12, 0x68, 0x69, 0x67, 0x68, 0x5f, 0x66, 0x65, 0x65, 0x5f, + 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x0f, 0x68, 0x69, 0x67, 0x68, 0x46, 0x65, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x51, 0x0a, 0x15, 0x65, 0x78, 0x74, 0x72, 0x65, 0x6d, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, + 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x12, 0x65, 0x78, 0x74, 0x72, 0x65, 0x6d, 0x65, 0x46, 0x65, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x49, + 0x6e, 0x66, 0x6f, 0x22, 0x43, 0x0a, 0x15, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x74, 0x6c, + 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, + 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x06, 0x6e, 0x6f, + 0x6e, 0x63, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0c, 0x52, 0x04, 0x73, 0x69, 0x67, 0x73, 0x22, 0x23, 0x0a, 0x21, 0x50, 0x75, 0x73, 0x68, + 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x48, 0x74, 0x6c, + 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xc6, 0x02, + 0x0a, 0x25, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, + 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, + 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x62, 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, + 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, + 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, + 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, + 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x23, 0x0a, 0x0d, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x63, 0x0a, 0x10, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, + 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x44, 0x0a, 0x1a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x69, + 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x73, 0x69, 0x67, 0x22, 0x28, 0x0a, 0x26, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, 0x4c, 0x0a, - 0x12, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, - 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, - 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x10, 0x73, 0x74, 0x61, 0x6e, 0x64, - 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x4b, 0x0a, 0x12, 0x68, - 0x69, 0x67, 0x68, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, - 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, - 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x68, 0x69, 0x67, 0x68, 0x46, 0x65, 0x65, - 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x51, 0x0a, 0x15, 0x65, 0x78, 0x74, 0x72, - 0x65, 0x6d, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x6e, 0x66, - 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, - 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, - 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x12, 0x65, 0x78, 0x74, 0x72, 0x65, 0x6d, 0x65, - 0x46, 0x65, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x43, 0x0a, 0x15, 0x43, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, - 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0c, 0x52, 0x06, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, - 0x73, 0x69, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x69, 0x67, 0x73, - 0x22, 0x23, 0x0a, 0x21, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xc6, 0x02, 0x0a, 0x25, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, - 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, - 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, - 0x74, 0x78, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, - 0x12, 0x62, 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x6e, 0x66, 0x6f, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, + 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x26, 0x0a, 0x1c, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x56, 0x30, 0x10, 0x00, 0x32, 0xb5, + 0x04, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x57, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x65, 0x77, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x65, 0x77, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x59, 0x0a, 0x16, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, + 0x77, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, + 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, + 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x19, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x12, 0x29, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, + 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, + 0x0a, 0x19, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x12, 0x29, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, - 0x66, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, - 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x63, 0x0a, 0x10, 0x53, 0x69, 0x67, - 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, - 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, - 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x44, - 0x0a, 0x1a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, - 0x73, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, - 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, - 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x03, 0x73, 0x69, 0x67, 0x22, 0x28, 0x0a, 0x26, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, - 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, - 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x26, - 0x0a, 0x1c, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x06, - 0x0a, 0x02, 0x56, 0x30, 0x10, 0x00, 0x32, 0xb5, 0x04, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x74, 0x69, - 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x57, - 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x16, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x73, 0x12, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x72, 0x0a, 0x19, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, - 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x12, - 0x29, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x6f, - 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, - 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, - 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x6f, 0x70, 0x49, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x19, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, - 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x48, 0x74, 0x6c, 0x63, 0x53, - 0x69, 0x67, 0x73, 0x12, 0x29, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, - 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x48, - 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, - 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, - 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, - 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x1e, 0x50, - 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x12, 0x2e, 0x2e, - 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, - 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, - 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, - 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, - 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, - 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2d, - 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, - 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, - 0x73, 0x77, 0x61, 0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x73, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x1e, 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, + 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, + 0x73, 0x53, 0x69, 0x67, 0x73, 0x12, 0x2e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x50, 0x75, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x53, 0x77, 0x65, 0x65, 0x70, 0x6c, 0x65, 0x73, 0x73, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, + 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x73, 0x77, 0x61, 0x70, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/swapserverrpc/staticaddr.proto b/swapserverrpc/staticaddr.proto index 9aceefb3b..ce6890a28 100644 --- a/swapserverrpc/staticaddr.proto +++ b/swapserverrpc/staticaddr.proto @@ -107,7 +107,9 @@ message ServerStaticAddressLoopInRequest { bytes swap_hash = 2; // The deposit outpoints the client wishes to loop in. They implicitly state - // the swap amount. + // the swap amount if the amount field is not specified. If the amount field + // is specified, the server will use the total amount of the deposit + // outpoints minus the amount as the change amount. repeated string deposit_outpoints = 3; // The swap invoice that the client wants the server to pay. @@ -135,6 +137,17 @@ message ServerStaticAddressLoopInRequest { // swap payment. If the timeout is reached the swap will be aborted on the // server side and the client can retry the swap with different parameters. uint32 payment_timeout_seconds = 8; + + /* + The optional swap amount the client is attempting to swap. If specified the + server will take out this amount from the total value of provided + deposit_outpoints and will send the change back to the static address. If + this results in dust change the server will reject the swap request. If the + amount is not specified the server will use the total amount of the + deposit_outpoints as swap amount without providing an additional flag - this + is to maintain backwards compatibility. + */ + uint64 amount = 9; } message ServerStaticAddressLoopInResponse { From 57d96fcee7339fdb006cc61a0ba7c317805cfd58 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 2 Apr 2025 14:29:48 +0200 Subject: [PATCH 4/4] staticaddr: method to fetch deposits by outpoints --- loopdb/sqlc/querier.go | 1 + .../sqlc/queries/static_address_deposits.sql | 10 +++++ loopdb/sqlc/static_address_deposits.sql.go | 33 +++++++++++++++ staticaddr/deposit/interface.go | 4 ++ staticaddr/deposit/manager.go | 33 +++++++++++++++ staticaddr/deposit/manager_test.go | 7 ++++ staticaddr/deposit/sql_store.go | 42 +++++++++++++++++++ staticaddr/loopin/interface.go | 5 +++ 8 files changed, 135 insertions(+) diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 15b2e388f..26e4c0554 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -18,6 +18,7 @@ type Querier interface { CreateStaticAddress(ctx context.Context, arg CreateStaticAddressParams) error CreateWithdrawal(ctx context.Context, arg CreateWithdrawalParams) error CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error + DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error) FetchLiquidityParams(ctx context.Context) ([]byte, error) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error) diff --git a/loopdb/sqlc/queries/static_address_deposits.sql b/loopdb/sqlc/queries/static_address_deposits.sql index bc782bbb2..2987e469e 100644 --- a/loopdb/sqlc/queries/static_address_deposits.sql +++ b/loopdb/sqlc/queries/static_address_deposits.sql @@ -49,6 +49,16 @@ FROM WHERE deposit_id = $1; +-- name: DepositForOutpoint :one +SELECT + * +FROM + deposits +WHERE + tx_hash = $1 +AND + out_index = $2; + -- name: AllDeposits :many SELECT * diff --git a/loopdb/sqlc/static_address_deposits.sql.go b/loopdb/sqlc/static_address_deposits.sql.go index 75645c8e1..3a5c8076b 100644 --- a/loopdb/sqlc/static_address_deposits.sql.go +++ b/loopdb/sqlc/static_address_deposits.sql.go @@ -100,6 +100,39 @@ func (q *Queries) CreateDeposit(ctx context.Context, arg CreateDepositParams) er return err } +const depositForOutpoint = `-- name: DepositForOutpoint :one +SELECT + id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx +FROM + deposits +WHERE + tx_hash = $1 +AND + out_index = $2 +` + +type DepositForOutpointParams struct { + TxHash []byte + OutIndex int32 +} + +func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error) { + row := q.db.QueryRowContext(ctx, depositForOutpoint, arg.TxHash, arg.OutIndex) + var i Deposit + err := row.Scan( + &i.ID, + &i.DepositID, + &i.TxHash, + &i.OutIndex, + &i.Amount, + &i.ConfirmationHeight, + &i.TimeoutSweepPkScript, + &i.ExpirySweepTxid, + &i.FinalizedWithdrawalTx, + ) + return i, err +} + const getDeposit = `-- name: GetDeposit :one SELECT id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx diff --git a/staticaddr/deposit/interface.go b/staticaddr/deposit/interface.go index d38cfbaac..c18011bc3 100644 --- a/staticaddr/deposit/interface.go +++ b/staticaddr/deposit/interface.go @@ -26,6 +26,10 @@ type Store interface { // GetDeposit retrieves a deposit with depositID from the database. GetDeposit(ctx context.Context, depositID ID) (*Deposit, error) + // DepositForOutpoint retrieves the deposit with the given outpoint. + DepositForOutpoint(ctx context.Context, outpoint string) (*Deposit, + error) + // AllDeposits retrieves all deposits from the store. AllDeposits(ctx context.Context) ([]*Deposit, error) } diff --git a/staticaddr/deposit/manager.go b/staticaddr/deposit/manager.go index 44ca40d2c..cdea4949a 100644 --- a/staticaddr/deposit/manager.go +++ b/staticaddr/deposit/manager.go @@ -572,3 +572,36 @@ func (m *Manager) toActiveDeposits(outpoints *[]wire.OutPoint) ([]*FSM, return fsms, deposits } + +// DepositsForOutpoints returns all deposits that are behind the given +// outpoints. +func (m *Manager) DepositsForOutpoints(ctx context.Context, + outpoints []string) ([]*Deposit, error) { + + // Check for duplicates. + existingOutpoints := make(map[string]struct{}, len(outpoints)) + for i, o := range outpoints { + if _, ok := existingOutpoints[o]; ok { + return nil, fmt.Errorf("duplicate outpoint %s "+ + "at index %d", o, i) + } + existingOutpoints[o] = struct{}{} + } + + deposits := make([]*Deposit, 0, len(outpoints)) + for _, o := range outpoints { + op, err := wire.NewOutPointFromString(o) + if err != nil { + return nil, err + } + + deposit, err := m.cfg.Store.DepositForOutpoint(ctx, op.String()) + if err != nil { + return nil, err + } + + deposits = append(deposits, deposit) + } + + return deposits, nil +} diff --git a/staticaddr/deposit/manager_test.go b/staticaddr/deposit/manager_test.go index 007414c2c..74a361eb2 100644 --- a/staticaddr/deposit/manager_test.go +++ b/staticaddr/deposit/manager_test.go @@ -165,6 +165,13 @@ func (s *mockStore) GetDeposit(ctx context.Context, depositID ID) (*Deposit, return args.Get(0).(*Deposit), args.Error(1) } +func (s *mockStore) DepositForOutpoint(ctx context.Context, + outpoint string) (*Deposit, error) { + + args := s.Called(ctx, outpoint) + return args.Get(0).(*Deposit), args.Error(1) +} + func (s *mockStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { args := s.Called(ctx) return args.Get(0).([]*Deposit), args.Error(1) diff --git a/staticaddr/deposit/sql_store.go b/staticaddr/deposit/sql_store.go index 6a52ae282..1303c6a70 100644 --- a/staticaddr/deposit/sql_store.go +++ b/staticaddr/deposit/sql_store.go @@ -149,6 +149,48 @@ func (s *SqlStore) GetDeposit(ctx context.Context, id ID) (*Deposit, error) { return deposit, nil } +// DepositForOutpoint retrieves the deposit with the given outpoint from the +// database. +func (s *SqlStore) DepositForOutpoint(ctx context.Context, + outpoint string) (*Deposit, error) { + + var deposit *Deposit + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlReadOpts(), + func(q *sqlc.Queries) error { + op, err := wire.NewOutPointFromString(outpoint) + if err != nil { + return err + } + params := sqlc.DepositForOutpointParams{ + TxHash: op.Hash[:], + OutIndex: int32(op.Index), + } + row, err := q.DepositForOutpoint(ctx, params) + if err != nil { + return err + } + + latestUpdate, err := q.GetLatestDepositUpdate( + ctx, row.DepositID, + ) + if err != nil { + return err + } + + deposit, err = s.toDeposit(row, latestUpdate) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return deposit, nil +} + // AllDeposits retrieves all known deposits to our static address. func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { var allDeposits []*Deposit diff --git a/staticaddr/loopin/interface.go b/staticaddr/loopin/interface.go index e4e73862c..cf95f838b 100644 --- a/staticaddr/loopin/interface.go +++ b/staticaddr/loopin/interface.go @@ -48,6 +48,11 @@ type DepositManager interface { // invalid. TransitionDeposits(ctx context.Context, deposits []*deposit.Deposit, event fsm.EventType, expectedFinalState fsm.StateType) error + + // DepositsForOutpoints returns all deposits that behind the given + // outpoints. + DepositsForOutpoints(ctx context.Context, outpoints []string) ( + []*deposit.Deposit, error) } // StaticAddressLoopInStore provides access to the static address loop-in DB.