Merge pull request #5781 from Crypt-iQ/accurate_dust0922

multi: replace DefaultDustLimit with accurate dust limit
This commit is contained in:
Olaoluwa Osuntokun
2021-09-29 16:28:18 -07:00
committed by GitHub
29 changed files with 420 additions and 168 deletions

View File

@@ -174,15 +174,6 @@ const (
BtcToLtcConversionRate = 60
)
// DefaultBtcChannelConstraints is the default set of channel constraints that are
// meant to be used when initially funding a Bitcoin channel.
//
// TODO(halseth): make configurable at startup?
var DefaultBtcChannelConstraints = channeldb.ChannelConstraints{
DustLimit: lnwallet.DefaultDustLimit(),
MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
}
// DefaultLtcChannelConstraints is the default set of channel constraints that are
// meant to be used when initially funding a Litecoin channel.
var DefaultLtcChannelConstraints = channeldb.ChannelConstraints{
@@ -235,6 +226,19 @@ type ChainControl struct {
MinHtlcIn lnwire.MilliSatoshi
}
// GenDefaultBtcChannelConstraints generates the default set of channel
// constraints that are to be used when funding a Bitcoin channel.
func GenDefaultBtcConstraints() channeldb.ChannelConstraints {
// We use the dust limit for the maximally sized witness program with
// a 40-byte data push.
dustLimit := lnwallet.DustLimitForSize(input.UnknownWitnessSize)
return channeldb.ChannelConstraints{
DustLimit: dustLimit,
MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
}
}
// NewChainControl attempts to create a ChainControl instance according
// to the parameters in the passed configuration. Currently three
// branches of ChainControl instances exist: one backed by a running btcd
@@ -674,7 +678,7 @@ func NewChainControl(cfg *Config, blockCache *blockcache.BlockCache) (
cc.Wc = wc
// Select the default channel constraints for the primary chain.
channelConstraints := DefaultBtcChannelConstraints
channelConstraints := GenDefaultBtcConstraints()
if cfg.PrimaryChain() == LitecoinChain {
channelConstraints = DefaultLtcChannelConstraints
}

View File

@@ -0,0 +1,9 @@
# Release Notes
## Wallet
* [The `DefaultDustLimit` method has been removed in favor of `DustLimitForSize` which calculates the proper network dust limit for a given output size. This also fixes certain APIs like SendCoins to be able to send 294 sats to a P2WPKH script.](https://github.com/lightningnetwork/lnd/pull/5781)
# Contributors (Alphabetical Order)
* Eugene Siegel

View File

@@ -1340,7 +1340,7 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer,
CsvDelay: msg.CsvDelay,
}
err = reservation.CommitConstraints(
channelConstraints, f.cfg.MaxLocalCSVDelay,
channelConstraints, f.cfg.MaxLocalCSVDelay, true,
)
if err != nil {
log.Errorf("Unacceptable channel constraints: %v", err)
@@ -1386,7 +1386,19 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer,
remoteCsvDelay = acceptorResp.CSVDelay
}
chanReserve := f.cfg.RequiredRemoteChanReserve(amt, msg.DustLimit)
// If our default dust limit was above their ChannelReserve, we change
// it to the ChannelReserve. We must make sure the ChannelReserve we
// send in the AcceptChannel message is above both dust limits.
// Therefore, take the maximum of msg.DustLimit and our dust limit.
//
// NOTE: Even with this bounding, the ChannelAcceptor may return an
// BOLT#02-invalid ChannelReserve.
maxDustLimit := reservation.OurContribution().DustLimit
if msg.DustLimit > maxDustLimit {
maxDustLimit = msg.DustLimit
}
chanReserve := f.cfg.RequiredRemoteChanReserve(amt, maxDustLimit)
if acceptorResp.Reserve != 0 {
chanReserve = acceptorResp.Reserve
}
@@ -1576,7 +1588,7 @@ func (f *Manager) handleFundingAccept(peer lnpeer.Peer,
CsvDelay: msg.CsvDelay,
}
err = resCtx.reservation.CommitConstraints(
channelConstraints, resCtx.maxLocalCsv,
channelConstraints, resCtx.maxLocalCsv, false,
)
if err != nil {
log.Warnf("Unacceptable channel constraints: %v", err)
@@ -1586,8 +1598,11 @@ func (f *Manager) handleFundingAccept(peer lnpeer.Peer,
// As they've accepted our channel constraints, we'll regenerate them
// here so we can properly commit their accepted constraints to the
// reservation.
chanReserve := f.cfg.RequiredRemoteChanReserve(resCtx.chanAmt, msg.DustLimit)
// reservation. Also make sure that we re-generate the ChannelReserve
// with our dust limit or we can get stuck channels.
chanReserve := f.cfg.RequiredRemoteChanReserve(
resCtx.chanAmt, resCtx.reservation.OurContribution().DustLimit,
)
// The remote node has responded with their portion of the channel
// contribution. At this point, we can process their contribution which
@@ -3114,19 +3129,10 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
maxCSV = f.cfg.MaxLocalCSVDelay
}
// We'll determine our dust limit depending on which chain is active.
var ourDustLimit btcutil.Amount
switch f.cfg.RegisteredChains.PrimaryChain() {
case chainreg.BitcoinChain:
ourDustLimit = lnwallet.DefaultDustLimit()
case chainreg.LitecoinChain:
ourDustLimit = chainreg.DefaultLitecoinDustLimit
}
log.Infof("Initiating fundingRequest(local_amt=%v "+
"(subtract_fees=%v), push_amt=%v, chain_hash=%v, peer=%x, "+
"dust_limit=%v, min_confs=%v)", localAmt, msg.SubtractFees,
msg.PushAmt, msg.ChainHash, peerKey.SerializeCompressed(),
ourDustLimit, msg.MinConfs)
"min_confs=%v)", localAmt, msg.SubtractFees, msg.PushAmt,
msg.ChainHash, peerKey.SerializeCompressed(), msg.MinConfs)
// We set the channel flags to indicate whether we want this channel to
// be announced to the network.
@@ -3300,6 +3306,12 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
// request to the remote peer, kicking off the funding workflow.
ourContribution := reservation.OurContribution()
// Fetch our dust limit which is part of the default channel
// constraints, and log it.
ourDustLimit := ourContribution.DustLimit
log.Infof("Dust limit for pendingID(%x): %v", chanID, ourDustLimit)
// Finally, we'll use the current value of the channels and our default
// policy to determine of required commitment constraints for the
// remote party.
@@ -3313,7 +3325,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
PendingChannelID: chanID,
FundingAmount: capacity,
PushAmount: msg.PushAmt,
DustLimit: ourContribution.DustLimit,
DustLimit: ourDustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
HtlcMinimum: minHtlcIn,

View File

@@ -276,7 +276,7 @@ func createTestWallet(cdb *channeldb.DB, netParams *chaincfg.Params,
ChainIO: bio,
FeeEstimator: estimator,
NetParams: *netParams,
DefaultConstraints: chainreg.DefaultBtcChannelConstraints,
DefaultConstraints: chainreg.GenDefaultBtcConstraints(),
})
if err != nil {
return nil, err

11
go.mod
View File

@@ -5,13 +5,14 @@ require (
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2
github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4
github.com/btcsuite/btcd v0.22.0-beta.0.20210916191717-f8e6854197cd
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890
github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890
github.com/btcsuite/btcwallet v0.12.1-0.20210826004415-4ef582f76b02
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.2-0.20210803004036-eebed51155ec
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0
github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0
github.com/btcsuite/btcwallet/wallet/txrules v1.1.0
github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect
github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec
github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210822222949-9b5a201c344c
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
@@ -37,7 +38,7 @@ require (
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2 // indirect
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d // indirect
github.com/juju/version v0.0.0-20180108022336-b64dbd566305 // indirect
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec
github.com/kkdai/bstream v1.0.0
github.com/lightninglabs/neutrino v0.12.1
github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display
github.com/lightningnetwork/lightning-onion v1.0.2-0.20210520211913-522b799e65b1
@@ -56,7 +57,7 @@ require (
github.com/urfave/cli v1.20.0
go.etcd.io/etcd/client/pkg/v3 v3.5.0
go.etcd.io/etcd/client/v3 v3.5.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20210913180222-943fd674d43e
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 // indirect

19
go.sum
View File

@@ -76,8 +76,9 @@ github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcug
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.21.0-beta.0.20201208033208-6bd4c64a54fa/go.mod h1:Sv4JPQ3/M+teHz9Bo5jBpkNcP0x6r7rdihlNL/7tTAs=
github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4 h1:EmyLrldY44jDVa3dQ2iscj1S6ExuVJhRzCZBOXo93r0=
github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA=
github.com/btcsuite/btcd v0.22.0-beta.0.20210916191717-f8e6854197cd h1:Nq1fLF6IA8XfW0HTpLaVZoDKazt05J1C2AAeswYloBE=
github.com/btcsuite/btcd v0.22.0-beta.0.20210916191717-f8e6854197cd/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
@@ -92,13 +93,15 @@ github.com/btcsuite/btcwallet v0.12.1-0.20210826004415-4ef582f76b02 h1:Q8Scm1SXN
github.com/btcsuite/btcwallet v0.12.1-0.20210826004415-4ef582f76b02/go.mod h1:SdqXKJoEEi5LJq6zU67PcKiyqF97AcUOfBfyQHC7rqQ=
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU=
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.1-0.20210329233242-e0607006dce6/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU=
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.2-0.20210803004036-eebed51155ec h1:nuO8goa4gbgDM4iegCztF7mTq8io9NT1DAMoPrEI6S4=
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.2-0.20210803004036-eebed51155ec/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU=
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w=
github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 h1:8pO0pvPX1rFRfRiol4oV6kX7dY5y4chPwhfVwUfvwtk=
github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0/go.mod h1:ktYuJyumYtwG+QQ832Q+kqvxWJRAei3Nqs5qhSn4nww=
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA=
github.com/btcsuite/btcwallet/wallet/txrules v1.1.0 h1:Vg8G8zhNVjaCdwJg2QOmLoWn4RTP7K0J9xlwY8CJnLY=
github.com/btcsuite/btcwallet/wallet/txrules v1.1.0/go.mod h1:Zn9UTqpiTH+HOd5BLzSBzULzlOPmcoeyQIA0cp0WbQQ=
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs=
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.1-0.20210519225359-6ab9b615576f h1:bzrmHuQ3ZGWWhGDyTL0OqihQWXGXSXNuBPkDoDB8SS4=
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.1-0.20210519225359-6ab9b615576f/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs=
github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 h1:wZnOolEAeNOHzHTnznw/wQv+j35ftCIokNrnOTOU5o8=
github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs=
github.com/btcsuite/btcwallet/walletdb v1.3.4/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
github.com/btcsuite/btcwallet/walletdb v1.3.5/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec h1:zcAU3Ij8SmqaE+ITtS76fua2Niq7DRNp46sJRhi8PiI=
@@ -408,8 +411,9 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8=
github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
@@ -674,8 +678,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=

View File

@@ -45,6 +45,55 @@ func WitnessScriptHash(witnessScript []byte) ([]byte, error) {
return bldr.Script()
}
// WitnessPubKeyHash generates a pay-to-witness-pubkey-hash public key script
// paying to a version 0 witness program containing the passed serialized
// public key.
func WitnessPubKeyHash(pubkey []byte) ([]byte, error) {
bldr := txscript.NewScriptBuilder()
bldr.AddOp(txscript.OP_0)
pkhash := btcutil.Hash160(pubkey)
bldr.AddData(pkhash)
return bldr.Script()
}
// GenerateP2SH generates a pay-to-script-hash public key script paying to the
// passed redeem script.
func GenerateP2SH(script []byte) ([]byte, error) {
bldr := txscript.NewScriptBuilder()
bldr.AddOp(txscript.OP_HASH160)
scripthash := btcutil.Hash160(script)
bldr.AddData(scripthash)
bldr.AddOp(txscript.OP_EQUAL)
return bldr.Script()
}
// GenerateP2PKH generates a pay-to-public-key-hash public key script paying to
// the passed serialized public key.
func GenerateP2PKH(pubkey []byte) ([]byte, error) {
bldr := txscript.NewScriptBuilder()
bldr.AddOp(txscript.OP_DUP)
bldr.AddOp(txscript.OP_HASH160)
pkhash := btcutil.Hash160(pubkey)
bldr.AddData(pkhash)
bldr.AddOp(txscript.OP_EQUALVERIFY)
bldr.AddOp(txscript.OP_CHECKSIG)
return bldr.Script()
}
// GenerateUnknownWitness generates the maximum-sized witness public key script
// consisting of a version push and a 40-byte data push.
func GenerateUnknownWitness() ([]byte, error) {
bldr := txscript.NewScriptBuilder()
bldr.AddOp(txscript.OP_0)
witnessScript := make([]byte, 40)
bldr.AddData(witnessScript)
return bldr.Script()
}
// GenMultiSigScript generates the non-p2sh'd multisig script for 2 of 2
// pubkeys.
func GenMultiSigScript(aPub, bPub []byte) ([]byte, error) {

View File

@@ -41,11 +41,20 @@ const (
// - P2WSHWitnessProgram: 34 bytes
NestedP2WSHSize = 1 + P2WSHSize
// UnknownWitnessSize 42 bytes
// - OP_x: 1 byte
// - OP_DATA: 1 byte (max-size length)
// - max-size: 40 bytes
UnknownWitnessSize = 1 + 1 + 40
// P2PKHSize 25 bytes
P2PKHSize = 25
// P2PKHOutputSize 34 bytes
// - value: 8 bytes
// - var_int: 1 byte (pkscript_length)
// - pkscript (p2pkh): 25 bytes
P2PKHOutputSize = 8 + 1 + 25
P2PKHOutputSize = 8 + 1 + P2PKHSize
// P2WKHOutputSize 31 bytes
// - value: 8 bytes
@@ -59,11 +68,14 @@ const (
// - pkscript (p2wsh): 34 bytes
P2WSHOutputSize = 8 + 1 + P2WSHSize
// P2SHSize 23 bytes
P2SHSize = 23
// P2SHOutputSize 32 bytes
// - value: 8 bytes
// - var_int: 1 byte (pkscript_length)
// - pkscript (p2sh): 23 bytes
P2SHOutputSize = 8 + 1 + 23
P2SHOutputSize = 8 + 1 + P2SHSize
// P2PKHScriptSigSize 108 bytes
// - OP_DATA: 1 byte (signature length)

View File

@@ -16,6 +16,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
@@ -417,12 +418,15 @@ func testListChannels(net *lntest.NetworkHarness, t *harnessTest) {
// Check the returned response is correct.
aliceChannel := resp.Channels[0]
// Calculate the dust limit we'll use for the test.
dustLimit := lnwallet.DustLimitForSize(input.UnknownWitnessSize)
// defaultConstraints is a ChannelConstraints with default values. It is
// used to test against Alice's local channel constraints.
defaultConstraints := &lnrpc.ChannelConstraints{
CsvDelay: 4,
ChanReserveSat: 1000,
DustLimitSat: uint64(lnwallet.DefaultDustLimit()),
DustLimitSat: uint64(dustLimit),
MaxPendingAmtMsat: 99000000,
MinHtlcMsat: 1,
MaxAcceptedHtlcs: bobRemoteMaxHtlcs,
@@ -438,7 +442,7 @@ func testListChannels(net *lntest.NetworkHarness, t *harnessTest) {
customizedConstraints := &lnrpc.ChannelConstraints{
CsvDelay: 4,
ChanReserveSat: 1000,
DustLimitSat: uint64(lnwallet.DefaultDustLimit()),
DustLimitSat: uint64(dustLimit),
MaxPendingAmtMsat: 99000000,
MinHtlcMsat: customizedMinHtlc,
MaxAcceptedHtlcs: aliceRemoteMaxHtlcs,

View File

@@ -219,7 +219,7 @@ func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
// If the the output is too small after subtracting the fee, the coin
// selection cannot be performed with an amount this small.
if outputAmt <= dustLimit {
if outputAmt < dustLimit {
return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
"subtracting fees(%v) below dust limit(%v)", outputAmt,
requiredFeeNoChange, dustLimit)
@@ -233,7 +233,7 @@ func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
// If adding a change output leads to both outputs being above
// the dust limit, we'll add the change output. Otherwise we'll
// go with the no change tx we originally found.
if newChange > dustLimit && newOutput > dustLimit {
if newChange >= dustLimit && newOutput >= dustLimit {
outputAmt = newOutput
changeAmt = newChange
}

View File

@@ -383,7 +383,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: int64(fundingFee(feeRate, 1, false) + dustLimit),
Value: int64(fundingFee(feeRate, 1, false) + dustLimit - 1),
},
},
},

View File

@@ -304,7 +304,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
// Sanity check: The addition of the outputs should not lead to the
// creation of dust.
if changeAmt != 0 && changeAmt <= w.cfg.DustLimit {
if changeAmt != 0 && changeAmt < w.cfg.DustLimit {
return fmt.Errorf("change amount(%v) after coin "+
"select is below dust limit(%v)", changeAmt,
w.cfg.DustLimit)

View File

@@ -152,6 +152,14 @@ func ErrChanTooLarge(chanSize, maxChanSize btcutil.Amount) ReservationError {
}
}
// ErrInvalidDustLimit returns an error indicating that a proposed DustLimit
// was rejected.
func ErrInvalidDustLimit(dustLimit btcutil.Amount) ReservationError {
return ReservationError{
fmt.Errorf("dust limit %v is invalid", dustLimit),
}
}
// ErrHtlcIndexAlreadyFailed is returned when the HTLC index has already been
// failed, but has not been committed by our commitment state.
type ErrHtlcIndexAlreadyFailed uint64

View File

@@ -1,13 +1,50 @@
package lnwallet
import (
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/lightningnetwork/lnd/input"
)
// DefaultDustLimit is used to calculate the dust HTLC amount which will be
// send to other node during funding process.
func DefaultDustLimit() btcutil.Amount {
return txrules.GetDustThreshold(input.P2WSHSize, txrules.DefaultRelayFeePerKb)
// DustLimitForSize retrieves the dust limit for a given pkscript size. Given
// the size, it automatically determines whether the script is a witness script
// or not. It calls btcd's GetDustThreshold method under the hood. It must be
// called with a proper size parameter or else a panic occurs.
func DustLimitForSize(scriptSize int) btcutil.Amount {
var (
dustlimit btcutil.Amount
pkscript []byte
)
// With the size of the script, determine which type of pkscript to
// create. This will be used in the call to GetDustThreshold. We pass
// in an empty byte slice since the contents of the script itself don't
// matter.
switch scriptSize {
case input.P2WPKHSize:
pkscript, _ = input.WitnessPubKeyHash([]byte{})
case input.P2WSHSize:
pkscript, _ = input.WitnessScriptHash([]byte{})
case input.P2SHSize:
pkscript, _ = input.GenerateP2SH([]byte{})
case input.P2PKHSize:
pkscript, _ = input.GenerateP2PKH([]byte{})
case input.UnknownWitnessSize:
pkscript, _ = input.GenerateUnknownWitness()
default:
panic("invalid script size")
}
// Call GetDustThreshold with a TxOut containing the generated
// pkscript.
txout := &wire.TxOut{PkScript: pkscript}
dustlimit = btcutil.Amount(mempool.GetDustThreshold(txout))
return dustlimit
}

View File

@@ -0,0 +1,56 @@
package lnwallet
import (
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input"
"github.com/stretchr/testify/require"
)
// TestDustLimitForSize tests that we receive the expected dust limits for
// various script types from btcd's GetDustThreshold function.
func TestDustLimitForSize(t *testing.T) {
t.Parallel()
tests := []struct {
name string
size int
expectedLimit btcutil.Amount
}{
{
name: "p2pkh dust limit",
size: input.P2PKHSize,
expectedLimit: btcutil.Amount(546),
},
{
name: "p2sh dust limit",
size: input.P2SHSize,
expectedLimit: btcutil.Amount(540),
},
{
name: "p2wpkh dust limit",
size: input.P2WPKHSize,
expectedLimit: btcutil.Amount(294),
},
{
name: "p2wsh dust limit",
size: input.P2WSHSize,
expectedLimit: btcutil.Amount(330),
},
{
name: "unknown witness limit",
size: input.UnknownWitnessSize,
expectedLimit: btcutil.Amount(354),
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
dustlimit := DustLimitForSize(test.size)
require.Equal(t, test.expectedLimit, dustlimit)
})
}
}

View File

@@ -205,6 +205,9 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
feeMSat += 2 * lnwire.NewMSatFromSatoshis(anchorSize)
}
// Used to cut down on verbosity.
defaultDust := wallet.Cfg.DefaultConstraints.DustLimit
// If we're the responder to a single-funder reservation, then we have
// no initial balance in the channel unless the remote party is pushing
// some funds to us within the first commitment state.
@@ -218,7 +221,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
if int64(theirBalance) < 0 {
return nil, ErrFunderBalanceDust(
int64(commitFee), int64(theirBalance.ToSatoshis()),
int64(2*DefaultDustLimit()),
int64(2*defaultDust),
)
}
} else {
@@ -247,7 +250,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
if int64(ourBalance) < 0 {
return nil, ErrFunderBalanceDust(
int64(commitFee), int64(ourBalance),
int64(2*DefaultDustLimit()),
int64(2*defaultDust),
)
}
}
@@ -257,21 +260,21 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
// reject this channel creation request.
//
// TODO(roasbeef): reject if 30% goes to fees? dust channel
if initiator && ourBalance.ToSatoshis() <= 2*DefaultDustLimit() {
if initiator && ourBalance.ToSatoshis() <= 2*defaultDust {
return nil, ErrFunderBalanceDust(
int64(commitFee),
int64(ourBalance.ToSatoshis()),
int64(2*DefaultDustLimit()),
int64(2*defaultDust),
)
}
// Similarly we ensure their balance is reasonable if we are not the
// initiator.
if !initiator && theirBalance.ToSatoshis() <= 2*DefaultDustLimit() {
if !initiator && theirBalance.ToSatoshis() <= 2*defaultDust {
return nil, ErrFunderBalanceDust(
int64(commitFee),
int64(theirBalance.ToSatoshis()),
int64(2*DefaultDustLimit()),
int64(2*defaultDust),
)
}
@@ -391,7 +394,7 @@ func (r *ChannelReservation) SetNumConfsRequired(numConfs uint16) {
// will also attempt to verify the constraints for sanity, returning an error
// if the parameters are seemed unsound.
func (r *ChannelReservation) CommitConstraints(c *channeldb.ChannelConstraints,
maxLocalCSVDelay uint16) error {
maxLocalCSVDelay uint16, responder bool) error {
r.Lock()
defer r.Unlock()
@@ -406,6 +409,13 @@ func (r *ChannelReservation) CommitConstraints(c *channeldb.ChannelConstraints,
return ErrChanReserveTooSmall(c.ChanReserve, c.DustLimit)
}
// Validate against the maximum-sized witness script dust limit, and
// also ensure that the DustLimit is not too large.
maxWitnessLimit := DustLimitForSize(input.UnknownWitnessSize)
if c.DustLimit < maxWitnessLimit || c.DustLimit > 3*maxWitnessLimit {
return ErrInvalidDustLimit(c.DustLimit)
}
// Fail if we consider the channel reserve to be too large. We
// currently fail if it is greater than 20% of the channel capacity.
maxChanReserve := r.partialState.Capacity / 5
@@ -446,7 +456,7 @@ func (r *ChannelReservation) CommitConstraints(c *channeldb.ChannelConstraints,
// Our dust limit should always be less than or equal to our proposed
// channel reserve.
if r.ourContribution.DustLimit > c.ChanReserve {
if responder && r.ourContribution.DustLimit > c.ChanReserve {
r.ourContribution.DustLimit = c.ChanReserve
}
@@ -459,6 +469,31 @@ func (r *ChannelReservation) CommitConstraints(c *channeldb.ChannelConstraints,
return nil
}
// validateReserveBounds checks that both ChannelReserve values are above both
// DustLimit values. This not only avoids stuck channels, but is also mandated
// by BOLT#02 even if it's not explicit. This returns true if the bounds are
// valid. This function should be called with the lock held.
func (r *ChannelReservation) validateReserveBounds() bool {
ourDustLimit := r.ourContribution.DustLimit
ourRequiredReserve := r.ourContribution.ChanReserve
theirDustLimit := r.theirContribution.DustLimit
theirRequiredReserve := r.theirContribution.ChanReserve
// We take the smaller of the two ChannelReserves and compare it
// against the larger of the two DustLimits.
minChanReserve := ourRequiredReserve
if minChanReserve > theirRequiredReserve {
minChanReserve = theirRequiredReserve
}
maxDustLimit := ourDustLimit
if maxDustLimit < theirDustLimit {
maxDustLimit = theirDustLimit
}
return minChanReserve >= maxDustLimit
}
// OurContribution returns the wallet's fully populated contribution to the
// pending payment channel. See 'ChannelContribution' for further details
// regarding the contents of a contribution.

View File

@@ -446,7 +446,7 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness,
}
aliceChanReservation.SetNumConfsRequired(numReqConfs)
channelConstraints := &channeldb.ChannelConstraints{
DustLimit: lnwallet.DefaultDustLimit(),
DustLimit: alice.Cfg.DefaultConstraints.DustLimit,
ChanReserve: fundingAmount / 100,
MaxPendingAmount: lnwire.NewMSatFromSatoshis(fundingAmount),
MinHTLC: 1,
@@ -454,7 +454,7 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness,
CsvDelay: csvDelay,
}
err = aliceChanReservation.CommitConstraints(
channelConstraints, defaultMaxLocalCsvDelay,
channelConstraints, defaultMaxLocalCsvDelay, false,
)
if err != nil {
t.Fatalf("unable to verify constraints: %v", err)
@@ -490,7 +490,7 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness,
t.Fatalf("bob unable to init channel reservation: %v", err)
}
err = bobChanReservation.CommitConstraints(
channelConstraints, defaultMaxLocalCsvDelay,
channelConstraints, defaultMaxLocalCsvDelay, true,
)
if err != nil {
t.Fatalf("unable to verify constraints: %v", err)
@@ -896,7 +896,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
}
aliceChanReservation.SetNumConfsRequired(numReqConfs)
channelConstraints := &channeldb.ChannelConstraints{
DustLimit: lnwallet.DefaultDustLimit(),
DustLimit: alice.Cfg.DefaultConstraints.DustLimit,
ChanReserve: fundingAmt / 100,
MaxPendingAmount: lnwire.NewMSatFromSatoshis(fundingAmt),
MinHTLC: 1,
@@ -904,7 +904,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
CsvDelay: csvDelay,
}
err = aliceChanReservation.CommitConstraints(
channelConstraints, defaultMaxLocalCsvDelay,
channelConstraints, defaultMaxLocalCsvDelay, false,
)
if err != nil {
t.Fatalf("unable to verify constraints: %v", err)
@@ -947,7 +947,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
t.Fatalf("unable to create bob reservation: %v", err)
}
err = bobChanReservation.CommitConstraints(
channelConstraints, defaultMaxLocalCsvDelay,
channelConstraints, defaultMaxLocalCsvDelay, true,
)
if err != nil {
t.Fatalf("unable to verify constraints: %v", err)

View File

@@ -542,16 +542,19 @@ func testSpendValidation(t *testing.T, tweakless bool) {
Privkeys: []*btcec.PrivateKey{aliceKeyPriv},
}
// Calculate the dust limit we'll use for the test.
dustLimit := DustLimitForSize(input.UnknownWitnessSize)
aliceChanCfg := &channeldb.ChannelConfig{
ChannelConstraints: channeldb.ChannelConstraints{
DustLimit: DefaultDustLimit(),
DustLimit: dustLimit,
CsvDelay: csvTimeout,
},
}
bobChanCfg := &channeldb.ChannelConfig{
ChannelConstraints: channeldb.ChannelConstraints{
DustLimit: DefaultDustLimit(),
DustLimit: dustLimit,
CsvDelay: csvTimeout,
},
}

View File

@@ -689,12 +689,15 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
// If no chanFunder was provided, then we'll assume the default
// assembler, which is backed by the wallet's internal coin selection.
if req.ChanFunder == nil {
// We use the P2WSH dust limit since it is larger than the
// P2WPKH dust limit and to avoid threading through two
// different dust limits.
cfg := chanfunding.WalletConfig{
CoinSource: &CoinSource{l},
CoinSelectLocker: l,
CoinLocker: l,
Signer: l.Cfg.Signer,
DustLimit: DefaultDustLimit(),
DustLimit: DustLimitForSize(input.P2WSHSize),
}
req.ChanFunder = chanfunding.NewWalletAssembler(cfg)
}
@@ -1263,6 +1266,13 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
theirContribution := req.contribution
ourContribution := pendingReservation.ourContribution
// Perform bounds-checking on both ChannelReserve and DustLimit
// parameters.
if !pendingReservation.validateReserveBounds() {
req.err <- fmt.Errorf("invalid reserve and dust bounds")
return
}
var (
chanPoint *wire.OutPoint
err error
@@ -1561,9 +1571,6 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg
pendingReservation.Lock()
defer pendingReservation.Unlock()
// TODO(roasbeef): verify sanity of remote party's parameters, fail if
// disagree
// Validate that the remote's UpfrontShutdownScript is a valid script
// if it's set.
shutdown := req.contribution.UpfrontShutdown
@@ -1581,6 +1588,14 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg
theirContribution := pendingReservation.theirContribution
chanState := pendingReservation.partialState
// Perform bounds checking on both ChannelReserve and DustLimit
// parameters. The ChannelReserve may have been changed by the
// ChannelAcceptor RPC, so this is necessary.
if !pendingReservation.validateReserveBounds() {
req.err <- fmt.Errorf("invalid reserve and dust bounds")
return
}
// Initialize an empty sha-chain for them, tracking the current pending
// revocation hash (we don't yet know the preimage so we can't add it
// to the chain).

View File

@@ -1227,8 +1227,7 @@ func (r *rpcServer) SendCoins(ctx context.Context,
// pay to the change address created above if we needed to
// reserve any value, the rest will go to targetAddr.
sweepTxPkg, err := sweep.CraftSweepAllTx(
feePerKw, lnwallet.DefaultDustLimit(),
uint32(bestHeight), nil, targetAddr, wallet,
feePerKw, uint32(bestHeight), nil, targetAddr, wallet,
wallet, wallet.WalletController,
r.server.cc.FeeEstimator, r.server.cc.Signer,
minConfs,
@@ -1280,9 +1279,9 @@ func (r *rpcServer) SendCoins(ctx context.Context,
}
sweepTxPkg, err = sweep.CraftSweepAllTx(
feePerKw, lnwallet.DefaultDustLimit(),
uint32(bestHeight), outputs, targetAddr, wallet,
wallet, wallet.WalletController,
feePerKw, uint32(bestHeight), outputs,
targetAddr, wallet, wallet,
wallet.WalletController,
r.server.cc.FeeEstimator, r.server.cc.Signer,
minConfs,
)

View File

@@ -1150,7 +1150,7 @@ func (s *UtxoSweeper) getInputLists(cluster inputCluster,
if len(retryInputs) > 0 {
var err error
allSets, err = generateInputPartitionings(
append(retryInputs, newInputs...), s.relayFeeRate,
append(retryInputs, newInputs...),
cluster.sweepFeeRate, s.cfg.MaxInputsPerTx,
s.cfg.Wallet,
)
@@ -1161,8 +1161,8 @@ func (s *UtxoSweeper) getInputLists(cluster inputCluster,
// Create sets for just the new inputs.
newSets, err := generateInputPartitionings(
newInputs, s.relayFeeRate, cluster.sweepFeeRate,
s.cfg.MaxInputsPerTx, s.cfg.Wallet,
newInputs, cluster.sweepFeeRate, s.cfg.MaxInputsPerTx,
s.cfg.Wallet,
)
if err != nil {
return nil, fmt.Errorf("input partitionings: %v", err)
@@ -1193,7 +1193,7 @@ func (s *UtxoSweeper) sweep(inputs inputSet, feeRate chainfee.SatPerKWeight,
// Create sweep tx.
tx, err := createSweepTx(
inputs, nil, s.currentOutputScript, uint32(currentHeight),
feeRate, dustLimit(s.relayFeeRate), s.cfg.Signer,
feeRate, s.cfg.Signer,
)
if err != nil {
return fmt.Errorf("create sweep tx: %v", err)
@@ -1488,7 +1488,7 @@ func (s *UtxoSweeper) CreateSweepTx(inputs []input.Input, feePref FeePreference,
return createSweepTx(
inputs, nil, pkScript, currentBlockHeight, feePerKw,
dustLimit(s.relayFeeRate), s.cfg.Signer,
s.cfg.Signer,
)
}

View File

@@ -136,7 +136,8 @@ func createSweeperTestContext(t *testing.T) *sweeperTestContext {
Store: store,
Signer: &mock.DummySigner{},
GenSweepScript: func() ([]byte, error) {
script := []byte{outputScriptCount}
script := make([]byte, input.P2WPKHSize)
script[0] = outputScriptCount
outputScriptCount++
return script, nil
},
@@ -1801,6 +1802,24 @@ func TestRequiredTxOuts(t *testing.T) {
locktime2 := uint32(52)
locktime3 := uint32(53)
aPkScript := make([]byte, input.P2WPKHSize)
aPkScript[0] = 'a'
bPkScript := make([]byte, input.P2WSHSize)
bPkScript[0] = 'b'
cPkScript := make([]byte, input.P2PKHSize)
cPkScript[0] = 'c'
dPkScript := make([]byte, input.P2SHSize)
dPkScript[0] = 'd'
ePkScript := make([]byte, input.UnknownWitnessSize)
ePkScript[0] = 'e'
fPkScript := make([]byte, input.P2WSHSize)
fPkScript[0] = 'f'
testCases := []struct {
name string
inputs []*testInput
@@ -1815,7 +1834,7 @@ func TestRequiredTxOuts(t *testing.T) {
{
BaseInput: inputs[0],
reqTxOut: &wire.TxOut{
PkScript: []byte("aaa"),
PkScript: aPkScript,
Value: 100000,
},
},
@@ -1836,7 +1855,7 @@ func TestRequiredTxOuts(t *testing.T) {
// output must be the first one.
require.Equal(t, 2, len(tx.TxOut))
out := tx.TxOut[0]
require.Equal(t, []byte("aaa"), out.PkScript)
require.Equal(t, aPkScript, out.PkScript)
require.Equal(t, int64(100000), out.Value)
},
},
@@ -1848,13 +1867,13 @@ func TestRequiredTxOuts(t *testing.T) {
{
BaseInput: inputs[0],
reqTxOut: &wire.TxOut{
PkScript: []byte("aaa"),
PkScript: aPkScript,
// Fee will be about 5340 sats.
// Subtract a bit more to
// ensure no dust change output
// is manifested.
Value: inputs[0].SignDesc().Output.Value - 5600,
Value: inputs[0].SignDesc().Output.Value - 6300,
},
},
},
@@ -1871,10 +1890,10 @@ func TestRequiredTxOuts(t *testing.T) {
require.Equal(t, 1, len(tx.TxOut))
out := tx.TxOut[0]
require.Equal(t, []byte("aaa"), out.PkScript)
require.Equal(t, aPkScript, out.PkScript)
require.Equal(
t,
inputs[0].SignDesc().Output.Value-5600,
inputs[0].SignDesc().Output.Value-6300,
out.Value,
)
},
@@ -1897,7 +1916,7 @@ func TestRequiredTxOuts(t *testing.T) {
// The second input requires a TxOut.
BaseInput: inputs[0],
reqTxOut: &wire.TxOut{
PkScript: []byte("aaa"),
PkScript: aPkScript,
Value: inputs[0].SignDesc().Output.Value,
},
},
@@ -1916,7 +1935,7 @@ func TestRequiredTxOuts(t *testing.T) {
// The required TxOut should be the first one.
out := tx.TxOut[0]
require.Equal(t, []byte("aaa"), out.PkScript)
require.Equal(t, aPkScript, out.PkScript)
require.Equal(
t, inputs[0].SignDesc().Output.Value,
out.Value,
@@ -1947,7 +1966,7 @@ func TestRequiredTxOuts(t *testing.T) {
{
BaseInput: inputs[0],
reqTxOut: &wire.TxOut{
PkScript: []byte("aaa"),
PkScript: aPkScript,
Value: inputs[0].SignDesc().Output.Value,
},
},
@@ -1965,7 +1984,7 @@ func TestRequiredTxOuts(t *testing.T) {
require.Equal(t, 2, len(tx.TxOut))
out := tx.TxOut[0]
require.Equal(t, []byte("aaa"), out.PkScript)
require.Equal(t, aPkScript, out.PkScript)
require.Equal(
t, inputs[0].SignDesc().Output.Value,
out.Value,
@@ -1980,21 +1999,21 @@ func TestRequiredTxOuts(t *testing.T) {
{
BaseInput: inputs[0],
reqTxOut: &wire.TxOut{
PkScript: []byte("aaa"),
PkScript: aPkScript,
Value: inputs[0].SignDesc().Output.Value,
},
},
{
BaseInput: inputs[1],
reqTxOut: &wire.TxOut{
PkScript: []byte("bbb"),
PkScript: bPkScript,
Value: inputs[1].SignDesc().Output.Value,
},
},
{
BaseInput: inputs[2],
reqTxOut: &wire.TxOut{
PkScript: []byte("ccc"),
PkScript: cPkScript,
Value: inputs[2].SignDesc().Output.Value,
},
},
@@ -2041,7 +2060,7 @@ func TestRequiredTxOuts(t *testing.T) {
BaseInput: inputs[0],
locktime: &locktime1,
reqTxOut: &wire.TxOut{
PkScript: []byte("aaa"),
PkScript: aPkScript,
Value: inputs[0].SignDesc().Output.Value,
},
},
@@ -2049,7 +2068,7 @@ func TestRequiredTxOuts(t *testing.T) {
BaseInput: inputs[1],
locktime: &locktime1,
reqTxOut: &wire.TxOut{
PkScript: []byte("bbb"),
PkScript: bPkScript,
Value: inputs[1].SignDesc().Output.Value,
},
},
@@ -2057,7 +2076,7 @@ func TestRequiredTxOuts(t *testing.T) {
BaseInput: inputs[2],
locktime: &locktime2,
reqTxOut: &wire.TxOut{
PkScript: []byte("ccc"),
PkScript: cPkScript,
Value: inputs[2].SignDesc().Output.Value,
},
},
@@ -2065,7 +2084,7 @@ func TestRequiredTxOuts(t *testing.T) {
BaseInput: inputs[3],
locktime: &locktime2,
reqTxOut: &wire.TxOut{
PkScript: []byte("ddd"),
PkScript: dPkScript,
Value: inputs[3].SignDesc().Output.Value,
},
},
@@ -2073,7 +2092,7 @@ func TestRequiredTxOuts(t *testing.T) {
BaseInput: inputs[4],
locktime: &locktime3,
reqTxOut: &wire.TxOut{
PkScript: []byte("eee"),
PkScript: ePkScript,
Value: inputs[4].SignDesc().Output.Value,
},
},
@@ -2081,7 +2100,7 @@ func TestRequiredTxOuts(t *testing.T) {
BaseInput: inputs[5],
locktime: &locktime3,
reqTxOut: &wire.TxOut{
PkScript: []byte("fff"),
PkScript: fPkScript,
Value: inputs[5].SignDesc().Output.Value,
},
},

View File

@@ -7,7 +7,6 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -109,9 +108,6 @@ func (t *txInputSetState) clone() txInputSetState {
type txInputSet struct {
txInputSetState
// dustLimit is the minimum output value of the tx.
dustLimit btcutil.Amount
// maxInputs is the maximum number of inputs that will be accepted in
// the set.
maxInputs int
@@ -121,25 +117,15 @@ type txInputSet struct {
wallet Wallet
}
func dustLimit(relayFee chainfee.SatPerKWeight) btcutil.Amount {
return txrules.GetDustThreshold(
input.P2WPKHSize,
btcutil.Amount(relayFee.FeePerKVByte()),
)
}
// newTxInputSet constructs a new, empty input set.
func newTxInputSet(wallet Wallet, feePerKW,
relayFee chainfee.SatPerKWeight, maxInputs int) *txInputSet {
dustLimit := dustLimit(relayFee)
func newTxInputSet(wallet Wallet, feePerKW chainfee.SatPerKWeight,
maxInputs int) *txInputSet {
state := txInputSetState{
feeRate: feePerKW,
}
b := txInputSet{
dustLimit: dustLimit,
maxInputs: maxInputs,
wallet: wallet,
txInputSetState: state,
@@ -153,7 +139,7 @@ func newTxInputSet(wallet Wallet, feePerKW,
func (t *txInputSet) enoughInput() bool {
// If we have a change output above dust, then we certainly have enough
// inputs to the transaction.
if t.changeOutput >= t.dustLimit {
if t.changeOutput >= lnwallet.DustLimitForSize(input.P2WPKHSize) {
return true
}
@@ -192,8 +178,12 @@ func (t *txInputSet) addToState(inp input.Input, constraints addConstraints) *tx
// If the input comes with a required tx out that is below dust, we
// won't add it.
reqOut := inp.RequiredTxOut()
if reqOut != nil && btcutil.Amount(reqOut.Value) < t.dustLimit {
return nil
if reqOut != nil {
// Fetch the dust limit for this output.
dustLimit := lnwallet.DustLimitForSize(len(reqOut.PkScript))
if btcutil.Amount(reqOut.Value) < dustLimit {
return nil
}
}
// Clone the current set state.

View File

@@ -14,14 +14,9 @@ import (
func TestTxInputSet(t *testing.T) {
const (
feeRate = 1000
relayFee = 300
maxInputs = 10
)
set := newTxInputSet(nil, feeRate, relayFee, maxInputs)
if set.dustLimit != 537 {
t.Fatalf("incorrect dust limit")
}
set := newTxInputSet(nil, feeRate, maxInputs)
// Create a 300 sat input. The fee to sweep this input to a P2WKH output
// is 439 sats. That means that this input yields -139 sats and we
@@ -66,16 +61,15 @@ func TestTxInputSet(t *testing.T) {
func TestTxInputSetFromWallet(t *testing.T) {
const (
feeRate = 500
relayFee = 300
maxInputs = 10
)
wallet := &mockWallet{}
set := newTxInputSet(wallet, feeRate, relayFee, maxInputs)
set := newTxInputSet(wallet, feeRate, maxInputs)
// Add a 700 sat input to the set. It yields positively, but doesn't
// Add a 500 sat input to the set. It yields positively, but doesn't
// reach the output dust limit.
if !set.add(createP2WKHInput(700), constraintsRegular) {
if !set.add(createP2WKHInput(500), constraintsRegular) {
t.Fatal("expected add of positively yielding input to succeed")
}
if set.enoughInput() {
@@ -138,13 +132,9 @@ func (r *reqInput) RequiredTxOut() *wire.TxOut {
func TestTxInputSetRequiredOutput(t *testing.T) {
const (
feeRate = 1000
relayFee = 300
maxInputs = 10
)
set := newTxInputSet(nil, feeRate, relayFee, maxInputs)
if set.dustLimit != 537 {
t.Fatalf("incorrect dust limit")
}
set := newTxInputSet(nil, feeRate, maxInputs)
// Attempt to add an input with a required txout below the dust limit.
// This should fail since we cannot trim such outputs.
@@ -152,7 +142,7 @@ func TestTxInputSetRequiredOutput(t *testing.T) {
Input: createP2WKHInput(500),
txOut: &wire.TxOut{
Value: 500,
PkScript: make([]byte, 33),
PkScript: make([]byte, input.P2PKHSize),
},
}
require.False(t, set.add(inp, constraintsRegular),
@@ -164,7 +154,7 @@ func TestTxInputSetRequiredOutput(t *testing.T) {
Input: createP2WKHInput(1000),
txOut: &wire.TxOut{
Value: 1000,
PkScript: make([]byte, 22),
PkScript: make([]byte, input.P2WPKHSize),
},
}
require.True(t, set.add(inp, constraintsRegular), "failed adding input")

View File

@@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
@@ -37,8 +38,8 @@ type inputSet []input.Input
// inputs are skipped. No input sets with a total value after fees below the
// dust limit are returned.
func generateInputPartitionings(sweepableInputs []txInput,
relayFeePerKW, feePerKW chainfee.SatPerKWeight,
maxInputsPerTx int, wallet Wallet) ([]inputSet, error) {
feePerKW chainfee.SatPerKWeight, maxInputsPerTx int,
wallet Wallet) ([]inputSet, error) {
// Sort input by yield. We will start constructing input sets starting
// with the highest yield inputs. This is to prevent the construction
@@ -85,9 +86,7 @@ func generateInputPartitionings(sweepableInputs []txInput,
// Start building a set of positive-yield tx inputs under the
// condition that the tx will be published with the specified
// fee rate.
txInputs := newTxInputSet(
wallet, feePerKW, relayFeePerKW, maxInputsPerTx,
)
txInputs := newTxInputSet(wallet, feePerKW, maxInputsPerTx)
// From the set of sweepable inputs, keep adding inputs to the
// input set until the tx output value no longer goes up or the
@@ -111,10 +110,12 @@ func generateInputPartitionings(sweepableInputs []txInput,
// continuing with the remaining inputs will only lead to sets
// with an even lower output value.
if !txInputs.enoughInput() {
// The change output is always a p2wpkh here.
dl := lnwallet.DustLimitForSize(input.P2WPKHSize)
log.Debugf("Set value %v (r=%v, c=%v) below dust "+
"limit of %v", txInputs.totalOutput(),
txInputs.requiredOutput, txInputs.changeOutput,
txInputs.dustLimit)
dl)
return sets, nil
}
@@ -135,8 +136,8 @@ func generateInputPartitionings(sweepableInputs []txInput,
// sending any leftover change to the change script.
func createSweepTx(inputs []input.Input, outputs []*wire.TxOut,
changePkScript []byte, currentBlockHeight uint32,
feePerKw chainfee.SatPerKWeight, dustLimit btcutil.Amount,
signer input.Signer) (*wire.MsgTx, error) {
feePerKw chainfee.SatPerKWeight, signer input.Signer) (*wire.MsgTx,
error) {
inputs, estimator := getWeightEstimate(inputs, outputs, feePerKw)
txFee := estimator.fee()
@@ -227,16 +228,20 @@ func createSweepTx(inputs []input.Input, outputs []*wire.TxOut,
// sweep tx has a change output.
changeAmt := totalInput - requiredOutput - txFee
// We'll calculate the dust limit for the given changePkScript since it
// is variable.
changeLimit := lnwallet.DustLimitForSize(len(changePkScript))
// The txn will sweep the amount after fees to the pkscript generated
// above.
if changeAmt >= dustLimit {
if changeAmt >= changeLimit {
sweepTx.AddTxOut(&wire.TxOut{
PkScript: changePkScript,
Value: int64(changeAmt),
})
} else {
log.Infof("Change amt %v below dustlimit %v, not adding "+
"change output", changeAmt, dustLimit)
"change output", changeAmt, changeLimit)
}
// We'll default to using the current block height as locktime, if none

View File

@@ -165,8 +165,8 @@ type DeliveryAddr struct {
// output, as specified by the change 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 chainfee.SatPerKWeight, dustLimit btcutil.Amount,
blockHeight uint32, deliveryAddrs []DeliveryAddr, changeAddr btcutil.Address,
func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, blockHeight uint32,
deliveryAddrs []DeliveryAddr, changeAddr btcutil.Address,
coinSelectLocker CoinSelectionLocker, utxoSource UtxoSource,
outpointLocker OutpointLocker, feeEstimator chainfee.Estimator,
signer input.Signer, minConfs int32) (*WalletSweepPackage, error) {
@@ -302,7 +302,7 @@ func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, dustLimit btcutil.Amount,
// respects our fee preference and targets all the UTXOs of the wallet.
sweepTx, err := createSweepTx(
inputsToSweep, txOuts, changePkScript, blockHeight, feeRate,
dustLimit, signer,
signer,
)
if err != nil {
unlockOutputs()

View File

@@ -288,8 +288,8 @@ func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
utxoLocker := newMockOutpointLocker()
_, err := CraftSweepAllTx(
0, 100, 10, nil, nil, coinSelectLocker, utxoSource,
utxoLocker, nil, nil, 0,
0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLocker, nil,
nil, 0,
)
// Since we instructed the coin select locker to fail above, we should
@@ -314,8 +314,8 @@ func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
utxoLocker := newMockOutpointLocker()
_, err := CraftSweepAllTx(
0, 100, 10, nil, nil, coinSelectLocker, utxoSource,
utxoLocker, nil, nil, 0,
0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLocker, nil,
nil, 0,
)
// Since passed in a p2wsh output, which is unknown, we should fail to
@@ -349,7 +349,7 @@ func TestCraftSweepAllTx(t *testing.T) {
utxoLocker := newMockOutpointLocker()
sweepPkg, err := CraftSweepAllTx(
0, 100, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
utxoLocker, feeEstimator, signer, 0,
)
if err != nil {

View File

@@ -300,7 +300,7 @@ func TestBackupTask(t *testing.T) {
expSweepCommitRewardLocal int64 = 197390
expSweepCommitRewardRemote int64 = 98437
sweepFeeRateNoRewardRemoteDust chainfee.SatPerKWeight = 227500
sweepFeeRateRewardRemoteDust chainfee.SatPerKWeight = 175000
sweepFeeRateRewardRemoteDust chainfee.SatPerKWeight = 175350
)
if chanType.HasAnchors() {
expSweepCommitNoRewardBoth = 299236
@@ -309,8 +309,8 @@ func TestBackupTask(t *testing.T) {
expSweepCommitRewardBoth = 296112
expSweepCommitRewardLocal = 197389
expSweepCommitRewardRemote = 98433
sweepFeeRateNoRewardRemoteDust = 225000
sweepFeeRateRewardRemoteDust = 173750
sweepFeeRateNoRewardRemoteDust = 225400
sweepFeeRateRewardRemoteDust = 174100
}
backupTaskTests = append(backupTaskTests, []backupTaskTest{

View File

@@ -6,6 +6,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/watchtower/blob"
@@ -165,10 +166,9 @@ func (p *Policy) ComputeAltruistOutput(totalAmt btcutil.Amount,
sweepAmt := totalAmt - txFee
// TODO(conner): replace w/ configurable dust limit
dustLimit := lnwallet.DefaultDustLimit()
// Check that the created outputs won't be dusty.
if sweepAmt <= dustLimit {
// Check that the created outputs won't be dusty. The sweep pkscript is
// currently a p2wpkh, so we'll use that script's dust limit.
if sweepAmt < lnwallet.DustLimitForSize(input.P2WPKHSize) {
return 0, ErrCreatesDust
}
@@ -199,10 +199,9 @@ func (p *Policy) ComputeRewardOutputs(totalAmt btcutil.Amount,
sweepAmt := totalAmt - rewardAmt - txFee
// TODO(conner): replace w/ configurable dust limit
dustLimit := lnwallet.DefaultDustLimit()
// Check that the created outputs won't be dusty.
if sweepAmt <= dustLimit {
// Check that the created outputs won't be dusty. The sweep pkscript is
// currently a p2wpkh, so we'll use that script's dust limit.
if sweepAmt < lnwallet.DustLimitForSize(input.P2WPKHSize) {
return 0, 0, ErrCreatesDust
}