Merge pull request #2198 from Roasbeef/sendall-rpc
multi: add ability to sweep all coins in the the wallet to an addr to sendcoins
This commit is contained in:
22 files changed
+1761
-712
No files matched your search
+7
-6
@@ -807,13 +807,13 @@ func (bo *breachedOutput) SignDesc() *lnwallet.SignDescriptor {
|
|||||||
return &bo.signDesc
|
return &bo.signDesc
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildWitness computes a valid witness that allows us to spend from the
|
// CraftInputScript computes a valid witness that allows us to spend from the
|
||||||
// breached output. It does so by first generating and memoizing the witness
|
// breached output. It does so by first generating and memoizing the witness
|
||||||
// generation function, which parameterized primarily by the witness type and
|
// generation function, which parameterized primarily by the witness type and
|
||||||
// sign descriptor. The method then returns the witness computed by invoking
|
// sign descriptor. The method then returns the witness computed by invoking
|
||||||
// this function on the first and subsequent calls.
|
// this function on the first and subsequent calls.
|
||||||
func (bo *breachedOutput) BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx,
|
func (bo *breachedOutput) CraftInputScript(signer lnwallet.Signer, txn *wire.MsgTx,
|
||||||
hashCache *txscript.TxSigHashes, txinIdx int) ([][]byte, error) {
|
hashCache *txscript.TxSigHashes, txinIdx int) (*lnwallet.InputScript, error) {
|
||||||
|
|
||||||
// First, we ensure that the witness generation function has been
|
// First, we ensure that the witness generation function has been
|
||||||
// initialized for this breached output.
|
// initialized for this breached output.
|
||||||
@@ -1082,15 +1082,16 @@ func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight int64,
|
|||||||
// First, we construct a valid witness for this outpoint and
|
// First, we construct a valid witness for this outpoint and
|
||||||
// transaction using the SpendableOutput's witness generation
|
// transaction using the SpendableOutput's witness generation
|
||||||
// function.
|
// function.
|
||||||
witness, err := so.BuildWitness(b.cfg.Signer, txn, hashCache,
|
inputScript, err := so.CraftInputScript(
|
||||||
idx)
|
b.cfg.Signer, txn, hashCache, idx,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, we add the witness to the transaction at the
|
// Then, we add the witness to the transaction at the
|
||||||
// appropriate txin index.
|
// appropriate txin index.
|
||||||
txn.TxIn[idx].Witness = witness
|
txn.TxIn[idx].Witness = inputScript.Witness
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-3
@@ -161,7 +161,13 @@ var sendCoinsCommand = cli.Command{
|
|||||||
Name: "addr",
|
Name: "addr",
|
||||||
Usage: "the BASE58 encoded bitcoin address to send coins to on-chain",
|
Usage: "the BASE58 encoded bitcoin address to send coins to on-chain",
|
||||||
},
|
},
|
||||||
// TODO(roasbeef): switch to BTC on command line? int may not be sufficient
|
cli.BoolFlag{
|
||||||
|
Name: "sweepall",
|
||||||
|
Usage: "if set, then the amount field will be ignored, " +
|
||||||
|
"and all the wallet will attempt to sweep all " +
|
||||||
|
"outputs within the wallet to the target " +
|
||||||
|
"address",
|
||||||
|
},
|
||||||
cli.Int64Flag{
|
cli.Int64Flag{
|
||||||
Name: "amt",
|
Name: "amt",
|
||||||
Usage: "the number of bitcoin denominated in satoshis to send",
|
Usage: "the number of bitcoin denominated in satoshis to send",
|
||||||
@@ -215,14 +221,18 @@ func sendCoins(ctx *cli.Context) error {
|
|||||||
amt = ctx.Int64("amt")
|
amt = ctx.Int64("amt")
|
||||||
case args.Present():
|
case args.Present():
|
||||||
amt, err = strconv.ParseInt(args.First(), 10, 64)
|
amt, err = strconv.ParseInt(args.First(), 10, 64)
|
||||||
default:
|
case !ctx.Bool("sweepall"):
|
||||||
return fmt.Errorf("Amount argument missing")
|
return fmt.Errorf("Amount argument missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to decode amount: %v", err)
|
return fmt.Errorf("unable to decode amount: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if amt != 0 && ctx.Bool("sweepall") {
|
||||||
|
return fmt.Errorf("amount cannot be set if attempting to " +
|
||||||
|
"sweep all coins out of the wallet")
|
||||||
|
}
|
||||||
|
|
||||||
ctxb := context.Background()
|
ctxb := context.Background()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@@ -232,6 +242,7 @@ func sendCoins(ctx *cli.Context) error {
|
|||||||
Amount: amt,
|
Amount: amt,
|
||||||
TargetConf: int32(ctx.Int64("conf_target")),
|
TargetConf: int32(ctx.Int64("conf_target")),
|
||||||
SatPerByte: ctx.Int64("sat_per_byte"),
|
SatPerByte: ctx.Int64("sat_per_byte"),
|
||||||
|
SendAll: ctx.Bool("sweepall"),
|
||||||
}
|
}
|
||||||
txid, err := client.SendCoins(ctxb, req)
|
txid, err := client.SendCoins(ctxb, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -476,7 +476,10 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
|
|||||||
// TODO: Use time-based sweeper and result chan.
|
// TODO: Use time-based sweeper and result chan.
|
||||||
var err error
|
var err error
|
||||||
h.sweepTx, err = h.Sweeper.CreateSweepTx(
|
h.sweepTx, err = h.Sweeper.CreateSweepTx(
|
||||||
[]sweep.Input{&input}, sweepConfTarget, 0,
|
[]sweep.Input{&input},
|
||||||
|
sweep.FeePreference{
|
||||||
|
ConfTarget: sweepConfTarget,
|
||||||
|
}, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1270,7 +1273,10 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) {
|
|||||||
//
|
//
|
||||||
// TODO: Use time-based sweeper and result chan.
|
// TODO: Use time-based sweeper and result chan.
|
||||||
c.sweepTx, err = c.Sweeper.CreateSweepTx(
|
c.sweepTx, err = c.Sweeper.CreateSweepTx(
|
||||||
[]sweep.Input{&input}, sweepConfTarget, 0,
|
[]sweep.Input{&input},
|
||||||
|
sweep.FeePreference{
|
||||||
|
ConfTarget: sweepConfTarget,
|
||||||
|
}, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+83
@@ -12617,12 +12617,95 @@ func testAbandonChannel(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
cleanupForceClose(t, net, net.Bob, chanPoint)
|
cleanupForceClose(t, net, net.Bob, chanPoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testSweepAllCoins tests that we're able to properly sweep all coins from the
|
||||||
|
// wallet into a single target address at the specified fee rate.
|
||||||
|
func testSweepAllCoins(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// First, we'll make a new node, ainz who'll we'll use to test wallet
|
||||||
|
// sweeping.
|
||||||
|
ainz, err := net.NewNode("Ainz", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create new node: %v", err)
|
||||||
|
}
|
||||||
|
defer shutdownAndAssert(net, t, ainz)
|
||||||
|
|
||||||
|
// Next, we'll give Ainz exactly 2 utxos of 1 BTC each, with one of
|
||||||
|
// them being p2wkh and the other being a n2wpkh address.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, ainz)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send coins to eve: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.SendCoinsNP2WKH(ctxt, btcutil.SatoshiPerBitcoin, ainz)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send coins to eve: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the two coins above mined, we'll now instruct ainz to sweep all
|
||||||
|
// the coins to an external address not under its control.
|
||||||
|
minerAddr, err := net.Miner.NewAddress()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create new miner addr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepReq := &lnrpc.SendCoinsRequest{
|
||||||
|
Addr: minerAddr.String(),
|
||||||
|
SendAll: true,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
_, err = ainz.SendCoins(ctxt, sweepReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to sweep coins: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll mine a block wish should include the sweep transaction we
|
||||||
|
// generated above.
|
||||||
|
block := mineBlocks(t, net, 1, 1)[0]
|
||||||
|
|
||||||
|
// The sweep transaction should have exactly two inputs as we only had
|
||||||
|
// two UTXOs in the wallet.
|
||||||
|
sweepTx := block.Transactions[1]
|
||||||
|
if len(sweepTx.TxIn) != 2 {
|
||||||
|
t.Fatalf("expected 2 inputs instead have %v", len(sweepTx.TxIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, Ainz should now have no coins at all within his wallet.
|
||||||
|
balReq := &lnrpc.WalletBalanceRequest{}
|
||||||
|
resp, err := ainz.WalletBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get ainz's balance: %v", err)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case resp.ConfirmedBalance != 0:
|
||||||
|
t.Fatalf("expected no confirmed balance, instead have %v",
|
||||||
|
resp.ConfirmedBalance)
|
||||||
|
|
||||||
|
case resp.UnconfirmedBalance != 0:
|
||||||
|
t.Fatalf("expected no unconfirmed balance, instead have %v",
|
||||||
|
resp.UnconfirmedBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we try again, but this time specifying an amount, then the call
|
||||||
|
// should fail.
|
||||||
|
sweepReq.Amount = 10000
|
||||||
|
_, err = ainz.SendCoins(ctxt, sweepReq)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("sweep attempt should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
test func(net *lntest.NetworkHarness, t *harnessTest)
|
test func(net *lntest.NetworkHarness, t *harnessTest)
|
||||||
}
|
}
|
||||||
|
|
||||||
var testsCases = []*testCase{
|
var testsCases = []*testCase{
|
||||||
|
{
|
||||||
|
name: "sweep coins",
|
||||||
|
test: testSweepAllCoins,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "onchain fund recovery",
|
name: "onchain fund recovery",
|
||||||
test: testOnchainFundRecovery,
|
test: testOnchainFundRecovery,
|
||||||
|
|||||||
+560
-544
File diff suppressed because it is too large.
Load diff
@@ -837,6 +837,13 @@ message SendCoinsRequest {
|
|||||||
|
|
||||||
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
|
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
|
||||||
int64 sat_per_byte = 5;
|
int64 sat_per_byte = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
If set, then the amount field will be ignored, and lnd will attempt to
|
||||||
|
send all the coins under control of the internal wallet to the specified
|
||||||
|
address.
|
||||||
|
*/
|
||||||
|
bool send_all = 6;
|
||||||
}
|
}
|
||||||
message SendCoinsResponse {
|
message SendCoinsResponse {
|
||||||
/// The transaction ID of the transaction
|
/// The transaction ID of the transaction
|
||||||
|
|||||||
@@ -1288,10 +1288,12 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"chain": {
|
"chain": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"title": "/ The blockchain the node is on (eg bitcoin, litecoin)"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"title": "/ The network the node is on (eg regtest, testnet, mainnet)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2782,6 +2784,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"description": "/ A manual fee rate set in sat/byte that should be used when crafting the transaction."
|
"description": "/ A manual fee rate set in sat/byte that should be used when crafting the transaction."
|
||||||
|
},
|
||||||
|
"send_all": {
|
||||||
|
"type": "boolean",
|
||||||
|
"format": "boolean",
|
||||||
|
"description": "*\nIf set, then the amount field will be ignored, and lnd will attempt to\nsend all the coins under control of the internal wallet to the specified\naddress."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -180,9 +180,10 @@ func (b *BtcWallet) SignOutputRaw(tx *wire.MsgTx,
|
|||||||
// TODO(roasbeef): generate sighash midstate if not present?
|
// TODO(roasbeef): generate sighash midstate if not present?
|
||||||
|
|
||||||
amt := signDesc.Output.Value
|
amt := signDesc.Output.Value
|
||||||
sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes,
|
sig, err := txscript.RawTxInWitnessSignature(
|
||||||
signDesc.InputIndex, amt, witnessScript, signDesc.HashType,
|
tx, signDesc.SigHashes, signDesc.InputIndex, amt,
|
||||||
privKey)
|
witnessScript, signDesc.HashType, privKey,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -227,8 +228,9 @@ func (b *BtcWallet) ComputeInputScript(tx *wire.MsgTx,
|
|||||||
// spend the p2sh output. The sigScript will contain only a
|
// spend the p2sh output. The sigScript will contain only a
|
||||||
// single push of the p2wkh witness program corresponding to
|
// single push of the p2wkh witness program corresponding to
|
||||||
// the matching public key of this address.
|
// the matching public key of this address.
|
||||||
p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash,
|
p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||||
b.netParams)
|
pubKeyHash, b.netParams,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -244,7 +246,7 @@ func (b *BtcWallet) ComputeInputScript(tx *wire.MsgTx,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
inputScript.ScriptSig = sigScript
|
inputScript.SigScript = sigScript
|
||||||
|
|
||||||
// Otherwise, this is a regular p2wkh output, so we include the
|
// Otherwise, this is a regular p2wkh output, so we include the
|
||||||
// witness program itself as the subscript to generate the proper
|
// witness program itself as the subscript to generate the proper
|
||||||
@@ -267,7 +269,8 @@ func (b *BtcWallet) ComputeInputScript(tx *wire.MsgTx,
|
|||||||
// TODO(roasbeef): adhere to passed HashType
|
// TODO(roasbeef): adhere to passed HashType
|
||||||
witnessScript, err := txscript.WitnessSignature(tx, signDesc.SigHashes,
|
witnessScript, err := txscript.WitnessSignature(tx, signDesc.SigHashes,
|
||||||
signDesc.InputIndex, signDesc.Output.Value, witnessProgram,
|
signDesc.InputIndex, signDesc.Output.Value, witnessProgram,
|
||||||
signDesc.HashType, privKey, true)
|
signDesc.HashType, privKey, true,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ type TransactionSubscription interface {
|
|||||||
type WalletController interface {
|
type WalletController interface {
|
||||||
// FetchInputInfo queries for the WalletController's knowledge of the
|
// FetchInputInfo queries for the WalletController's knowledge of the
|
||||||
// passed outpoint. If the base wallet determines this output is under
|
// passed outpoint. If the base wallet determines this output is under
|
||||||
// its control, then the original txout should be returned. Otherwise,
|
// its control, then the original txout should be returned. Otherwise,
|
||||||
// a non-nil error value of ErrNotMine should be returned instead.
|
// a non-nil error value of ErrNotMine should be returned instead.
|
||||||
FetchInputInfo(prevOut *wire.OutPoint) (*wire.TxOut, error)
|
FetchInputInfo(prevOut *wire.OutPoint) (*wire.TxOut, error)
|
||||||
|
|
||||||
|
|||||||
@@ -50,11 +50,15 @@ func (c *ChannelContribution) toChanConfig() channeldb.ChannelConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// InputScript represents any script inputs required to redeem a previous
|
// InputScript represents any script inputs required to redeem a previous
|
||||||
// output. This struct is used rather than just a witness, or scripSig in
|
// output. This struct is used rather than just a witness, or scripSig in order
|
||||||
// order to accommodate nested p2sh which utilizes both types of input scripts.
|
// to accommodate nested p2sh which utilizes both types of input scripts.
|
||||||
type InputScript struct {
|
type InputScript struct {
|
||||||
Witness [][]byte
|
// Witness is the full witness stack required to unlock this output.
|
||||||
ScriptSig []byte
|
Witness wire.TxWitness
|
||||||
|
|
||||||
|
// SigScript will only be populated if this is an input script sweeping
|
||||||
|
// a nested p2sh output.
|
||||||
|
SigScript []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelReservation represents an intent to open a lightning payment channel
|
// ChannelReservation represents an intent to open a lightning payment channel
|
||||||
|
|||||||
@@ -438,13 +438,15 @@ func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor
|
|||||||
"address %v", addresses[0])
|
"address %v", addresses[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
scriptSig, err := txscript.SignatureScript(tx, signDesc.InputIndex,
|
sigScript, err := txscript.SignatureScript(
|
||||||
signDesc.Output.PkScript, txscript.SigHashAll, privKey, true)
|
tx, signDesc.InputIndex, signDesc.Output.PkScript,
|
||||||
|
txscript.SigHashAll, privKey, true,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &InputScript{ScriptSig: scriptSig}, nil
|
return &InputScript{SigScript: sigScript}, nil
|
||||||
|
|
||||||
case txscript.WitnessV0PubKeyHashTy:
|
case txscript.WitnessV0PubKeyHashTy:
|
||||||
privKey := m.findKey(addresses[0].ScriptAddress(), signDesc.SingleTweak,
|
privKey := m.findKey(addresses[0].ScriptAddress(), signDesc.SingleTweak,
|
||||||
|
|||||||
+16
-5
@@ -747,14 +747,15 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
|
|||||||
signDesc.Output = info
|
signDesc.Output = info
|
||||||
signDesc.InputIndex = i
|
signDesc.InputIndex = i
|
||||||
|
|
||||||
inputScript, err := l.Cfg.Signer.ComputeInputScript(fundingTx,
|
inputScript, err := l.Cfg.Signer.ComputeInputScript(
|
||||||
&signDesc)
|
fundingTx, &signDesc,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.err <- err
|
req.err <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
txIn.SignatureScript = inputScript.ScriptSig
|
txIn.SignatureScript = inputScript.SigScript
|
||||||
txIn.Witness = inputScript.Witness
|
txIn.Witness = inputScript.Witness
|
||||||
pendingReservation.ourFundingInputScripts = append(
|
pendingReservation.ourFundingInputScripts = append(
|
||||||
pendingReservation.ourFundingInputScripts,
|
pendingReservation.ourFundingInputScripts,
|
||||||
@@ -958,7 +959,7 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
|
|||||||
if len(inputScripts) != 0 && len(txin.Witness) == 0 {
|
if len(inputScripts) != 0 && len(txin.Witness) == 0 {
|
||||||
// Attach the input scripts so we can verify it below.
|
// Attach the input scripts so we can verify it below.
|
||||||
txin.Witness = inputScripts[sigIndex].Witness
|
txin.Witness = inputScripts[sigIndex].Witness
|
||||||
txin.SignatureScript = inputScripts[sigIndex].ScriptSig
|
txin.SignatureScript = inputScripts[sigIndex].SigScript
|
||||||
|
|
||||||
// Fetch the alleged previous output along with the
|
// Fetch the alleged previous output along with the
|
||||||
// pkscript referenced by this input.
|
// pkscript referenced by this input.
|
||||||
@@ -1251,12 +1252,22 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
|
|||||||
l.limboMtx.Unlock()
|
l.limboMtx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithCoinSelectLock will execute the passed function closure in a
|
||||||
|
// synchronized manner preventing any coin selection operations from proceeding
|
||||||
|
// while the closure if executing. This can be seen as the ability to execute a
|
||||||
|
// function closure under an exclusive coin selection lock.
|
||||||
|
func (l *LightningWallet) WithCoinSelectLock(f func() error) error {
|
||||||
|
l.coinSelectMtx.Lock()
|
||||||
|
defer l.coinSelectMtx.Unlock()
|
||||||
|
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
|
||||||
// selectCoinsAndChange performs coin selection in order to obtain witness
|
// selectCoinsAndChange performs coin selection in order to obtain witness
|
||||||
// outputs which sum to at least 'numCoins' amount of satoshis. If coin
|
// outputs which sum to at least 'numCoins' amount of satoshis. If coin
|
||||||
// selection is successful/possible, then the selected coins are available
|
// selection is successful/possible, then the selected coins are available
|
||||||
// within the passed contribution's inputs. If necessary, a change address will
|
// within the passed contribution's inputs. If necessary, a change address will
|
||||||
// also be generated.
|
// also be generated.
|
||||||
// TODO(roasbeef): remove hardcoded fees.
|
|
||||||
func (l *LightningWallet) selectCoinsAndChange(feeRate SatPerKWeight,
|
func (l *LightningWallet) selectCoinsAndChange(feeRate SatPerKWeight,
|
||||||
amt btcutil.Amount, minConfs int32,
|
amt btcutil.Amount, minConfs int32,
|
||||||
contribution *ChannelContribution) error {
|
contribution *ChannelContribution) error {
|
||||||
|
|||||||
+97
-15
@@ -69,6 +69,16 @@ const (
|
|||||||
// broadcast a revoked commitment, but then also immediately attempt to
|
// broadcast a revoked commitment, but then also immediately attempt to
|
||||||
// go to the second level to claim the HTLC.
|
// go to the second level to claim the HTLC.
|
||||||
HtlcSecondLevelRevoke WitnessType = 9
|
HtlcSecondLevelRevoke WitnessType = 9
|
||||||
|
|
||||||
|
// WitnessKeyHash is a witness type that allows us to spend a regular
|
||||||
|
// p2wkh output that's sent to an output which is under complete
|
||||||
|
// control of the backing wallet.
|
||||||
|
WitnessKeyHash WitnessType = 10
|
||||||
|
|
||||||
|
// NestedWitnessKeyHash is a witness type that allows us to sweep an
|
||||||
|
// output that sends to a nested P2SH script that pays to a key solely
|
||||||
|
// under our control. The witness generated needs to include the
|
||||||
|
NestedWitnessKeyHash WitnessType = 11
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stirng returns a human readable version of the target WitnessType.
|
// Stirng returns a human readable version of the target WitnessType.
|
||||||
@@ -110,18 +120,22 @@ func (wt WitnessType) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WitnessGenerator represents a function which is able to generate the final
|
// WitnessGenerator represents a function which is able to generate the final
|
||||||
// witness for a particular public key script. This function acts as an
|
// witness for a particular public key script. Additionally, if required, this
|
||||||
// abstraction layer, hiding the details of the underlying script.
|
// function will also return the sigScript for spending nested P2SH witness
|
||||||
|
// outputs. This function acts as an abstraction layer, hiding the details of
|
||||||
|
// the underlying script.
|
||||||
type WitnessGenerator func(tx *wire.MsgTx, hc *txscript.TxSigHashes,
|
type WitnessGenerator func(tx *wire.MsgTx, hc *txscript.TxSigHashes,
|
||||||
inputIndex int) ([][]byte, error)
|
inputIndex int) (*InputScript, error)
|
||||||
|
|
||||||
// GenWitnessFunc will return a WitnessGenerator function that an output
|
// GenWitnessFunc will return a WitnessGenerator function that an output uses
|
||||||
// uses to generate the witness for a sweep transaction.
|
// to generate the witness and optionally the sigScript for a sweep
|
||||||
|
// transaction. The sigScript will be generated if the witness type warrants
|
||||||
|
// one for spending, such as the NestedWitnessKeyHash witness type.
|
||||||
func (wt WitnessType) GenWitnessFunc(signer Signer,
|
func (wt WitnessType) GenWitnessFunc(signer Signer,
|
||||||
descriptor *SignDescriptor) WitnessGenerator {
|
descriptor *SignDescriptor) WitnessGenerator {
|
||||||
|
|
||||||
return func(tx *wire.MsgTx, hc *txscript.TxSigHashes,
|
return func(tx *wire.MsgTx, hc *txscript.TxSigHashes,
|
||||||
inputIndex int) ([][]byte, error) {
|
inputIndex int) (*InputScript, error) {
|
||||||
|
|
||||||
desc := descriptor
|
desc := descriptor
|
||||||
desc.SigHashes = hc
|
desc.SigHashes = hc
|
||||||
@@ -129,34 +143,102 @@ func (wt WitnessType) GenWitnessFunc(signer Signer,
|
|||||||
|
|
||||||
switch wt {
|
switch wt {
|
||||||
case CommitmentTimeLock:
|
case CommitmentTimeLock:
|
||||||
return CommitSpendTimeout(signer, desc, tx)
|
witness, err := CommitSpendTimeout(signer, desc, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case CommitmentNoDelay:
|
case CommitmentNoDelay:
|
||||||
return CommitSpendNoDelay(signer, desc, tx)
|
witness, err := CommitSpendNoDelay(signer, desc, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case CommitmentRevoke:
|
case CommitmentRevoke:
|
||||||
return CommitSpendRevoke(signer, desc, tx)
|
witness, err := CommitSpendRevoke(signer, desc, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case HtlcOfferedRevoke:
|
case HtlcOfferedRevoke:
|
||||||
return ReceiverHtlcSpendRevoke(signer, desc, tx)
|
witness, err := ReceiverHtlcSpendRevoke(signer, desc, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case HtlcAcceptedRevoke:
|
case HtlcAcceptedRevoke:
|
||||||
return SenderHtlcSpendRevoke(signer, desc, tx)
|
witness, err := SenderHtlcSpendRevoke(signer, desc, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case HtlcOfferedTimeoutSecondLevel:
|
case HtlcOfferedTimeoutSecondLevel:
|
||||||
return HtlcSecondLevelSpend(signer, desc, tx)
|
witness, err := HtlcSecondLevelSpend(signer, desc, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case HtlcAcceptedSuccessSecondLevel:
|
case HtlcAcceptedSuccessSecondLevel:
|
||||||
return HtlcSecondLevelSpend(signer, desc, tx)
|
witness, err := HtlcSecondLevelSpend(signer, desc, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case HtlcOfferedRemoteTimeout:
|
case HtlcOfferedRemoteTimeout:
|
||||||
// We pass in a value of -1 for the timeout, as we
|
// We pass in a value of -1 for the timeout, as we
|
||||||
// expect the caller to have already set the lock time
|
// expect the caller to have already set the lock time
|
||||||
// value.
|
// value.
|
||||||
return receiverHtlcSpendTimeout(signer, desc, tx, -1)
|
witness, err := receiverHtlcSpendTimeout(signer, desc, tx, -1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case HtlcSecondLevelRevoke:
|
case HtlcSecondLevelRevoke:
|
||||||
return htlcSpendRevoke(signer, desc, tx)
|
witness, err := htlcSpendRevoke(signer, desc, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
case WitnessKeyHash:
|
||||||
|
fallthrough
|
||||||
|
case NestedWitnessKeyHash:
|
||||||
|
return signer.ComputeInputScript(tx, desc)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown witness type: %v", wt)
|
return nil, fmt.Errorf("unknown witness type: %v", wt)
|
||||||
|
|||||||
+134
-64
@@ -39,6 +39,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/macaroons"
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
"github.com/lightningnetwork/lnd/routing"
|
"github.com/lightningnetwork/lnd/routing"
|
||||||
"github.com/lightningnetwork/lnd/signal"
|
"github.com/lightningnetwork/lnd/signal"
|
||||||
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
"github.com/lightningnetwork/lnd/zpay32"
|
"github.com/lightningnetwork/lnd/zpay32"
|
||||||
"github.com/tv42/zbase32"
|
"github.com/tv42/zbase32"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
@@ -637,53 +638,7 @@ func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
txHash := tx.TxHash()
|
txHash := tx.TxHash()
|
||||||
return &txHash, err
|
return &txHash, nil
|
||||||
}
|
|
||||||
|
|
||||||
// determineFeePerKw will determine the fee in sat/kw that should be paid given
|
|
||||||
// an estimator, a confirmation target, and a manual value for sat/byte. A value
|
|
||||||
// is chosen based on the two free parameters as one, or both of them can be
|
|
||||||
// zero.
|
|
||||||
func determineFeePerKw(feeEstimator lnwallet.FeeEstimator, targetConf int32,
|
|
||||||
feePerByte int64) (lnwallet.SatPerKWeight, error) {
|
|
||||||
|
|
||||||
switch {
|
|
||||||
// If the target number of confirmations is set, then we'll use that to
|
|
||||||
// consult our fee estimator for an adequate fee.
|
|
||||||
case targetConf != 0:
|
|
||||||
feePerKw, err := feeEstimator.EstimateFeePerKW(
|
|
||||||
uint32(targetConf),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("unable to query fee "+
|
|
||||||
"estimator: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return feePerKw, nil
|
|
||||||
|
|
||||||
// If a manual sat/byte fee rate is set, then we'll use that directly.
|
|
||||||
// We'll need to convert it to sat/kw as this is what we use internally.
|
|
||||||
case feePerByte != 0:
|
|
||||||
feePerKW := lnwallet.SatPerKVByte(feePerByte * 1000).FeePerKWeight()
|
|
||||||
if feePerKW < lnwallet.FeePerKwFloor {
|
|
||||||
rpcsLog.Infof("Manual fee rate input of %d sat/kw is "+
|
|
||||||
"too low, using %d sat/kw instead", feePerKW,
|
|
||||||
lnwallet.FeePerKwFloor)
|
|
||||||
feePerKW = lnwallet.FeePerKwFloor
|
|
||||||
}
|
|
||||||
return feePerKW, nil
|
|
||||||
|
|
||||||
// Otherwise, we'll attempt a relaxed confirmation target for the
|
|
||||||
// transaction
|
|
||||||
default:
|
|
||||||
feePerKw, err := feeEstimator.EstimateFeePerKW(6)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("unable to query fee estimator: "+
|
|
||||||
"%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return feePerKw, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUnspent returns useful information about each unspent output owned by
|
// ListUnspent returns useful information about each unspent output owned by
|
||||||
@@ -803,20 +758,100 @@ func (r *rpcServer) SendCoins(ctx context.Context,
|
|||||||
|
|
||||||
// Based on the passed fee related parameters, we'll determine an
|
// Based on the passed fee related parameters, we'll determine an
|
||||||
// appropriate fee rate for this transaction.
|
// appropriate fee rate for this transaction.
|
||||||
feePerKw, err := determineFeePerKw(
|
satPerKw := lnwallet.SatPerKVByte(in.SatPerByte * 1000).FeePerKWeight()
|
||||||
r.server.cc.feeEstimator, in.TargetConf, in.SatPerByte,
|
feePerKw, err := sweep.DetermineFeePerKw(
|
||||||
|
r.server.cc.feeEstimator, sweep.FeePreference{
|
||||||
|
ConfTarget: uint32(in.TargetConf),
|
||||||
|
FeeRate: satPerKw,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rpcsLog.Infof("[sendcoins] addr=%v, amt=%v, sat/kw=%v", in.Addr,
|
rpcsLog.Infof("[sendcoins] addr=%v, amt=%v, sat/kw=%v, sweep_all=%v",
|
||||||
btcutil.Amount(in.Amount), int64(feePerKw))
|
in.Addr, btcutil.Amount(in.Amount), int64(feePerKw),
|
||||||
|
in.SendAll)
|
||||||
|
|
||||||
paymentMap := map[string]int64{in.Addr: in.Amount}
|
var txid *chainhash.Hash
|
||||||
txid, err := r.sendCoinsOnChain(paymentMap, feePerKw)
|
|
||||||
if err != nil {
|
wallet := r.server.cc.wallet
|
||||||
return nil, err
|
|
||||||
|
// If the send all flag is active, then we'll attempt to sweep all the
|
||||||
|
// coins in the wallet in a single transaction (if possible),
|
||||||
|
// otherwise, we'll respect the amount, and attempt a regular 2-output
|
||||||
|
// send.
|
||||||
|
if in.SendAll {
|
||||||
|
// At this point, the amount shouldn't be set since we've been
|
||||||
|
// instructed to sweep all the coins from the wallet.
|
||||||
|
if in.Amount != 0 {
|
||||||
|
return nil, fmt.Errorf("amount set while SendAll is " +
|
||||||
|
"active")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additionally, we'll need to convert the sweep address passed
|
||||||
|
// into a useable struct, and also query for the latest block
|
||||||
|
// height so we can properly construct the transaction.
|
||||||
|
sweepAddr, err := btcutil.DecodeAddress(
|
||||||
|
in.Addr, activeNetParams.Params,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, bestHeight, err := r.server.cc.chainIO.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the sweeper instance created, we can now generate a
|
||||||
|
// transaction that will sweep ALL outputs from the wallet in a
|
||||||
|
// single transaction. This will be generated in a concurrent
|
||||||
|
// safe manner, so no need to worry about locking.
|
||||||
|
sweepTxPkg, err := sweep.CraftSweepAllTx(
|
||||||
|
feePerKw, uint32(bestHeight), sweepAddr, wallet,
|
||||||
|
wallet.WalletController, wallet.WalletController,
|
||||||
|
r.server.cc.feeEstimator, r.server.cc.signer,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcsLog.Debugf("Sweeping all coins from wallet to addr=%v, "+
|
||||||
|
"with tx=%v", in.Addr, spew.Sdump(sweepTxPkg.SweepTx))
|
||||||
|
|
||||||
|
// As our sweep transaction was created, successfully, we'll
|
||||||
|
// now attempt to publish it, cancelling the sweep pkg to
|
||||||
|
// return all outputs if it fails.
|
||||||
|
err = wallet.PublishTransaction(sweepTxPkg.SweepTx)
|
||||||
|
if err != nil {
|
||||||
|
sweepTxPkg.CancelSweepAttempt()
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unable to broadcast sweep "+
|
||||||
|
"transaction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepTXID := sweepTxPkg.SweepTx.TxHash()
|
||||||
|
txid = &sweepTXID
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// We'll now construct out payment map, and use the wallet's
|
||||||
|
// coin selection synchronization method to ensure that no coin
|
||||||
|
// selection (funding, sweep alls, other sends) can proceed
|
||||||
|
// while we instruct the wallet to send this transaction.
|
||||||
|
paymentMap := map[string]int64{in.Addr: in.Amount}
|
||||||
|
err := wallet.WithCoinSelectLock(func() error {
|
||||||
|
newTXID, err := r.sendCoinsOnChain(paymentMap, feePerKw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
txid = newTXID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rpcsLog.Infof("[sendcoins] spend generated txid: %v", txid.String())
|
rpcsLog.Infof("[sendcoins] spend generated txid: %v", txid.String())
|
||||||
@@ -831,8 +866,12 @@ func (r *rpcServer) SendMany(ctx context.Context,
|
|||||||
|
|
||||||
// Based on the passed fee related parameters, we'll determine an
|
// Based on the passed fee related parameters, we'll determine an
|
||||||
// appropriate fee rate for this transaction.
|
// appropriate fee rate for this transaction.
|
||||||
feePerKw, err := determineFeePerKw(
|
satPerKw := lnwallet.SatPerKVByte(in.SatPerByte * 1000).FeePerKWeight()
|
||||||
r.server.cc.feeEstimator, in.TargetConf, in.SatPerByte,
|
feePerKw, err := sweep.DetermineFeePerKw(
|
||||||
|
r.server.cc.feeEstimator, sweep.FeePreference{
|
||||||
|
ConfTarget: uint32(in.TargetConf),
|
||||||
|
FeeRate: satPerKw,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -841,7 +880,24 @@ func (r *rpcServer) SendMany(ctx context.Context,
|
|||||||
rpcsLog.Infof("[sendmany] outputs=%v, sat/kw=%v",
|
rpcsLog.Infof("[sendmany] outputs=%v, sat/kw=%v",
|
||||||
spew.Sdump(in.AddrToAmount), int64(feePerKw))
|
spew.Sdump(in.AddrToAmount), int64(feePerKw))
|
||||||
|
|
||||||
txid, err := r.sendCoinsOnChain(in.AddrToAmount, feePerKw)
|
var txid *chainhash.Hash
|
||||||
|
|
||||||
|
// We'll attempt to send to the target set of outputs, ensuring that we
|
||||||
|
// synchronize with any other ongoing coin selection attempts which
|
||||||
|
// happen to also be concurrently executing.
|
||||||
|
wallet := r.server.cc.wallet
|
||||||
|
err = wallet.WithCoinSelectLock(func() error {
|
||||||
|
sendManyTXID, err := r.sendCoinsOnChain(
|
||||||
|
in.AddrToAmount, feePerKw,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
txid = sendManyTXID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1170,8 +1226,12 @@ func (r *rpcServer) OpenChannel(in *lnrpc.OpenChannelRequest,
|
|||||||
|
|
||||||
// Based on the passed fee related parameters, we'll determine an
|
// Based on the passed fee related parameters, we'll determine an
|
||||||
// appropriate fee rate for the funding transaction.
|
// appropriate fee rate for the funding transaction.
|
||||||
feeRate, err := determineFeePerKw(
|
satPerKw := lnwallet.SatPerKVByte(in.SatPerByte * 1000).FeePerKWeight()
|
||||||
r.server.cc.feeEstimator, in.TargetConf, in.SatPerByte,
|
feeRate, err := sweep.DetermineFeePerKw(
|
||||||
|
r.server.cc.feeEstimator, sweep.FeePreference{
|
||||||
|
ConfTarget: uint32(in.TargetConf),
|
||||||
|
FeeRate: satPerKw,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1317,8 +1377,12 @@ func (r *rpcServer) OpenChannelSync(ctx context.Context,
|
|||||||
|
|
||||||
// Based on the passed fee related parameters, we'll determine an
|
// Based on the passed fee related parameters, we'll determine an
|
||||||
// appropriate fee rate for the funding transaction.
|
// appropriate fee rate for the funding transaction.
|
||||||
feeRate, err := determineFeePerKw(
|
satPerKw := lnwallet.SatPerKVByte(in.SatPerByte * 1000).FeePerKWeight()
|
||||||
r.server.cc.feeEstimator, in.TargetConf, in.SatPerByte,
|
feeRate, err := sweep.DetermineFeePerKw(
|
||||||
|
r.server.cc.feeEstimator, sweep.FeePreference{
|
||||||
|
ConfTarget: uint32(in.TargetConf),
|
||||||
|
FeeRate: satPerKw,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1506,8 +1570,14 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
|
|||||||
// Based on the passed fee related parameters, we'll determine
|
// Based on the passed fee related parameters, we'll determine
|
||||||
// an appropriate fee rate for the cooperative closure
|
// an appropriate fee rate for the cooperative closure
|
||||||
// transaction.
|
// transaction.
|
||||||
feeRate, err := determineFeePerKw(
|
satPerKw := lnwallet.SatPerKVByte(
|
||||||
r.server.cc.feeEstimator, in.TargetConf, in.SatPerByte,
|
in.SatPerByte * 1000,
|
||||||
|
).FeePerKWeight()
|
||||||
|
feeRate, err := sweep.DetermineFeePerKw(
|
||||||
|
r.server.cc.feeEstimator, sweep.FeePreference{
|
||||||
|
ConfTarget: uint32(in.TargetConf),
|
||||||
|
FeeRate: satPerKw,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -608,7 +608,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.sweeper = sweep.New(&sweep.UtxoSweeperConfig{
|
s.sweeper = sweep.New(&sweep.UtxoSweeperConfig{
|
||||||
Estimator: cc.feeEstimator,
|
FeeEstimator: cc.feeEstimator,
|
||||||
GenSweepScript: func() ([]byte, error) {
|
GenSweepScript: func() ([]byte, error) {
|
||||||
return newSweepPkScript(cc.wallet)
|
return newSweepPkScript(cc.wallet)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package sweep
|
package sweep
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mockFeeEstimator implements a mock fee estimator. It closely resembles
|
// mockFeeEstimator implements a mock fee estimator. It closely resembles
|
||||||
@@ -13,6 +14,8 @@ type mockFeeEstimator struct {
|
|||||||
|
|
||||||
relayFee lnwallet.SatPerKWeight
|
relayFee lnwallet.SatPerKWeight
|
||||||
|
|
||||||
|
blocksToFee map[uint32]lnwallet.SatPerKWeight
|
||||||
|
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,8 +23,9 @@ func newMockFeeEstimator(feePerKW,
|
|||||||
relayFee lnwallet.SatPerKWeight) *mockFeeEstimator {
|
relayFee lnwallet.SatPerKWeight) *mockFeeEstimator {
|
||||||
|
|
||||||
return &mockFeeEstimator{
|
return &mockFeeEstimator{
|
||||||
feePerKW: feePerKW,
|
feePerKW: feePerKW,
|
||||||
relayFee: relayFee,
|
relayFee: relayFee,
|
||||||
|
blocksToFee: make(map[uint32]lnwallet.SatPerKWeight),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +45,10 @@ func (e *mockFeeEstimator) EstimateFeePerKW(numBlocks uint32) (
|
|||||||
e.lock.Lock()
|
e.lock.Lock()
|
||||||
defer e.lock.Unlock()
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
|
if fee, ok := e.blocksToFee[numBlocks]; ok {
|
||||||
|
return fee, nil
|
||||||
|
}
|
||||||
|
|
||||||
return e.feePerKW, nil
|
return e.feePerKW, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-20
@@ -6,7 +6,10 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Input contains all data needed to construct a sweep tx input.
|
// Input represents an abstract UTXO which is to be spent using a sweeping
|
||||||
|
// transaction. The method provided give the caller all information needed to
|
||||||
|
// construct a valid input within a sweeping transaction to sweep this
|
||||||
|
// lingering UTXO.
|
||||||
type Input interface {
|
type Input interface {
|
||||||
// Outpoint returns the reference to the output being spent, used to
|
// Outpoint returns the reference to the output being spent, used to
|
||||||
// construct the corresponding transaction input.
|
// construct the corresponding transaction input.
|
||||||
@@ -21,12 +24,14 @@ type Input interface {
|
|||||||
// that spends this output.
|
// that spends this output.
|
||||||
SignDesc() *lnwallet.SignDescriptor
|
SignDesc() *lnwallet.SignDescriptor
|
||||||
|
|
||||||
// BuildWitness returns a valid witness allowing this output to be
|
// CraftInputScript returns a valid set of input scripts allowing this
|
||||||
// spent, the witness should be attached to the transaction at the
|
// output to be spent. The returns input scripts should target the
|
||||||
// location determined by the given `txinIdx`.
|
// input at location txIndex within the passed transaction. The input
|
||||||
BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx,
|
// scripts generated by this method support spending p2wkh, p2wsh, and
|
||||||
|
// also nested p2sh outputs.
|
||||||
|
CraftInputScript(signer lnwallet.Signer, txn *wire.MsgTx,
|
||||||
hashCache *txscript.TxSigHashes,
|
hashCache *txscript.TxSigHashes,
|
||||||
txinIdx int) ([][]byte, error)
|
txinIdx int) (*lnwallet.InputScript, error)
|
||||||
|
|
||||||
// BlocksToMaturity returns the relative timelock, as a number of
|
// BlocksToMaturity returns the relative timelock, as a number of
|
||||||
// blocks, that must be built on top of the confirmation height before
|
// blocks, that must be built on top of the confirmation height before
|
||||||
@@ -91,12 +96,12 @@ func MakeBaseInput(outpoint *wire.OutPoint, witnessType lnwallet.WitnessType,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildWitness computes a valid witness that allows us to spend from the
|
// CraftInputScript returns a valid set of input scripts allowing this output
|
||||||
// breached output. It does so by generating the witness generation function,
|
// to be spent. The returns input scripts should target the input at location
|
||||||
// which is parameterized primarily by the witness type and sign descriptor.
|
// txIndex within the passed transaction. The input scripts generated by this
|
||||||
// The method then returns the witness computed by invoking this function.
|
// method support spending p2wkh, p2wsh, and also nested p2sh outputs.
|
||||||
func (bi *BaseInput) BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx,
|
func (bi *BaseInput) CraftInputScript(signer lnwallet.Signer, txn *wire.MsgTx,
|
||||||
hashCache *txscript.TxSigHashes, txinIdx int) ([][]byte, error) {
|
hashCache *txscript.TxSigHashes, txinIdx int) (*lnwallet.InputScript, error) {
|
||||||
|
|
||||||
witnessFunc := bi.witnessType.GenWitnessFunc(
|
witnessFunc := bi.witnessType.GenWitnessFunc(
|
||||||
signer, bi.SignDesc(),
|
signer, bi.SignDesc(),
|
||||||
@@ -138,20 +143,27 @@ func MakeHtlcSucceedInput(outpoint *wire.OutPoint,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildWitness computes a valid witness that allows us to spend from the
|
// CraftInputScript returns a valid set of input scripts allowing this output
|
||||||
// breached output. For HtlcSpendInput it will need to make the preimage part
|
// to be spent. The returns input scripts should target the input at location
|
||||||
// of the witness.
|
// txIndex within the passed transaction. The input scripts generated by this
|
||||||
func (h *HtlcSucceedInput) BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx,
|
// method support spending p2wkh, p2wsh, and also nested p2sh outputs.
|
||||||
hashCache *txscript.TxSigHashes, txinIdx int) ([][]byte, error) {
|
func (h *HtlcSucceedInput) CraftInputScript(signer lnwallet.Signer, txn *wire.MsgTx,
|
||||||
|
hashCache *txscript.TxSigHashes, txinIdx int) (*lnwallet.InputScript, error) {
|
||||||
|
|
||||||
desc := h.signDesc
|
desc := h.signDesc
|
||||||
desc.SigHashes = hashCache
|
desc.SigHashes = hashCache
|
||||||
desc.InputIndex = txinIdx
|
desc.InputIndex = txinIdx
|
||||||
|
|
||||||
return lnwallet.SenderHtlcSpendRedeem(
|
witness, err := lnwallet.SenderHtlcSpendRedeem(
|
||||||
signer, &desc, txn,
|
signer, &desc, txn, h.preimage,
|
||||||
h.preimage,
|
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lnwallet.InputScript{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlocksToMaturity returns the relative timelock, as a number of blocks, that
|
// BlocksToMaturity returns the relative timelock, as a number of blocks, that
|
||||||
|
|||||||
+8
-8
@@ -85,10 +85,10 @@ type UtxoSweeperConfig struct {
|
|||||||
// funds can be swept.
|
// funds can be swept.
|
||||||
GenSweepScript func() ([]byte, error)
|
GenSweepScript func() ([]byte, error)
|
||||||
|
|
||||||
// Estimator is used when crafting sweep transactions to estimate the
|
// FeeEstimator is used when crafting sweep transactions to estimate
|
||||||
// necessary fee relative to the expected size of the sweep
|
// the necessary fee relative to the expected size of the sweep
|
||||||
// transaction.
|
// transaction.
|
||||||
Estimator lnwallet.FeeEstimator
|
FeeEstimator lnwallet.FeeEstimator
|
||||||
|
|
||||||
// PublishTransaction facilitates the process of broadcasting a signed
|
// PublishTransaction facilitates the process of broadcasting a signed
|
||||||
// transaction to the appropriate network.
|
// transaction to the appropriate network.
|
||||||
@@ -198,7 +198,7 @@ func (s *UtxoSweeper) Start() error {
|
|||||||
|
|
||||||
// Retrieve relay fee for dust limit calculation. Assume that this will
|
// Retrieve relay fee for dust limit calculation. Assume that this will
|
||||||
// not change from here on.
|
// not change from here on.
|
||||||
s.relayFeePerKW = s.cfg.Estimator.RelayFeePerKW()
|
s.relayFeePerKW = s.cfg.FeeEstimator.RelayFeePerKW()
|
||||||
|
|
||||||
// Register for block epochs to retry sweeping every block.
|
// Register for block epochs to retry sweeping every block.
|
||||||
bestHash, bestHeight, err := s.cfg.ChainIO.GetBestBlock()
|
bestHash, bestHeight, err := s.cfg.ChainIO.GetBestBlock()
|
||||||
@@ -407,7 +407,7 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch,
|
|||||||
|
|
||||||
// Retrieve fee estimate for input filtering and final
|
// Retrieve fee estimate for input filtering and final
|
||||||
// tx fee calculation.
|
// tx fee calculation.
|
||||||
satPerKW, err := s.cfg.Estimator.EstimateFeePerKW(
|
satPerKW, err := s.cfg.FeeEstimator.EstimateFeePerKW(
|
||||||
s.cfg.SweepTxConfTarget,
|
s.cfg.SweepTxConfTarget,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -465,7 +465,7 @@ func (s *UtxoSweeper) scheduleSweep(currentHeight int32) error {
|
|||||||
|
|
||||||
// Retrieve fee estimate for input filtering and final tx fee
|
// Retrieve fee estimate for input filtering and final tx fee
|
||||||
// calculation.
|
// calculation.
|
||||||
satPerKW, err := s.cfg.Estimator.EstimateFeePerKW(
|
satPerKW, err := s.cfg.FeeEstimator.EstimateFeePerKW(
|
||||||
s.cfg.SweepTxConfTarget,
|
s.cfg.SweepTxConfTarget,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -750,10 +750,10 @@ func (s *UtxoSweeper) waitForSpend(outpoint wire.OutPoint,
|
|||||||
// - Make handling re-orgs easier.
|
// - Make handling re-orgs easier.
|
||||||
// - Thwart future possible fee sniping attempts.
|
// - Thwart future possible fee sniping attempts.
|
||||||
// - Make us blend in with the bitcoind wallet.
|
// - Make us blend in with the bitcoind wallet.
|
||||||
func (s *UtxoSweeper) CreateSweepTx(inputs []Input, confTarget uint32,
|
func (s *UtxoSweeper) CreateSweepTx(inputs []Input, feePref FeePreference,
|
||||||
currentBlockHeight uint32) (*wire.MsgTx, error) {
|
currentBlockHeight uint32) (*wire.MsgTx, error) {
|
||||||
|
|
||||||
feePerKw, err := s.cfg.Estimator.EstimateFeePerKW(confTarget)
|
feePerKw, err := DetermineFeePerKw(s.cfg.FeeEstimator, feePref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/build"
|
"github.com/lightningnetwork/lnd/build"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
@@ -135,7 +136,7 @@ func createSweeperTestContext(t *testing.T) *sweeperTestContext {
|
|||||||
outputScriptCount++
|
outputScriptCount++
|
||||||
return script, nil
|
return script, nil
|
||||||
},
|
},
|
||||||
Estimator: estimator,
|
FeeEstimator: estimator,
|
||||||
MaxInputsPerTx: testMaxInputsPerTx,
|
MaxInputsPerTx: testMaxInputsPerTx,
|
||||||
MaxSweepAttempts: testMaxSweepAttempts,
|
MaxSweepAttempts: testMaxSweepAttempts,
|
||||||
NextAttemptDeltaFunc: func(attempts int) int32 {
|
NextAttemptDeltaFunc: func(attempts int) int32 {
|
||||||
@@ -376,7 +377,8 @@ func TestNegativeInput(t *testing.T) {
|
|||||||
if !testTxIns(&sweepTx1, []*wire.OutPoint{
|
if !testTxIns(&sweepTx1, []*wire.OutPoint{
|
||||||
largeInput.OutPoint(), positiveInput.OutPoint(),
|
largeInput.OutPoint(), positiveInput.OutPoint(),
|
||||||
}) {
|
}) {
|
||||||
t.Fatal("Tx does not contain expected inputs")
|
t.Fatalf("Tx does not contain expected inputs: %v",
|
||||||
|
spew.Sdump(sweepTx1))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.backend.mine()
|
ctx.backend.mine()
|
||||||
|
|||||||
+49
-22
@@ -55,7 +55,7 @@ func generateInputPartitionings(sweepableInputs []Input,
|
|||||||
// on the signature length, which is not known yet at this point.
|
// on the signature length, which is not known yet at this point.
|
||||||
yields := make(map[wire.OutPoint]int64)
|
yields := make(map[wire.OutPoint]int64)
|
||||||
for _, input := range sweepableInputs {
|
for _, input := range sweepableInputs {
|
||||||
size, err := getInputWitnessSizeUpperBound(input)
|
size, _, err := getInputWitnessSizeUpperBound(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"failed adding input weight: %v", err)
|
"failed adding input weight: %v", err)
|
||||||
@@ -125,10 +125,14 @@ func getPositiveYieldInputs(sweepableInputs []Input, maxInputs int,
|
|||||||
for idx, input := range sweepableInputs {
|
for idx, input := range sweepableInputs {
|
||||||
// Can ignore error, because it has already been checked when
|
// Can ignore error, because it has already been checked when
|
||||||
// calculating the yields.
|
// calculating the yields.
|
||||||
size, _ := getInputWitnessSizeUpperBound(input)
|
size, isNestedP2SH, _ := getInputWitnessSizeUpperBound(input)
|
||||||
|
|
||||||
// Keep a running weight estimate of the input set.
|
// Keep a running weight estimate of the input set.
|
||||||
weightEstimate.AddWitnessInput(size)
|
if isNestedP2SH {
|
||||||
|
weightEstimate.AddNestedP2WSHInput(size)
|
||||||
|
} else {
|
||||||
|
weightEstimate.AddWitnessInput(size)
|
||||||
|
}
|
||||||
|
|
||||||
newTotal := total + btcutil.Amount(input.SignDesc().Output.Value)
|
newTotal := total + btcutil.Amount(input.SignDesc().Output.Value)
|
||||||
|
|
||||||
@@ -216,25 +220,29 @@ func createSweepTx(inputs []Input, outputPkScript []byte,
|
|||||||
|
|
||||||
hashCache := txscript.NewTxSigHashes(sweepTx)
|
hashCache := txscript.NewTxSigHashes(sweepTx)
|
||||||
|
|
||||||
// With all the inputs in place, use each output's unique witness
|
// With all the inputs in place, use each output's unique input script
|
||||||
// function to generate the final witness required for spending.
|
// function to generate the final witness required for spending.
|
||||||
addWitness := func(idx int, tso Input) error {
|
addInputScript := func(idx int, tso Input) error {
|
||||||
witness, err := tso.BuildWitness(
|
inputScript, err := tso.CraftInputScript(
|
||||||
signer, sweepTx, hashCache, idx,
|
signer, sweepTx, hashCache, idx,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sweepTx.TxIn[idx].Witness = witness
|
sweepTx.TxIn[idx].Witness = inputScript.Witness
|
||||||
|
|
||||||
|
if len(inputScript.SigScript) != 0 {
|
||||||
|
sweepTx.TxIn[idx].SignatureScript = inputScript.SigScript
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally we'll attach a valid witness to each csv and cltv input
|
// Finally we'll attach a valid input script to each csv and cltv input
|
||||||
// within the sweeping transaction.
|
// within the sweeping transaction.
|
||||||
for i, input := range inputs {
|
for i, input := range inputs {
|
||||||
if err := addWitness(i, input); err != nil {
|
if err := addInputScript(i, input); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,45 +251,54 @@ func createSweepTx(inputs []Input, outputPkScript []byte,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getInputWitnessSizeUpperBound returns the maximum length of the witness for
|
// getInputWitnessSizeUpperBound returns the maximum length of the witness for
|
||||||
// the given input if it would be included in a tx.
|
// the given input if it would be included in a tx. We also return if the
|
||||||
func getInputWitnessSizeUpperBound(input Input) (int, error) {
|
// output itself is a nested p2sh output, if so then we need to take into
|
||||||
|
// account the extra sigScript data size.
|
||||||
|
func getInputWitnessSizeUpperBound(input Input) (int, bool, error) {
|
||||||
switch input.WitnessType() {
|
switch input.WitnessType() {
|
||||||
|
|
||||||
// Outputs on a remote commitment transaction that pay directly
|
// Outputs on a remote commitment transaction that pay directly to us.
|
||||||
// to us.
|
case lnwallet.WitnessKeyHash:
|
||||||
|
fallthrough
|
||||||
case lnwallet.CommitmentNoDelay:
|
case lnwallet.CommitmentNoDelay:
|
||||||
return lnwallet.P2WKHWitnessSize, nil
|
return lnwallet.P2WKHWitnessSize, false, nil
|
||||||
|
|
||||||
// Outputs on a past commitment transaction that pay directly
|
// Outputs on a past commitment transaction that pay directly
|
||||||
// to us.
|
// to us.
|
||||||
case lnwallet.CommitmentTimeLock:
|
case lnwallet.CommitmentTimeLock:
|
||||||
return lnwallet.ToLocalTimeoutWitnessSize, nil
|
return lnwallet.ToLocalTimeoutWitnessSize, false, nil
|
||||||
|
|
||||||
// Outgoing second layer HTLC's that have confirmed within the
|
// Outgoing second layer HTLC's that have confirmed within the
|
||||||
// chain, and the output they produced is now mature enough to
|
// chain, and the output they produced is now mature enough to
|
||||||
// sweep.
|
// sweep.
|
||||||
case lnwallet.HtlcOfferedTimeoutSecondLevel:
|
case lnwallet.HtlcOfferedTimeoutSecondLevel:
|
||||||
return lnwallet.ToLocalTimeoutWitnessSize, nil
|
return lnwallet.ToLocalTimeoutWitnessSize, false, nil
|
||||||
|
|
||||||
// Incoming second layer HTLC's that have confirmed within the
|
// Incoming second layer HTLC's that have confirmed within the
|
||||||
// chain, and the output they produced is now mature enough to
|
// chain, and the output they produced is now mature enough to
|
||||||
// sweep.
|
// sweep.
|
||||||
case lnwallet.HtlcAcceptedSuccessSecondLevel:
|
case lnwallet.HtlcAcceptedSuccessSecondLevel:
|
||||||
return lnwallet.ToLocalTimeoutWitnessSize, nil
|
return lnwallet.ToLocalTimeoutWitnessSize, false, nil
|
||||||
|
|
||||||
// An HTLC on the commitment transaction of the remote party,
|
// An HTLC on the commitment transaction of the remote party,
|
||||||
// that has had its absolute timelock expire.
|
// that has had its absolute timelock expire.
|
||||||
case lnwallet.HtlcOfferedRemoteTimeout:
|
case lnwallet.HtlcOfferedRemoteTimeout:
|
||||||
return lnwallet.AcceptedHtlcTimeoutWitnessSize, nil
|
return lnwallet.AcceptedHtlcTimeoutWitnessSize, false, nil
|
||||||
|
|
||||||
// An HTLC on the commitment transaction of the remote party,
|
// An HTLC on the commitment transaction of the remote party,
|
||||||
// that can be swept with the preimage.
|
// that can be swept with the preimage.
|
||||||
case lnwallet.HtlcAcceptedRemoteSuccess:
|
case lnwallet.HtlcAcceptedRemoteSuccess:
|
||||||
return lnwallet.OfferedHtlcSuccessWitnessSize, nil
|
return lnwallet.OfferedHtlcSuccessWitnessSize, false, nil
|
||||||
|
|
||||||
|
// A nested P2SH input that has a p2wkh witness script. We'll mark this
|
||||||
|
// as nested P2SH so the caller can estimate the weight properly
|
||||||
|
// including the sigScript.
|
||||||
|
case lnwallet.NestedWitnessKeyHash:
|
||||||
|
return lnwallet.P2WKHWitnessSize, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, fmt.Errorf("unexpected witness type: %v", input.WitnessType())
|
return 0, false, fmt.Errorf("unexpected witness type: %v",
|
||||||
|
input.WitnessType())
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWeightEstimate returns a weight estimate for the given inputs.
|
// getWeightEstimate returns a weight estimate for the given inputs.
|
||||||
@@ -308,7 +325,10 @@ func getWeightEstimate(inputs []Input) ([]Input, int64, int, int) {
|
|||||||
for i := range inputs {
|
for i := range inputs {
|
||||||
input := inputs[i]
|
input := inputs[i]
|
||||||
|
|
||||||
size, err := getInputWitnessSizeUpperBound(input)
|
// For fee estimation purposes, we'll now attempt to obtain an
|
||||||
|
// upper bound on the weight this input will add when fully
|
||||||
|
// populated.
|
||||||
|
size, isNestedP2SH, err := getInputWitnessSizeUpperBound(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn(err)
|
log.Warn(err)
|
||||||
|
|
||||||
@@ -316,7 +336,14 @@ func getWeightEstimate(inputs []Input) ([]Input, int64, int, int) {
|
|||||||
// given.
|
// given.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
weightEstimate.AddWitnessInput(size)
|
|
||||||
|
// If this is a nested P2SH input, then we'll need to factor in
|
||||||
|
// the additional data push within the sigScript.
|
||||||
|
if isNestedP2SH {
|
||||||
|
weightEstimate.AddNestedP2WSHInput(size)
|
||||||
|
} else {
|
||||||
|
weightEstimate.AddWitnessInput(size)
|
||||||
|
}
|
||||||
|
|
||||||
switch input.WitnessType() {
|
switch input.WitnessType() {
|
||||||
case lnwallet.CommitmentTimeLock,
|
case lnwallet.CommitmentTimeLock,
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
package sweep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultNumBlocksEstimate is the number of blocks that we fall back
|
||||||
|
// to issuing an estimate for if a fee pre fence doesn't specify an
|
||||||
|
// explicit conf target or fee rate.
|
||||||
|
defaultNumBlocksEstimate = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeePreference allows callers to express their time value for inclusion of a
|
||||||
|
// transaction into a block via either a confirmation target, or a fee rate.
|
||||||
|
type FeePreference struct {
|
||||||
|
// ConfTarget if non-zero, signals a fee preference expressed in the
|
||||||
|
// number of desired blocks between first broadcast, and confirmation.
|
||||||
|
ConfTarget uint32
|
||||||
|
|
||||||
|
// FeeRate if non-zero, signals a fee pre fence expressed in the fee
|
||||||
|
// rate expressed in sat/kw for a particular transaction.
|
||||||
|
FeeRate lnwallet.SatPerKWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetermineFeePerKw will determine the fee in sat/kw that should be paid given
|
||||||
|
// an estimator, a confirmation target, and a manual value for sat/byte. A
|
||||||
|
// value is chosen based on the two free parameters as one, or both of them can
|
||||||
|
// be zero.
|
||||||
|
func DetermineFeePerKw(feeEstimator lnwallet.FeeEstimator,
|
||||||
|
feePref FeePreference) (lnwallet.SatPerKWeight, error) {
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// If both values are set, then we'll return an error as we require a
|
||||||
|
// strict directive.
|
||||||
|
case feePref.FeeRate != 0 && feePref.ConfTarget != 0:
|
||||||
|
return 0, fmt.Errorf("only FeeRate or ConfTarget should " +
|
||||||
|
"be set for FeePreferences")
|
||||||
|
|
||||||
|
// If the target number of confirmations is set, then we'll use that to
|
||||||
|
// consult our fee estimator for an adequate fee.
|
||||||
|
case feePref.ConfTarget != 0:
|
||||||
|
feePerKw, err := feeEstimator.EstimateFeePerKW(
|
||||||
|
uint32(feePref.ConfTarget),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("unable to query fee "+
|
||||||
|
"estimator: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return feePerKw, nil
|
||||||
|
|
||||||
|
// If a manual sat/byte fee rate is set, then we'll use that directly.
|
||||||
|
// We'll need to convert it to sat/kw as this is what we use
|
||||||
|
// internally.
|
||||||
|
case feePref.FeeRate != 0:
|
||||||
|
feePerKW := feePref.FeeRate
|
||||||
|
if feePerKW < lnwallet.FeePerKwFloor {
|
||||||
|
log.Infof("Manual fee rate input of %d sat/kw is "+
|
||||||
|
"too low, using %d sat/kw instead", feePerKW,
|
||||||
|
lnwallet.FeePerKwFloor)
|
||||||
|
|
||||||
|
feePerKW = lnwallet.FeePerKwFloor
|
||||||
|
}
|
||||||
|
|
||||||
|
return feePerKW, nil
|
||||||
|
|
||||||
|
// Otherwise, we'll attempt a relaxed confirmation target for the
|
||||||
|
// transaction
|
||||||
|
default:
|
||||||
|
feePerKw, err := feeEstimator.EstimateFeePerKW(
|
||||||
|
defaultNumBlocksEstimate,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("unable to query fee estimator: "+
|
||||||
|
"%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return feePerKw, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UtxoSource is an interface that allows a caller to access a source of UTXOs
|
||||||
|
// to use when crafting sweep transactions.
|
||||||
|
type UtxoSource interface {
|
||||||
|
// ListUnspentWitness returns all UTXOs from the source that have
|
||||||
|
// between minConfs and maxConfs number of confirmations.
|
||||||
|
ListUnspentWitness(minConfs, maxConfs int32) ([]*lnwallet.Utxo, error)
|
||||||
|
|
||||||
|
// FetchInputInfo returns the matching output for an outpoint. If the
|
||||||
|
// outpoint doesn't belong to this UTXO source, then an error should be
|
||||||
|
// returned.
|
||||||
|
FetchInputInfo(*wire.OutPoint) (*wire.TxOut, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoinSelectionLocker is an interface that allows the caller to perform an
|
||||||
|
// operation, which is synchronized with all coin selection attempts. This can
|
||||||
|
// be used when an operation requires that all coin selection operations cease
|
||||||
|
// forward progress. Think of this as an exclusive lock on coin selection
|
||||||
|
// operations.
|
||||||
|
type CoinSelectionLocker interface {
|
||||||
|
// WithCoinSelectLock will execute the passed function closure in a
|
||||||
|
// synchronized manner preventing any coin selection operations from
|
||||||
|
// proceeding while the closure if executing. This can be seen as the
|
||||||
|
// ability to execute a function closure under an exclusive coin
|
||||||
|
// selection lock.
|
||||||
|
WithCoinSelectLock(func() error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutpointLocker allows a caller to lock/unlock an outpoint. When locked, the
|
||||||
|
// outpoints shouldn't be used for any sort of channel funding of coin
|
||||||
|
// selection. Locked outpoints are not expect to be persisted between restarts.
|
||||||
|
type OutpointLocker interface {
|
||||||
|
// LockOutpoint locks a target outpoint, rendering it unusable for coin
|
||||||
|
// selection.
|
||||||
|
LockOutpoint(o wire.OutPoint)
|
||||||
|
|
||||||
|
// UnlockOutpoint unlocks a target outpoint, allowing it to be used for
|
||||||
|
// coin selection once again.
|
||||||
|
UnlockOutpoint(o wire.OutPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalletSweepPackage is a package that gives the caller the ability to sweep
|
||||||
|
// ALL funds from a wallet in a single transaction. We also package a function
|
||||||
|
// closure that allows one to abort the operation.
|
||||||
|
type WalletSweepPackage struct {
|
||||||
|
// SweepTx is a fully signed, and valid transaction that is broadcast,
|
||||||
|
// will sweep ALL confirmed coins in the wallet with a single
|
||||||
|
// transaction.
|
||||||
|
SweepTx *wire.MsgTx
|
||||||
|
|
||||||
|
// CancelSweepAttempt allows the caller to cancel the sweep attempt.
|
||||||
|
//
|
||||||
|
// NOTE: If the sweeping transaction isn't or cannot be broadcast, then
|
||||||
|
// this closure MUST be called, otherwise all selected utxos will be
|
||||||
|
// unable to be used.
|
||||||
|
CancelSweepAttempt func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CraftSweepAllTx attempts to craft a WalletSweepPackage which will allow the
|
||||||
|
// caller to sweep ALL outputs within the wallet to a single UTXO, as specified
|
||||||
|
// by the delivery address. The sweep transaction will be crafted with the
|
||||||
|
// target fee rate, and will use the utxoSource and outpointLocker as sources
|
||||||
|
// for wallet funds.
|
||||||
|
func CraftSweepAllTx(feeRate lnwallet.SatPerKWeight, blockHeight uint32,
|
||||||
|
deliveryAddr btcutil.Address, coinSelectLocker CoinSelectionLocker,
|
||||||
|
utxoSource UtxoSource, outpointLocker OutpointLocker,
|
||||||
|
feeEstimator lnwallet.FeeEstimator,
|
||||||
|
signer lnwallet.Signer) (*WalletSweepPackage, error) {
|
||||||
|
|
||||||
|
// TODO(roasbeef): turn off ATPL as well when available?
|
||||||
|
|
||||||
|
var allOutputs []*lnwallet.Utxo
|
||||||
|
|
||||||
|
// We'll make a function closure up front that allows us to unlock all
|
||||||
|
// selected outputs to ensure that they become available again in the
|
||||||
|
// case of an error after the outputs have been locked, but before we
|
||||||
|
// can actually craft a sweeping transaction.
|
||||||
|
unlockOutputs := func() {
|
||||||
|
for _, utxo := range allOutputs {
|
||||||
|
outpointLocker.UnlockOutpoint(utxo.OutPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll use the coinSelectLocker to ensure that no coin
|
||||||
|
// selection takes place while we fetch and lock all outputs the wallet
|
||||||
|
// knows of. Otherwise, it may be possible for a new funding flow to
|
||||||
|
// lock an output while we fetch the set of unspent witnesses.
|
||||||
|
err := coinSelectLocker.WithCoinSelectLock(func() error {
|
||||||
|
// Now that we can be sure that no other coin selection
|
||||||
|
// operations are going on, we can grab a clean snapshot of the
|
||||||
|
// current UTXO state of the wallet.
|
||||||
|
utxos, err := utxoSource.ListUnspentWitness(
|
||||||
|
1, math.MaxInt32,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now lock each UTXO to ensure that other callers don't
|
||||||
|
// attempt to use these UTXOs in transactions while we're
|
||||||
|
// crafting out sweep all transaction.
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
outpointLocker.LockOutpoint(utxo.OutPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
allOutputs = append(allOutputs, utxos...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// If we failed at all, we'll unlock any outputs selected just
|
||||||
|
// in case we had any lingering outputs.
|
||||||
|
unlockOutputs()
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unable to fetch+lock wallet "+
|
||||||
|
"utxos: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've locked all the potential outputs to sweep, we'll
|
||||||
|
// assemble an input for each of them, so we can hand it off to the
|
||||||
|
// sweeper to generate and sign a transaction for us.
|
||||||
|
var inputsToSweep []Input
|
||||||
|
for _, output := range allOutputs {
|
||||||
|
// We'll consult the utxoSource for information concerning this
|
||||||
|
// outpoint, we'll need to properly populate a signDescriptor
|
||||||
|
// for this output.
|
||||||
|
outputInfo, err := utxoSource.FetchInputInfo(&output.OutPoint)
|
||||||
|
if err != nil {
|
||||||
|
unlockOutputs()
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// As we'll be signing for outputs under control of the wallet,
|
||||||
|
// we only need to populate the output value and output script.
|
||||||
|
// The rest of the items will be populated internally within
|
||||||
|
// the sweeper via the witness generation function.
|
||||||
|
signDesc := &lnwallet.SignDescriptor{
|
||||||
|
Output: outputInfo,
|
||||||
|
HashType: txscript.SigHashAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript := outputInfo.PkScript
|
||||||
|
|
||||||
|
// Based on the output type, we'll map it to the proper witness
|
||||||
|
// type so we can generate the set of input scripts needed to
|
||||||
|
// sweep the output.
|
||||||
|
var witnessType lnwallet.WitnessType
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// If this is a p2wkh output, then we'll assume it's a witness
|
||||||
|
// key hash witness type.
|
||||||
|
case txscript.IsPayToWitnessPubKeyHash(pkScript):
|
||||||
|
witnessType = lnwallet.WitnessKeyHash
|
||||||
|
|
||||||
|
// If this is a p2sh output, then as since it's under control
|
||||||
|
// of the wallet, we'll assume it's a nested p2sh output.
|
||||||
|
case txscript.IsPayToScriptHash(pkScript):
|
||||||
|
witnessType = lnwallet.NestedWitnessKeyHash
|
||||||
|
|
||||||
|
// All other output types we count as unknown and will fail to
|
||||||
|
// sweep.
|
||||||
|
default:
|
||||||
|
unlockOutputs()
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unable to sweep coins, "+
|
||||||
|
"unknown script: %x", pkScript[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've constructed the items required, we'll make an
|
||||||
|
// input which can be passed to the sweeper for ultimate
|
||||||
|
// sweeping.
|
||||||
|
input := MakeBaseInput(&output.OutPoint, witnessType, signDesc, 0)
|
||||||
|
inputsToSweep = append(inputsToSweep, &input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll convert the delivery addr to a pkScript that we can use
|
||||||
|
// to create the sweep transaction.
|
||||||
|
deliveryPkScript, err := txscript.PayToAddrScript(deliveryAddr)
|
||||||
|
if err != nil {
|
||||||
|
unlockOutputs()
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll ask the sweeper to craft a sweep transaction which
|
||||||
|
// respects our fee preference and targets all the UTXOs of the wallet.
|
||||||
|
sweepTx, err := createSweepTx(
|
||||||
|
inputsToSweep, deliveryPkScript, blockHeight, feeRate, signer,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
unlockOutputs()
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WalletSweepPackage{
|
||||||
|
SweepTx: sweepTx,
|
||||||
|
CancelSweepAttempt: unlockOutputs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
package sweep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestDetermineFeePerKw tests that given a fee preference, the
|
||||||
|
// DetermineFeePerKw will properly map it to a concrete fee in sat/kw.
|
||||||
|
func TestDetermineFeePerKw(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
defaultFee := lnwallet.SatPerKWeight(999)
|
||||||
|
relayFee := lnwallet.SatPerKWeight(300)
|
||||||
|
|
||||||
|
feeEstimator := newMockFeeEstimator(defaultFee, relayFee)
|
||||||
|
|
||||||
|
// We'll populate two items in the internal map which is used to query
|
||||||
|
// a fee based on a confirmation target: the default conf target, and
|
||||||
|
// an arbitrary conf target. We'll ensure below that both of these are
|
||||||
|
// properly
|
||||||
|
feeEstimator.blocksToFee[50] = 300
|
||||||
|
feeEstimator.blocksToFee[defaultNumBlocksEstimate] = 1000
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
// feePref is the target fee preference for this case.
|
||||||
|
feePref FeePreference
|
||||||
|
|
||||||
|
// fee is the value the DetermineFeePerKw should return given
|
||||||
|
// the FeePreference above
|
||||||
|
fee lnwallet.SatPerKWeight
|
||||||
|
|
||||||
|
// fail determines if this test case should fail or not.
|
||||||
|
fail bool
|
||||||
|
}{
|
||||||
|
// A fee rate below the fee rate floor should output the floor.
|
||||||
|
{
|
||||||
|
feePref: FeePreference{
|
||||||
|
FeeRate: lnwallet.SatPerKWeight(99),
|
||||||
|
},
|
||||||
|
fee: lnwallet.FeePerKwFloor,
|
||||||
|
},
|
||||||
|
|
||||||
|
// A fee rate above the floor, should pass through and return
|
||||||
|
// the target fee rate.
|
||||||
|
{
|
||||||
|
feePref: FeePreference{
|
||||||
|
FeeRate: 900,
|
||||||
|
},
|
||||||
|
fee: 900,
|
||||||
|
},
|
||||||
|
|
||||||
|
// A specified confirmation target should cause the function to
|
||||||
|
// query the estimator which will return our value specified
|
||||||
|
// above.
|
||||||
|
{
|
||||||
|
feePref: FeePreference{
|
||||||
|
ConfTarget: 50,
|
||||||
|
},
|
||||||
|
fee: 300,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If the caller doesn't specify any values at all, then we
|
||||||
|
// should query for the default conf target.
|
||||||
|
{
|
||||||
|
feePref: FeePreference{},
|
||||||
|
fee: 1000,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Both conf target and fee rate are set, we should return with
|
||||||
|
// an error.
|
||||||
|
{
|
||||||
|
feePref: FeePreference{
|
||||||
|
ConfTarget: 50,
|
||||||
|
FeeRate: 90000,
|
||||||
|
},
|
||||||
|
fee: 300,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
targetFee, err := DetermineFeePerKw(
|
||||||
|
feeEstimator, testCase.feePref,
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case testCase.fail && err != nil:
|
||||||
|
continue
|
||||||
|
|
||||||
|
case testCase.fail && err == nil:
|
||||||
|
t.Fatalf("expected failure for #%v", i)
|
||||||
|
|
||||||
|
case !testCase.fail && err != nil:
|
||||||
|
t.Fatalf("unable to estimate fee; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetFee != testCase.fee {
|
||||||
|
t.Fatalf("#%v: wrong fee: expected %v got %v", i,
|
||||||
|
testCase.fee, targetFee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockUtxoSource struct {
|
||||||
|
outpoints map[wire.OutPoint]*wire.TxOut
|
||||||
|
|
||||||
|
outputs []*lnwallet.Utxo
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockUtxoSource(utxos []*lnwallet.Utxo) *mockUtxoSource {
|
||||||
|
m := &mockUtxoSource{
|
||||||
|
outputs: utxos,
|
||||||
|
outpoints: make(map[wire.OutPoint]*wire.TxOut),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
m.outpoints[utxo.OutPoint] = &wire.TxOut{
|
||||||
|
Value: int64(utxo.Value),
|
||||||
|
PkScript: utxo.PkScript,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUtxoSource) ListUnspentWitness(minConfs int32,
|
||||||
|
maxConfs int32) ([]*lnwallet.Utxo, error) {
|
||||||
|
|
||||||
|
return m.outputs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUtxoSource) FetchInputInfo(op *wire.OutPoint) (*wire.TxOut, error) {
|
||||||
|
txOut, ok := m.outpoints[*op]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no output found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return txOut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockCoinSelectionLocker struct {
|
||||||
|
fail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCoinSelectionLocker) WithCoinSelectLock(f func() error) error {
|
||||||
|
if err := f(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.fail {
|
||||||
|
return fmt.Errorf("kek")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockOutpointLocker struct {
|
||||||
|
lockedOutpoints map[wire.OutPoint]struct{}
|
||||||
|
|
||||||
|
unlockedOutpoints map[wire.OutPoint]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockOutpointLocker() *mockOutpointLocker {
|
||||||
|
return &mockOutpointLocker{
|
||||||
|
lockedOutpoints: make(map[wire.OutPoint]struct{}),
|
||||||
|
|
||||||
|
unlockedOutpoints: make(map[wire.OutPoint]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockOutpointLocker) LockOutpoint(o wire.OutPoint) {
|
||||||
|
m.lockedOutpoints[o] = struct{}{}
|
||||||
|
}
|
||||||
|
func (m *mockOutpointLocker) UnlockOutpoint(o wire.OutPoint) {
|
||||||
|
m.unlockedOutpoints[o] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sweepScript = []byte{
|
||||||
|
0x0, 0x14, 0x64, 0x3d, 0x8b, 0x15, 0x69, 0x4a, 0x54,
|
||||||
|
0x7d, 0x57, 0x33, 0x6e, 0x51, 0xdf, 0xfd, 0x38, 0xe3,
|
||||||
|
0xe, 0x6e, 0xf8, 0xef,
|
||||||
|
}
|
||||||
|
|
||||||
|
var deliveryAddr = func() btcutil.Address {
|
||||||
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
||||||
|
sweepScript, &chaincfg.TestNet3Params,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return addrs[0]
|
||||||
|
}()
|
||||||
|
|
||||||
|
var testUtxos = []*lnwallet.Utxo{
|
||||||
|
{
|
||||||
|
// A p2wkh output.
|
||||||
|
PkScript: []byte{
|
||||||
|
0x0, 0x14, 0x64, 0x3d, 0x8b, 0x15, 0x69, 0x4a, 0x54,
|
||||||
|
0x7d, 0x57, 0x33, 0x6e, 0x51, 0xdf, 0xfd, 0x38, 0xe3,
|
||||||
|
0xe, 0x6e, 0xf7, 0xef,
|
||||||
|
},
|
||||||
|
Value: 1000,
|
||||||
|
OutPoint: wire.OutPoint{
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
// A np2wkh output.
|
||||||
|
PkScript: []byte{
|
||||||
|
0xa9, 0x14, 0x97, 0x17, 0xf7, 0xd1, 0x5f, 0x6f, 0x8b,
|
||||||
|
0x7, 0xe3, 0x58, 0x43, 0x19, 0xb9, 0x7e, 0xa9, 0x20,
|
||||||
|
0x18, 0xc3, 0x17, 0xd7, 0x87,
|
||||||
|
},
|
||||||
|
Value: 2000,
|
||||||
|
OutPoint: wire.OutPoint{
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// A p2wsh output.
|
||||||
|
{
|
||||||
|
PkScript: []byte{
|
||||||
|
0x0, 0x20, 0x70, 0x1a, 0x8d, 0x40, 0x1c, 0x84, 0xfb, 0x13,
|
||||||
|
0xe6, 0xba, 0xf1, 0x69, 0xd5, 0x96, 0x84, 0xe2, 0x7a, 0xbd,
|
||||||
|
0x9f, 0xa2, 0x16, 0xc8, 0xbc, 0x5b, 0x9f, 0xc6, 0x3d, 0x62,
|
||||||
|
0x2f, 0xf8, 0xc5, 0x8c,
|
||||||
|
},
|
||||||
|
Value: 3000,
|
||||||
|
OutPoint: wire.OutPoint{
|
||||||
|
Index: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertUtxosLocked(t *testing.T, utxoLocker *mockOutpointLocker,
|
||||||
|
utxos []*lnwallet.Utxo) {
|
||||||
|
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
if _, ok := utxoLocker.lockedOutpoints[utxo.OutPoint]; !ok {
|
||||||
|
t.Fatalf("utxo %v was never locked", utxo.OutPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNoUtxosUnlocked(t *testing.T, utxoLocker *mockOutpointLocker,
|
||||||
|
utxos []*lnwallet.Utxo) {
|
||||||
|
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if len(utxoLocker.unlockedOutpoints) != 0 {
|
||||||
|
t.Fatalf("outputs have been locked, but shouldn't have been")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertUtxosUnlocked(t *testing.T, utxoLocker *mockOutpointLocker,
|
||||||
|
utxos []*lnwallet.Utxo) {
|
||||||
|
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
if _, ok := utxoLocker.unlockedOutpoints[utxo.OutPoint]; !ok {
|
||||||
|
t.Fatalf("utxo %v was never unlocked", utxo.OutPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertUtxosLockedAndUnlocked(t *testing.T, utxoLocker *mockOutpointLocker,
|
||||||
|
utxos []*lnwallet.Utxo) {
|
||||||
|
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
if _, ok := utxoLocker.lockedOutpoints[utxo.OutPoint]; !ok {
|
||||||
|
t.Fatalf("utxo %v was never locked", utxo.OutPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := utxoLocker.unlockedOutpoints[utxo.OutPoint]; !ok {
|
||||||
|
t.Fatalf("utxo %v was never unlocked", utxo.OutPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCraftSweepAllTxCoinSelectFail tests that if coin selection fails, then
|
||||||
|
// we unlock any outputs we may have locked in the passed closure.
|
||||||
|
func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
utxoSource := newMockUtxoSource(testUtxos)
|
||||||
|
coinSelectLocker := &mockCoinSelectionLocker{
|
||||||
|
fail: true,
|
||||||
|
}
|
||||||
|
utxoLocker := newMockOutpointLocker()
|
||||||
|
|
||||||
|
_, err := CraftSweepAllTx(
|
||||||
|
0, 100, nil, coinSelectLocker, utxoSource, utxoLocker, nil, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Since we instructed the coin select locker to fail above, we should
|
||||||
|
// get an error.
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("sweep tx should have failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we'll now verify that all outputs were initially
|
||||||
|
// locked, and then also unlocked due to the failure.
|
||||||
|
assertUtxosLockedAndUnlocked(t, utxoLocker, testUtxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCraftSweepAllTxUnknownWitnessType tests that if one of the inputs we
|
||||||
|
// encounter is of an unknown witness type, then we fail and unlock any prior
|
||||||
|
// locked outputs.
|
||||||
|
func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
utxoSource := newMockUtxoSource(testUtxos)
|
||||||
|
coinSelectLocker := &mockCoinSelectionLocker{}
|
||||||
|
utxoLocker := newMockOutpointLocker()
|
||||||
|
|
||||||
|
_, err := CraftSweepAllTx(
|
||||||
|
0, 100, nil, coinSelectLocker, utxoSource, utxoLocker, nil, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Since passed in a p2wsh output, which is unknown, we should fail to
|
||||||
|
// map the output to a witness type.
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("sweep tx should have failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we'll now verify that all outputs were initially
|
||||||
|
// locked, and then also unlocked since we weren't able to find a
|
||||||
|
// witness type for the last output.
|
||||||
|
assertUtxosLockedAndUnlocked(t, utxoLocker, testUtxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCraftSweepAllTx tests that we'll properly lock all available outputs
|
||||||
|
// within the wallet, and craft a single sweep transaction that pays to the
|
||||||
|
// target output.
|
||||||
|
func TestCraftSweepAllTx(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// First, we'll make a mock signer along with a fee estimator, We'll
|
||||||
|
// use zero fees to we can assert a precise output value.
|
||||||
|
signer := &mockSigner{}
|
||||||
|
feeEstimator := newMockFeeEstimator(0, 0)
|
||||||
|
|
||||||
|
// For our UTXO source, we'll pass in all the UTXOs that we know of,
|
||||||
|
// other than the final one which is of an unknown witness type.
|
||||||
|
targetUTXOs := testUtxos[:2]
|
||||||
|
utxoSource := newMockUtxoSource(targetUTXOs)
|
||||||
|
coinSelectLocker := &mockCoinSelectionLocker{}
|
||||||
|
utxoLocker := newMockOutpointLocker()
|
||||||
|
|
||||||
|
sweepPkg, err := CraftSweepAllTx(
|
||||||
|
0, 100, deliveryAddr, coinSelectLocker, utxoSource, utxoLocker,
|
||||||
|
feeEstimator, signer,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to make sweep tx: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, all of the UTXOs that we made above should be locked
|
||||||
|
// and none of them unlocked.
|
||||||
|
assertUtxosLocked(t, utxoLocker, testUtxos[:2])
|
||||||
|
assertNoUtxosUnlocked(t, utxoLocker, testUtxos[:2])
|
||||||
|
|
||||||
|
// Now that we have our sweep transaction, we should find that we have
|
||||||
|
// a UTXO for each input, and also that our final output value is the
|
||||||
|
// sum of all our inputs.
|
||||||
|
sweepTx := sweepPkg.SweepTx
|
||||||
|
if len(sweepTx.TxIn) != len(targetUTXOs) {
|
||||||
|
t.Fatalf("expected %v utxo, got %v", len(targetUTXOs),
|
||||||
|
len(sweepTx.TxIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have a single output that pays to our sweep script
|
||||||
|
// generated above.
|
||||||
|
expectedSweepValue := int64(3000)
|
||||||
|
if len(sweepTx.TxOut) != 1 {
|
||||||
|
t.Fatalf("should have %v outputs, instead have %v", 1,
|
||||||
|
len(sweepTx.TxOut))
|
||||||
|
}
|
||||||
|
output := sweepTx.TxOut[0]
|
||||||
|
switch {
|
||||||
|
case output.Value != expectedSweepValue:
|
||||||
|
t.Fatalf("expected %v sweep value, instead got %v",
|
||||||
|
expectedSweepValue, output.Value)
|
||||||
|
|
||||||
|
case !bytes.Equal(sweepScript, output.PkScript):
|
||||||
|
t.Fatalf("expected %x sweep script, instead got %x", sweepScript,
|
||||||
|
output.PkScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we cancel the sweep attempt, then we should find that all the
|
||||||
|
// UTXOs within the sweep transaction are now unlocked.
|
||||||
|
sweepPkg.CancelSweepAttempt()
|
||||||
|
assertUtxosUnlocked(t, utxoLocker, testUtxos[:2])
|
||||||
|
}
|
||||||
Reference in new issue
Block a user