Skip to content

Commit

Permalink
(for fixup) sorted list of batches
Browse files Browse the repository at this point in the history
This commit will be squashed into commit
"sweepbatcher: add greedy batch selection algorithm"
  • Loading branch information
starius committed Jul 16, 2024
1 parent fece888 commit 60d06ab
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 78 deletions.
156 changes: 94 additions & 62 deletions sweepbatcher/greedy_batch_selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"sort"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
Expand All @@ -17,12 +18,15 @@ import (
// which minimizes costs, and adds the sweep to the batch. To accomplish this,
// it first collects fee details about the sweep being added, about a potential
// new batch composed of this sweep only, and about all existing batches. Then
// it passes the data to selectBatch() function, which emulates adding the sweep
// to each batch and creating new batch for the sweep, and calculates the costs
// of each alternative. Based on the estimates of selectBatch(), this method
// adds the sweep to the batch that results in the least overall fee increase,
// or creates new batch for it. If this method fails for whatever reason, the
// caller falls back to the simple algorithm (method handleSweep).
// it passes the data to selectBatches() function, which emulates adding the
// sweep to each batch and creating new batch for the sweep, and calculates the
// costs of each alternative. Based on the estimates of selectBatches(), this
// method adds the sweep to the batch that results in the least overall fee
// increase, or creates new batch for it. If the sweep is not accepted by an
// existing batch (may happen because of too distant timeouts), next batch is
// tried in the list returned by selectBatches(). If adding fails or new batch
// creation fails, this method returns an error. If this method fails for any
// reason, the caller falls back to the simple algorithm (method handleSweep).
func (b *Batcher) greedyAddSweep(ctx context.Context, sweep *sweep) error {
// Collect weight and fee rate info about the sweep and new batch.
sweepFeeDetails, newBatchFeeDetails, err := estimateSweepFeeIncrement(
Expand All @@ -44,43 +48,48 @@ func (b *Batcher) greedyAddSweep(ctx context.Context, sweep *sweep) error {
batches = append(batches, batchFeeDetails)
}

// Run the algorithm. Get batchId of the best batch, or newBatchSignal
// if the best option is to create new batch.
batchId, err := selectBatch(
// Run the algorithm. Get batchId of possible batches, sorted from best
// to worst.
batchesIds, err := selectBatches(
batches, sweepFeeDetails, newBatchFeeDetails,
)
if err != nil {
return fmt.Errorf("batch selection algorithm failed for sweep "+
"%x: %w", sweep.swapHash[:6], err)
}

// If the best option is to start new batch, do it.
if batchId == newBatchSignal {
return b.spinUpNewBatch(ctx, sweep)
}
// Try batches, starting with the best.
for _, batchId := range batchesIds {
// If the best option is to start new batch, do it.
if batchId == newBatchSignal {
return b.spinUpNewBatch(ctx, sweep)
}

// Locate the batch to add the sweep to.
bestBatch, has := b.batches[batchId]
if !has {
return fmt.Errorf("batch selection algorithm returned "+
"batch id %d which doesn't exist, for sweep %x",
batchId, sweep.swapHash[:6])
}
// Locate the batch to add the sweep to.
bestBatch, has := b.batches[batchId]
if !has {
return fmt.Errorf("batch selection algorithm returned "+
"batch id %d which doesn't exist, for sweep %x",
batchId, sweep.swapHash[:6])
}

// Add the sweep to the batch.
accepted, err := bestBatch.addSweep(ctx, sweep)
if err != nil {
return fmt.Errorf("batch selection algorithm returned "+
"batch id %d for sweep %x, but adding failed: %w",
batchId, sweep.swapHash[:6], err)
}
if !accepted {
return fmt.Errorf("batch selection algorithm returned "+
"batch id %d for sweep %x, but acceptance failed",
batchId, sweep.swapHash[:6])
// Add the sweep to the batch.
accepted, err := bestBatch.addSweep(ctx, sweep)
if err != nil {
return fmt.Errorf("batch selection algorithm returned "+
"batch id %d for sweep %x, but adding failed: "+
"%w", batchId, sweep.swapHash[:6], err)
}
if accepted {
return nil
}

log.Infof("Batch selection algorithm returned batch id %d for "+
"sweep %x, but acceptance failed.", batchId,
sweep.swapHash[:6])
}

return nil
return fmt.Errorf("no batch accepted sweep %x", sweep.swapHash[:6])
}

// estimateSweepFeeIncrement returns fee details for adding the sweep to a batch
Expand Down Expand Up @@ -159,8 +168,8 @@ func estimateBatchWeight(batch *batch) (feeDetails, error) {
var destAddr btcutil.Address
if theSweep.isExternalAddr {
if theSweep.destAddr == nil {
return feeDetails{}, errors.New("isExternalAddr=true, " +
"but destAddr is nil")
return feeDetails{}, errors.New("isExternalAddr=true," +
" but destAddr is nil")
}
destAddr = theSweep.destAddr
} else {
Expand Down Expand Up @@ -205,7 +214,7 @@ func estimateBatchWeight(batch *batch) (feeDetails, error) {
}

// newBatchSignal is the value that indicates a new batch. It is returned by
// selectBatch if the most cost-efficient action is new batch creation.
// selectBatches to encode new batch creation.
const newBatchSignal = -1

// feeDetails is either a batch or a sweep and it holds data important for
Expand Down Expand Up @@ -248,34 +257,45 @@ func (e1 feeDetails) combine(e2 feeDetails) feeDetails {
}
}

// selectBatch returns the index of the batch which adding to results in minimal
// costs. For each batch its fee rate and two weights are provided: weight in
// case of cooperative spending and weight in case non-cooperative spending
// (using preimages instead of taproot key spend). Also, a hint is provided to
// signal if the batch has to use non-cooperative spending path. The same data
// is also provided to the sweep for which we are selecting a batch to add. In
// case of the sweep weights are weight deltas resulted from adding the sweep.
// Finally, the same data is provided for new batch having this sweep only. The
// algorithm compares costs of adding the sweep to each existing batch, and
// costs of new batch creation for this sweep and returns BatchId of the winning
// batch. If the best option is to create a new batch, return newBatchSignal.
// Each fee details has also IsExternalAddr flag. There is a rule that sweeps
// having flag IsExternalAddr must go in individual batches. Cooperative
// spending is only available if all the sweeps support cooperative spending
// path.
func selectBatch(batches []feeDetails, sweep, oneSweepBatch feeDetails) (int32,
error) {
// selectBatches returns the list of id of batches sorted from best to worst.
// Creation a new batch is encoded as newBatchSignal. For each batch its fee
// rate and two weights are provided: weight in case of cooperative spending and
// weight in case non-cooperative spending (using preimages instead of taproot
// key spend). Also, a hint is provided to signal if the batch has to use
// non-cooperative spending path. The same data is also provided to the sweep
// for which we are selecting a batch to add. In case of the sweep weights are
// weight deltas resulted from adding the sweep. Finally, the same data is
// provided for new batch having this sweep only. The algorithm compares costs
// of adding the sweep to each existing batch, and costs of new batch creation
// for this sweep and returns BatchId of the winning batch. If the best option
// is to create a new batch, return newBatchSignal. Each fee details has also
// IsExternalAddr flag. There is a rule that sweeps having flag IsExternalAddr
// must go in individual batches. Cooperative spending is only available if all
// the sweeps support cooperative spending path.
func selectBatches(batches []feeDetails, sweep, oneSweepBatch feeDetails) (
[]int32, error) {

// If the sweep has IsExternalAddr flag, the sweep can't be added to
// a batch, so create new batch for it.
if sweep.IsExternalAddr {
return newBatchSignal, nil
return []int32{newBatchSignal}, nil
}

// alternative holds batch ID and its cost.
type alternative struct {
batchId int32
cost btcutil.Amount
}

// Create the list of possible actions and their costs.
alternatives := make([]alternative, 0, len(batches)+1)

// Track the best batch to add a sweep to. The default case is new batch
// creation with this sweep only in it. The cost is its full fee.
bestBatchId := int32(newBatchSignal)
bestCost := oneSweepBatch.fee()
alternatives = append(alternatives, alternative{
batchId: newBatchSignal,
cost: oneSweepBatch.fee(),
})

// Try to add the sweep to every batch, calculate the costs and
// find the batch adding to which results in minimum costs.
Expand All @@ -294,16 +314,28 @@ func selectBatch(batches []feeDetails, sweep, oneSweepBatch feeDetails) (int32,

// The cost must be positive, because we added a sweep.
if cost <= 0 {
return 0, fmt.Errorf("got non-positive cost of adding "+
"sweep to batch %d: %d", batch.BatchId, cost)
return nil, fmt.Errorf("got non-positive cost of "+
"adding sweep to batch %d: %d", batch.BatchId,
cost)
}

// Track the best batch, according to the costs.
if bestCost > cost {
bestBatchId = batch.BatchId
bestCost = cost
}
alternatives = append(alternatives, alternative{
batchId: batch.BatchId,
cost: cost,
})
}

// Sort the alternatives by cost. The lower the cost, the better.
sort.Slice(alternatives, func(i, j int) bool {
return alternatives[i].cost < alternatives[j].cost
})

// Collect batches IDs.
batchesIds := make([]int32, len(alternatives))
for i, alternative := range alternatives {
batchesIds[i] = alternative.batchId
}

return bestBatchId, nil
return batchesIds, nil
}
34 changes: 18 additions & 16 deletions sweepbatcher/greedy_batch_selection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,13 +367,13 @@ func TestEstimateBatchWeight(t *testing.T) {
}
}

// TestSelectBatch tests greedy batch selection algorithm.
func TestSelectBatch(t *testing.T) {
// TestSelectBatches tests greedy batch selection algorithm.
func TestSelectBatches(t *testing.T) {
cases := []struct {
name string
batches []feeDetails
sweep, oneSweepBatch feeDetails
wantBestBatchId int32
wantBestBatchesIds []int32
}{
{
name: "no existing batches",
Expand All @@ -388,7 +388,7 @@ func TestSelectBatch(t *testing.T) {
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchId: newBatchSignal,
wantBestBatchesIds: []int32{newBatchSignal},
},

{
Expand All @@ -411,7 +411,7 @@ func TestSelectBatch(t *testing.T) {
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchId: 1,
wantBestBatchesIds: []int32{1, newBatchSignal},
},

{
Expand All @@ -434,7 +434,7 @@ func TestSelectBatch(t *testing.T) {
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchId: newBatchSignal,
wantBestBatchesIds: []int32{newBatchSignal, 1},
},

{
Expand Down Expand Up @@ -463,7 +463,7 @@ func TestSelectBatch(t *testing.T) {
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchId: 1,
wantBestBatchesIds: []int32{1, newBatchSignal, 2},
},

{
Expand Down Expand Up @@ -492,7 +492,7 @@ func TestSelectBatch(t *testing.T) {
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchId: 2,
wantBestBatchesIds: []int32{2, newBatchSignal, 1},
},

{
Expand Down Expand Up @@ -523,7 +523,7 @@ func TestSelectBatch(t *testing.T) {
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
wantBestBatchId: newBatchSignal,
wantBestBatchesIds: []int32{newBatchSignal, 2, 1},
},

{
Expand Down Expand Up @@ -555,7 +555,7 @@ func TestSelectBatch(t *testing.T) {
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
wantBestBatchId: 2,
wantBestBatchesIds: []int32{2, newBatchSignal, 1},
},

{
Expand Down Expand Up @@ -586,7 +586,7 @@ func TestSelectBatch(t *testing.T) {
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
wantBestBatchId: newBatchSignal,
wantBestBatchesIds: []int32{newBatchSignal, 1, 2},
},

{
Expand Down Expand Up @@ -618,7 +618,7 @@ func TestSelectBatch(t *testing.T) {
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
wantBestBatchId: 1,
wantBestBatchesIds: []int32{1, newBatchSignal, 2},
},

{
Expand Down Expand Up @@ -649,7 +649,7 @@ func TestSelectBatch(t *testing.T) {
NonCoopWeight: nonCoopNewBatchWeight,
IsExternalAddr: true,
},
wantBestBatchId: newBatchSignal,
wantBestBatchesIds: []int32{newBatchSignal},
},

{
Expand Down Expand Up @@ -679,18 +679,20 @@ func TestSelectBatch(t *testing.T) {
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchId: 1,
wantBestBatchesIds: []int32{1, newBatchSignal},
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
gotBestBatchId, err := selectBatch(
gotBestBatchesIds, err := selectBatches(
tc.batches, tc.sweep, tc.oneSweepBatch,
)
require.NoError(t, err)
require.Equal(t, tc.wantBestBatchId, gotBestBatchId)
require.Equal(
t, tc.wantBestBatchesIds, gotBestBatchesIds,
)
})
}
}

0 comments on commit 60d06ab

Please sign in to comment.