Skip to content
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

WIP: make sweeper deadline aware #7549

Closed

Conversation

yyforyongyu
Copy link
Member

@yyforyongyu yyforyongyu commented Mar 29, 2023

This is a very raw PoC PR to make the sweeper deadline-aware and fixes #4215.

Depends on:

In this PR, we extend the current sweeper to perform a fee bump if the input specifies it's deadline-aware. The sweeper will then continuously fee bump the sweeping tx in every new block if it's not confirmed.

An alternative approach would be fee bumping at the callsite, similar to how anchor sweeping is handled here. This approach IMO is not as good as the current one as a) the current approach has a deeper interface on sweeper and b) we can pre-define a set of fee bumping strategies so they can be easily applied based on specific conditions.

TODO:

@saubyk saubyk linked an issue Mar 29, 2023 that may be closed by this pull request
3 tasks

// validateStrategy checks that a given strategy is defined.
func validateStrategy(s Strategy) error {
if s < strategySentinel {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: could use switch statement?

case StrategyDeadlineAware:
deadlineHeight := f.deadline

target := deadlineHeight - currentHeight
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need to watch for underflow here which would make target very large


// If we are already behind deadline, fallback to conf target 2
// because that's minimal allowed value.
if target < 2 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If target = 1, then we're not technically behind the deadline as there is still 1 more block to go before the timelock expires.

Also, why is 2 the minimal value?

}

// decideConfTarget calculates a new conf target based on the strategy used.
func (f *feeBumpParam) decideConfTarget(currentHeight uint32) uint32 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This returns a conf target, but in low-fee environments I believe the conf target can give the same feerate. We want to change the feerate up to some maximum % of the HTLC value here. I think instead this should only use a feerate rather than a conf-target. This would mean passing in a fee-rate from the timeout resolver.

sweep/sweeper.go Outdated
@@ -677,6 +695,16 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) {
}
log.Tracef("input %v scheduled", outpoint)

if input.params.Strategy == StrategyDeadlineAware {
err := s.handleDeadlineAwareInput(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this also need to be called up above when the input already exists in the sweeper?

sweep/sweeper.go Outdated
params := &feeBumpParam{
strategy: StrategyDeadlineAware,
initialConfTarget: initialConfTarget,
deadline: uint32(currentHeight) + initialConfTarget,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the deadline should be its own parameter because I think the current usage is that call-sites usually have a deadline but pass a conf target that is unrelated to the deadline. So adding the conf target to the current height might be too aggressive in some cases

sweep/sweeper.go Outdated
},
}

// TODO(yy): Inside a goroutine?
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this will block?

sweep/sweeper.go Outdated
input: *op,
params: ParamsUpdate{
Fee: FeePreference{
ConfTarget: confTarget,
Copy link
Collaborator

Choose a reason for hiding this comment

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

missing the strategy field?

sweep/sweeper.go Outdated
Fee: FeePreference{
ConfTarget: confTarget,
},
Force: true,
Copy link
Collaborator

Choose a reason for hiding this comment

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

think we can get rid of Force, if it's uneconomical it's ok to give up

sweep/sweeper.go Outdated
return nil
}

func (s *UtxoSweeper) handleFeeBump(currentHeight uint32) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO this should only be for the deadline-aware inputs. The logic is a bit weird, but I think we can improve that later. We need to make sure that deadline-aware inputs don't stop after MaxSweepAttempts since they are going to be fee-bumping more since they are being re-submitted more in this function

@yyforyongyu yyforyongyu force-pushed the deadline-aware branch 3 times, most recently from 0be3133 to 30b61da Compare April 2, 2023 16:47
This commit adds a new service, `feeBumper` along with its interface
`Bumper`, to perform fee bumping over time sensitive inputs.

This commit focuses on building the framework for the bumper, and the
following commits will implement the actual strategies.
This commit adds a new strategy, `StrategyLinear`, to linearly increase
the fee rate used for a given input when the deadline approaches.
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

Nice work porting this over from that other codebase! Left some inline comments, more specific to the lnd context. Main thing is that I think we can break up the new abstract strategies into an interface to further encapsulate the logic and allow for composition/re-use.

log btclog.Logger

// feeLog is used by the fee bumper.
feeLog btclog.Logger
Copy link
Member

Choose a reason for hiding this comment

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

Could make this a sub-package to have it get its own distinct logger, and also be useable as a dep-minimized sub-module.


// Generate a pkScript to be used by the fee bumper for weight
// calculation.
pkScript, err := s.cfg.GenSweepScript()
Copy link
Member

Choose a reason for hiding this comment

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

Can we alternatively just make something that's P2TR sized? This way we don't need to extend the look ahead gap each time we restart.

}

// Add the input.
if err := weightEstimate.add(m.Input); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem to factor in the additional overhead of the type of inputs added to bump the transaction.


// Check if the request has already been processed.
//
// TODO(yy): allow overwrite?
Copy link
Member

Choose a reason for hiding this comment

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

Or return a nil error unless we expect the caller to properly handle the returned ErrAlreadyMonitored.

feeRateUsed = feeRate
}

// If the requst doesn't specify a broadcast height, use the manager's
Copy link
Member

Choose a reason for hiding this comment

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

requst -> request

var deadlineHeight uint32

// Calculate the deadline height if the conf target is set.
if req.ConfTarget != NoHeightInfo {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be ==? Since in this case, no conf target was specified, so we need to enforce a default conf target.

f.monitoredInputs.Range(func(_ *wire.OutPoint,
r *monitoredInput) bool {

f.wg.Add(1)
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if this should actually be done serially 🤔

Though with the way the sweeper works, all the goroutines will be serialized anyway, so it's nice to not have to block here so we can proceed other blocks/requests.

}

// Strategy specifies how the fee bump is performed.
type Strategy uint8
Copy link
Member

Choose a reason for hiding this comment

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

A slightly different way to represent this would be to have Strategy itself be an interface with methods for: seeing it things need to be bumped, and suggesting the proper fee rate. This would allow us to further encapsulate the logic of each strategy, and each strategy can also maintain its own state to make better decisions.

default:
feeLog.Errorf("Unknown strategy %v", r.strategy)
return r.currentFeeRate
}
}

// TODO(yy): cache the results?
func (f *feeBumper) feeRateForStrategyLinear(
Copy link
Member

Choose a reason for hiding this comment

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

If we make this part of the strategy interface, then we can easily add caching there (though seems to be a premature optimization at this point imo).

@Roasbeef
Copy link
Member

Superseded by #8667

@Roasbeef Roasbeef closed this Apr 22, 2024
@yyforyongyu yyforyongyu deleted the deadline-aware branch April 23, 2024 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants