Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions cmd/lncli/walletrpc_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ package main

import (
"context"
"errors"
"fmt"
"sort"

"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/urfave/cli"
)
Expand All @@ -22,6 +26,7 @@ func walletCommands() []cli.Command {
Subcommands: []cli.Command{
pendingSweepsCommand,
bumpFeeCommand,
bumpCloseFeeCommand,
},
},
}
Expand Down Expand Up @@ -168,3 +173,130 @@ func bumpFee(ctx *cli.Context) error {

return nil
}

var bumpCloseFeeCommand = cli.Command{
Name: "bumpclosefee",
Usage: "Bumps the fee of a channel closing transaction.",
ArgsUsage: "channel_point",
Description: `
This command allows the fee of a channel closing transaction to be
increased by using the child-pays-for-parent mechanism. It will instruct
the sweeper to sweep the anchor outputs of transactions in the set
of valid commitments for the specified channel at the requested fee
rate or confirmation target.
`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "conf_target",
Usage: "the number of blocks that the output should " +
"be swept on-chain within",
},
cli.Uint64Flag{
Name: "sat_per_byte",
Usage: "a manual fee expressed in sat/byte that " +
"should be used when sweeping the output",
},
},
Action: actionDecorator(bumpCloseFee),
}

func bumpCloseFee(ctx *cli.Context) error {
// Display the command's help message if we do not have the expected
// number of arguments/flags.
if ctx.NArg() != 1 {
return cli.ShowCommandHelp(ctx, "bumpclosefee")
}

// Validate the channel point.
channelPoint := ctx.Args().Get(0)
_, err := NewProtoOutPoint(channelPoint)
if err != nil {
return err
}

// Fetch all waiting close channels.
client, cleanUp := getClient(ctx)
defer cleanUp()

// Fetch waiting close channel commitments.
commitments, err := getWaitingCloseCommitments(client, channelPoint)
if err != nil {
return err
}

// Retrieve pending sweeps.
walletClient, cleanUp := getWalletClient(ctx)
defer cleanUp()

ctxb := context.Background()
sweeps, err := walletClient.PendingSweeps(
ctxb, &walletrpc.PendingSweepsRequest{},
)
if err != nil {
return err
}

// Match pending sweeps with commitments of the channel for which a bump
// is requested and bump their fees.
commitSet := map[string]struct{}{
commitments.LocalTxid: {},
commitments.RemoteTxid: {},
}
if commitments.RemotePendingTxid != "" {
commitSet[commitments.RemotePendingTxid] = struct{}{}
}

for _, sweep := range sweeps.PendingSweeps {
// Only bump anchor sweeps.
if sweep.WitnessType != walletrpc.WitnessType_COMMITMENT_ANCHOR {
continue
}

// Skip unrelated sweeps.
sweepTxID, err := chainhash.NewHash(sweep.Outpoint.TxidBytes)
if err != nil {
return err
}
if _, match := commitSet[sweepTxID.String()]; !match {
continue
}

// Bump fee of the anchor sweep.
fmt.Printf("Bumping fee of %v:%v\n",
sweepTxID, sweep.Outpoint.OutputIndex)

_, err = walletClient.BumpFee(ctxb, &walletrpc.BumpFeeRequest{
Outpoint: sweep.Outpoint,
TargetConf: uint32(ctx.Uint64("conf_target")),
SatPerByte: uint32(ctx.Uint64("sat_per_byte")),
Force: true,
})
Comment thread
joostjager marked this conversation as resolved.
Outdated
if err != nil {
return err
}
}

return nil
}

func getWaitingCloseCommitments(client lnrpc.LightningClient,
channelPoint string) (*lnrpc.PendingChannelsResponse_Commitments,
error) {

ctxb := context.Background()

req := &lnrpc.PendingChannelsRequest{}
resp, err := client.PendingChannels(ctxb, req)
if err != nil {
return nil, err
}

// Lookup the channel commit tx hashes.
for _, channel := range resp.WaitingCloseChannels {
if channel.Channel.ChannelPoint == channelPoint {
return channel.Commitments, nil
}
}

return nil, errors.New("channel not found")
}
205 changes: 205 additions & 0 deletions contractcourt/anchor_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package contractcourt

import (
"errors"
"io"
"sync"

"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/sweep"
)

// anchorResolver is a resolver that will attempt to sweep our anchor output.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this may actually need to live independent of any given channel arb. Otherwise, they'll never transition from pending close to fully closed, since there'll be a lingering contract that's unresolved.

Copy link
Copy Markdown
Contributor Author

@joostjager joostjager Mar 11, 2020

Choose a reason for hiding this comment

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

Added select clause to mark as resolved when the sweeper gives up. By that time, the 16 block csv delay has long expired, so if fees ever come down far enough, the anchor will probably be swept by one of the cleaning services that we envision.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When will the sweeper "give up"? Will check out the diff for more details...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is a comment about this in the Resolve method

type anchorResolver struct {
// anchorSignDescriptor contains the information that is required to
// sweep the anchor.
anchorSignDescriptor input.SignDescriptor

// anchor is the outpoint on the commitment transaction.
anchor wire.OutPoint

// resolved reflects if the contract has been fully resolved or not.
resolved bool

// broadcastHeight is the height that the original contract was
// broadcast to the main-chain at. We'll use this value to bound any
// historical queries to the chain for spends/confirmations.
broadcastHeight uint32

// chanPoint is the channel point of the original contract.
chanPoint wire.OutPoint

// currentReport stores the current state of the resolver for reporting
// over the rpc interface.
currentReport ContractReport

// reportLock prevents concurrent access to the resolver report.
reportLock sync.Mutex

contractResolverKit
}

