Skip to content

Commit 58e30a1

Browse files
Set remote dust limit lower bound (#294)
We are slowly dropping support for non-segwit outputs, as proposed in lightning/bolts#894 We can thus safely allow dust limits all the way down to 354 satoshis. In very rare cases where dust_limit_satoshis is negotiated to a low value, our peer may generate closing txs that will not correctly relay on the bitcoin network due to dust relay policies. When that happens, we detect it and force-close instead of completing the mutual close flow.
1 parent 6e1fb8d commit 58e30a1

File tree

5 files changed

+73
-16
lines changed

5 files changed

+73
-16
lines changed

src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3011,8 +3011,10 @@ object Channel {
30113011

30123012
// We may need to rely on our peer's commit tx in certain cases (backup/restore) so we must ensure their transactions
30133013
// can propagate through the bitcoin network (assuming bitcoin core nodes with default policies).
3014-
// A minimal spend of a p2wsh output is 110 bytes and bitcoin core's dust-relay-fee is 3000 sat/kb, which amounts to 330 sat.
3015-
val MIN_DUST_LIMIT = 330.sat
3014+
// The various dust limits enforced by the bitcoin network are summarized here:
3015+
// https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits
3016+
// A dust limit of 354 sat ensures all segwit outputs will relay with default relay policies.
3017+
val MIN_DUST_LIMIT = 354.sat
30163018

30173019
// we won't exchange more than this many signatures when negotiating the closing fee
30183020
const val MAX_NEGOTIATION_ITERATIONS = 20

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ data class InvalidCommitmentSignature (override val channelId: Byte
4848
data class InvalidHtlcSignature (override val channelId: ByteVector32, val tx: Transaction) : ChannelException(channelId, "invalid htlc signature: tx=$tx")
4949
data class InvalidCloseSignature (override val channelId: ByteVector32, val tx: Transaction) : ChannelException(channelId, "invalid close signature: tx=$tx")
5050
data class InvalidCloseFee (override val channelId: ByteVector32, val fee: Satoshi) : ChannelException(channelId, "invalid close fee: fee_satoshis=$fee")
51+
data class InvalidCloseAmountBelowDust (override val channelId: ByteVector32, val tx: Transaction) : ChannelException(channelId, "invalid closing tx: some outputs are below dust: tx=$tx")
5152
data class HtlcSigCountMismatch (override val channelId: ByteVector32, val expected: Int, val actual: Int) : ChannelException(channelId, "htlc sig count mismatch: expected=$expected actual: $actual")
5253
data class ForcedLocalCommit (override val channelId: ByteVector32) : ChannelException(channelId, "forced local commit")
5354
data class UnexpectedHtlcId (override val channelId: ByteVector32, val expected: Long, val actual: Long) : ChannelException(channelId, "unexpected htlc id: expected=$expected actual=$actual")

src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -451,10 +451,32 @@ object Helpers {
451451
remoteClosingSig: ByteVector64
452452
): Either<ChannelException, ClosingTx> {
453453
val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee)
454-
val signedClosingTx = Transactions.addSigs(closingTx, commitments.localParams.channelKeys.fundingPubKey, commitments.remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
455-
return when (Transactions.checkSpendable(signedClosingTx)) {
456-
is Try.Success -> Either.Right(signedClosingTx)
457-
is Try.Failure -> Either.Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx))
454+
return if (checkClosingDustAmounts(closingTx)) {
455+
val signedClosingTx = Transactions.addSigs(closingTx, commitments.localParams.channelKeys.fundingPubKey, commitments.remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
456+
when (Transactions.checkSpendable(signedClosingTx)) {
457+
is Try.Success -> Either.Right(signedClosingTx)
458+
is Try.Failure -> Either.Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx))
459+
}
460+
} else {
461+
Either.Left(InvalidCloseAmountBelowDust(commitments.channelId, closingTx.tx))
462+
}
463+
}
464+
465+
/**
466+
* Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk
467+
* that the closing transaction will not be relayed to miners' mempool and will not confirm.
468+
* The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits
469+
*/
470+
fun checkClosingDustAmounts(closingTx: ClosingTx): Boolean {
471+
return closingTx.tx.txOut.all { txOut ->
472+
val publicKeyScript = txOut.publicKeyScript.toByteArray()
473+
when {
474+
Script.isPay2pkh(publicKeyScript) -> txOut.amount >= 546.sat
475+
Script.isPay2sh(publicKeyScript) -> txOut.amount >= 540.sat
476+
Script.isPay2wpkh(publicKeyScript) -> txOut.amount >= 294.sat
477+
Script.isPay2wsh(publicKeyScript) -> txOut.amount >= 330.sat
478+
else -> false
479+
}
458480
}
459481
}
460482

src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ import fr.acinq.bitcoin.*
44
import fr.acinq.bitcoin.Bitcoin.computeP2PkhAddress
55
import fr.acinq.bitcoin.Bitcoin.computeP2ShOfP2WpkhAddress
66
import fr.acinq.bitcoin.Bitcoin.computeP2WpkhAddress
7+
import fr.acinq.lightning.Lightning.randomKey
8+
import fr.acinq.lightning.channel.Helpers.Closing.checkClosingDustAmounts
79
import fr.acinq.lightning.tests.utils.LightningTestSuite
10+
import fr.acinq.lightning.transactions.Transactions
11+
import fr.acinq.lightning.utils.sat
812
import fr.acinq.secp256k1.Hex
913
import kotlin.test.Test
1014
import kotlin.test.assertEquals
15+
import kotlin.test.assertFalse
16+
import kotlin.test.assertTrue
1117

1218
class HelpersTestsCommon : LightningTestSuite() {
1319

@@ -30,14 +36,14 @@ class HelpersTestsCommon : LightningTestSuite() {
3036
}
3137

3238
listOf(
33-
Triple("0014d0b19277b0f76c9512f26d77573fd31a8fd15fc7", Block.TestnetGenesisBlock.hash, "tb1q6zceyaas7akf2yhjd4m4w07nr28azh78gw79kk"),
34-
Triple("00203287047df2aa7aade3f394790a9c9d6f9235943f48a012e8a9f2c3300ca4f2d1", Block.TestnetGenesisBlock.hash, "tb1qx2rsgl0j4fa2mclnj3us48yad7frt9plfzsp969f7tpnqr9y7tgsyprxej"),
35-
Triple("76a914b17deefe2feab87fef7221cf806bb8ca61f00fa188ac", Block.TestnetGenesisBlock.hash, "mwhSm2SHhRhd19KZyaQLgJyAtCLnkbzWbf"),
36-
Triple("a914d3cf9d04f4ecc36df8207b300e46bc6775fc84c087", Block.TestnetGenesisBlock.hash, "2NCZBGzKadAnLv1ijAqhrKavMuqvxqu18yY"),
37-
Triple("00145cb882efd643b7d63ae133e4d5e88e10bd5a20d7", Block.LivenetGenesisBlock.hash, "bc1qtjug9m7kgwmavwhpx0jdt6ywzz745gxhxwyn8u"),
38-
Triple("00208c2865c87ffd33fc5d698c7df9cf2d0fb39d93103c637a06dea32c848ebc3e1d", Block.LivenetGenesisBlock.hash, "bc1q3s5xtjrll5elchtf337lnnedp7eemycs833h5pk75vkgfr4u8cws3ytg02"),
39-
Triple("76a914536ffa992491508dca0354e52f32a3a7a679a53a88ac", Block.LivenetGenesisBlock.hash, "18cBEMRxXHqzWWCxZNtU91F5sbUNKhL5PX"),
40-
Triple("a91481b9ac6a59b53927da7277b5ad5460d781b365d987", Block.LivenetGenesisBlock.hash, "3DWwX7NYjnav66qygrm4mBCpiByjammaWy"),
39+
Triple("0014d0b19277b0f76c9512f26d77573fd31a8fd15fc7", Block.TestnetGenesisBlock.hash, "tb1q6zceyaas7akf2yhjd4m4w07nr28azh78gw79kk"),
40+
Triple("00203287047df2aa7aade3f394790a9c9d6f9235943f48a012e8a9f2c3300ca4f2d1", Block.TestnetGenesisBlock.hash, "tb1qx2rsgl0j4fa2mclnj3us48yad7frt9plfzsp969f7tpnqr9y7tgsyprxej"),
41+
Triple("76a914b17deefe2feab87fef7221cf806bb8ca61f00fa188ac", Block.TestnetGenesisBlock.hash, "mwhSm2SHhRhd19KZyaQLgJyAtCLnkbzWbf"),
42+
Triple("a914d3cf9d04f4ecc36df8207b300e46bc6775fc84c087", Block.TestnetGenesisBlock.hash, "2NCZBGzKadAnLv1ijAqhrKavMuqvxqu18yY"),
43+
Triple("00145cb882efd643b7d63ae133e4d5e88e10bd5a20d7", Block.LivenetGenesisBlock.hash, "bc1qtjug9m7kgwmavwhpx0jdt6ywzz745gxhxwyn8u"),
44+
Triple("00208c2865c87ffd33fc5d698c7df9cf2d0fb39d93103c637a06dea32c848ebc3e1d", Block.LivenetGenesisBlock.hash, "bc1q3s5xtjrll5elchtf337lnnedp7eemycs833h5pk75vkgfr4u8cws3ytg02"),
45+
Triple("76a914536ffa992491508dca0354e52f32a3a7a679a53a88ac", Block.LivenetGenesisBlock.hash, "18cBEMRxXHqzWWCxZNtU91F5sbUNKhL5PX"),
46+
Triple("a91481b9ac6a59b53927da7277b5ad5460d781b365d987", Block.LivenetGenesisBlock.hash, "3DWwX7NYjnav66qygrm4mBCpiByjammaWy"),
4147
).forEach {
4248
assertEquals(
4349
Helpers.Closing.btcAddressFromScriptPubKey(
@@ -48,4 +54,30 @@ class HelpersTestsCommon : LightningTestSuite() {
4854
)
4955
}
5056
}
57+
58+
@Test
59+
fun `check closing tx amounts above dust`() {
60+
val p2pkhBelowDust = listOf(TxOut(545.sat, Script.pay2pkh(randomKey().publicKey())))
61+
val p2shBelowDust = listOf(TxOut(539.sat, Script.pay2sh(Hex.decode("0000000000000000000000000000000000000000"))))
62+
val p2wpkhBelowDust = listOf(TxOut(293.sat, Script.pay2wpkh(randomKey().publicKey())))
63+
val p2wshBelowDust = listOf(TxOut(329.sat, Script.pay2wsh(Hex.decode("0000000000000000000000000000000000000000"))))
64+
val allOutputsAboveDust = listOf(
65+
TxOut(546.sat, Script.pay2pkh(randomKey().publicKey())),
66+
TxOut(540.sat, Script.pay2sh(Hex.decode("0000000000000000000000000000000000000000"))),
67+
TxOut(294.sat, Script.pay2wpkh(randomKey().publicKey())),
68+
TxOut(330.sat, Script.pay2wsh(Hex.decode("0000000000000000000000000000000000000000"))),
69+
)
70+
71+
fun toClosingTx(txOut: List<TxOut>): Transactions.TransactionWithInputInfo.ClosingTx {
72+
val input = Transactions.InputInfo(OutPoint(ByteVector32.Zeroes, 0), TxOut(1000.sat, listOf()), listOf())
73+
return Transactions.TransactionWithInputInfo.ClosingTx(input, Transaction(2, listOf(), txOut, 0), null)
74+
}
75+
76+
assertTrue(checkClosingDustAmounts(toClosingTx(allOutputsAboveDust)))
77+
assertFalse(checkClosingDustAmounts(toClosingTx(p2pkhBelowDust)))
78+
assertFalse(checkClosingDustAmounts(toClosingTx(p2shBelowDust)))
79+
assertFalse(checkClosingDustAmounts(toClosingTx(p2wpkhBelowDust)))
80+
assertFalse(checkClosingDustAmounts(toClosingTx(p2wshBelowDust)))
81+
}
82+
5183
}

src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() {
4141
@Test
4242
fun `recv AcceptChannel (dust limit too low)`() {
4343
val (alice, _, accept) = init()
44-
// we don't want their dust limit to be below 330
45-
val lowDustLimitSatoshis = 329.sat
44+
// we don't want their dust limit to be below 354
45+
val lowDustLimitSatoshis = 353.sat
4646
// but we only enforce it on mainnet
4747
val aliceMainnet = alice.copy(staticParams = alice.staticParams.copy(nodeParams = alice.staticParams.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash)))
4848
val (alice1, actions1) = aliceMainnet.process(ChannelEvent.MessageReceived(accept.copy(dustLimitSatoshis = lowDustLimitSatoshis)))

0 commit comments

Comments
 (0)