// newAnchorResolver instantiates a new anchor resolver.
func newAnchorResolver(anchorSignDescriptor input.SignDescriptor,
anchor wire.OutPoint, broadcastHeight uint32,
chanPoint wire.OutPoint, resCfg ResolverConfig) *anchorResolver {

amt := btcutil.Amount(anchorSignDescriptor.Output.Value)

report := ContractReport{
Outpoint: anchor,
Type: ReportOutputAnchor,
Amount: amt,
LimboBalance: amt,
RecoveredBalance: 0,
}

r := &anchorResolver{
contractResolverKit: *newContractResolverKit(resCfg),
anchorSignDescriptor: anchorSignDescriptor,
anchor: anchor,
broadcastHeight: broadcastHeight,
chanPoint: chanPoint,
currentReport: report,
}

r.initLogger(r)

return r
}

// ResolverKey returns an identifier which should be globally unique for this
// particular resolver within the chain the original contract resides within.
func (c *anchorResolver) ResolverKey() []byte {
// The anchor resolver is stateless and doesn't need a database key.
return nil
}

// Resolve offers the anchor output to the sweeper and waits for it to be swept.
func (c *anchorResolver) Resolve() (ContractResolver, error) {
// Attempt to update the sweep parameters to the post-confirmation
Comment thread
joostjager marked this conversation as resolved.
Outdated
// situation. We don't want to force sweep anymore, because the anchor
// lost its special purpose to get the commitment confirmed. It is just
// an output that we want to sweep only if it is economical to do so.
relayFeeRate := c.Sweeper.RelayFeePerKW()

resultChan, err := c.Sweeper.UpdateParams(
c.anchor,
sweep.ParamsUpdate{
Fee: sweep.FeePreference{
FeeRate: relayFeeRate,
Comment thread
joostjager marked this conversation as resolved.
Outdated
},
Force: false,
},
)

// After a restart or when the remote force closes, the sweeper is not
// yet aware of the anchor. In that case, offer it as a new input to the
// sweeper. An exclusive group is not necessary anymore, because we know
// that this is the only anchor that can be swept.
if err == lnwallet.ErrNotMine {
anchorInput := input.MakeBaseInput(
Comment thread
halseth marked this conversation as resolved.
Outdated
&c.anchor,
input.CommitmentAnchor,
&c.anchorSignDescriptor,
c.broadcastHeight,
)

resultChan, err = c.Sweeper.SweepInput(
&anchorInput,
sweep.Params{
Fee: sweep.FeePreference{
FeeRate: relayFeeRate,
},
},
)
if err != nil {
return nil, err
}
}

var anchorRecovered bool
select {
case sweepRes := <-resultChan:
Comment thread
joostjager marked this conversation as resolved.
Outdated
switch sweepRes.Err {

// Anchor was swept successfully.
case nil:
c.log.Debugf("anchor swept by tx %v",
sweepRes.Tx.TxHash())

anchorRecovered = true

// Anchor was swept by someone else. This is possible after the
// 16 block csv lock.
case sweep.ErrRemoteSpend:
c.log.Warnf("our anchor spent by someone else")

// The sweeper gave up on sweeping the anchor. This happens
// after the maximum number of sweep attempts has been reached.
// See sweep.DefaultMaxSweepAttempts. Sweep attempts are
// interspaced with random delays picked from a range that
// increases exponentially.
//
// We consider the anchor as being lost.
case sweep.ErrTooManyAttempts:
Comment thread
joostjager marked this conversation as resolved.
Outdated
c.log.Warnf("anchor sweep abandoned")

// An unexpected error occurred.
default:
c.log.Errorf("unable to sweep anchor: %v", sweepRes.Err)

return nil, sweepRes.Err
}

case <-c.quit:
return nil, errResolverShuttingDown
}

// Update report to reflect that funds are no longer in limbo.
c.reportLock.Lock()
if anchorRecovered {
c.currentReport.RecoveredBalance = c.currentReport.LimboBalance
}
c.currentReport.LimboBalance = 0
c.reportLock.Unlock()

c.resolved = true
Comment thread
joostjager marked this conversation as resolved.
Outdated
return nil, nil
}

// Stop signals the resolver to cancel any current resolution processes, and
// suspend.
//
// NOTE: Part of the ContractResolver interface.
func (c *anchorResolver) Stop() {
close(c.quit)
}

// IsResolved returns true if the stored state in the resolve is fully
// resolved. In this case the target output can be forgotten.
//
// NOTE: Part of the ContractResolver interface.
func (c *anchorResolver) IsResolved() bool {
return c.resolved
}

// report returns a report on the resolution state of the contract.
func (c *anchorResolver) report() *ContractReport {
c.reportLock.Lock()
defer c.reportLock.Unlock()

reportCopy := c.currentReport
return &reportCopy
}

func (c *anchorResolver) Encode(w io.Writer) error {
return errors.New("serialization not supported")
}

// A compile time assertion to ensure anchorResolver meets the
// ContractResolver interface.
var _ ContractResolver = (*anchorResolver)(nil)
Loading