Skip to content

Commit

Permalink
Add support for setting nSequence and nLocktime
Browse files Browse the repository at this point in the history
We add `nSequence` and `nLocktime` to `closing_complete`, to allow the
initiator to decide what values to use to provide better anonymity and
protection against fee sniping.
  • Loading branch information
t-bast committed Jan 16, 2024
1 parent 57ae172 commit 3b6879b
Show file tree
Hide file tree
Showing 7 changed files with 27 additions and 23 deletions.
12 changes: 7 additions & 5 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -690,26 +690,28 @@ object Helpers {
}

/** We are the closer: we sign closing transactions for which we pay the fees. */
def makeSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = {
def makeSimpleClosingTx(currentBlockHeight: BlockHeight, keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = {
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit = true, allowOpReturn = true), "invalid localScriptPubkey")
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit = true, allowOpReturn = true), "invalid remoteScriptPubkey")
// We want to signal replaceability and use a widely used value for nSequence to avoid fingerprinting.
val sequence = 0xFFFFFFFDL
val closingFee = {
val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), localScriptPubkey, remoteScriptPubkey)
val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), sequence, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
dummyClosingTxs.preferred_opt match {
case Some(dummyTx) =>
val dummySignedTx = Transactions.addSigs(dummyTx, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig)
SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.tx.weight()))
case None => return Left(CannotGenerateClosingTx(commitment.channelId))
}
}
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, localScriptPubkey, remoteScriptPubkey)
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, sequence, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
// The actual fee we're paying will be bigger than the one we previously computed if we omit our output.
val actualFee = closingTxs.preferred_opt match {
case Some(closingTx) if closingTx.fee > 0.sat => closingTx.fee
case _ => return Left(CannotGenerateClosingTx(commitment.channelId))
}
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
val closingComplete = ClosingComplete(commitment.channelId, actualFee, TlvStream(Set(
val closingComplete = ClosingComplete(commitment.channelId, actualFee, sequence, currentBlockHeight.toLong, TlvStream(Set(
closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserNoClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
closingTxs.remoteOnly_opt.map(tx => ClosingTlv.NoCloserClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
Expand All @@ -724,7 +726,7 @@ object Helpers {
*/
def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = {
val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees)
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, localScriptPubkey, remoteScriptPubkey)
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.sequence, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey)
// If our output isn't dust, they must provide a signature for a transaction that includes it.
// Note that we're the closee, so we look for signatures including the closee output.
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
// there are no pending signed changes, let's directly negotiate a closing transaction
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates)
MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, closingFeerate) match {
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, closingFeerate) match {
case Left(f) =>
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
goto(NEGOTIATING_SIMPLE) using DATA_NEGOTIATING_SIMPLE(d.commitments, localShutdown, remoteShutdown, Nil, Nil) storing() sending sendList
Expand Down Expand Up @@ -1330,7 +1330,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates)
MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, closingFeerate) match {
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, closingFeerate) match {
case Left(f) =>
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
goto(NEGOTIATING_SIMPLE) using DATA_NEGOTIATING_SIMPLE(d.commitments, localShutdown, remoteShutdown, Nil, Nil) storing() sending revocation
Expand Down Expand Up @@ -1381,7 +1381,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String)
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates)
MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, closingFeerate) match {
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, closingFeerate) match {
case Left(f) =>
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
goto(NEGOTIATING_SIMPLE) using DATA_NEGOTIATING_SIMPLE(d.commitments, localShutdown, remoteShutdown, Nil, Nil) storing()
Expand Down Expand Up @@ -1560,7 +1560,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
if (remoteShutdown.scriptPubKey != d.remoteShutdown.scriptPubKey) {
// Our peer changed their closing script: we sign a new version of our closing transaction using the new script.
val feerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates)
MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, feerate) match {
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, feerate) match {
case Left(_) => stay() using d.copy(remoteShutdown = remoteShutdown) storing()
case Right((closingTxs, closingComplete)) => stay() using d.copy(remoteShutdown = remoteShutdown, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) storing() sending closingComplete
}
Expand Down Expand Up @@ -1621,7 +1621,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
if (localShutdown_opt.nonEmpty || c.feerates.nonEmpty) {
val localScript = localShutdown_opt.map(_.scriptPubKey).getOrElse(d.localShutdown.scriptPubKey)
val feerate = c.feerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates))
MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, localScript, d.remoteShutdown.scriptPubKey, feerate) match {
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, d.remoteShutdown.scriptPubKey, feerate) match {
case Left(f) => handleCommandError(f, c)
case Right((closingTxs, closingComplete)) =>
log.info("new closing transaction created with script={} fees={}", localScript, closingComplete.fees)
Expand Down Expand Up @@ -2241,7 +2241,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
// We retransmit our shutdown: we may have updated our script and they may not have received it.
// We also sign a new round of closing transactions since network fees may have changed while we were offline.
val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates)
Closing.MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, closingFeerate) match {
Closing.MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, closingFeerate) match {
case Left(_) => goto(NEGOTIATING_SIMPLE) using d sending d.localShutdown
case Right((closingTxs, closingComplete)) => goto(NEGOTIATING_SIMPLE) using d.copy(proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) sending Seq(d.localShutdown, closingComplete)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -840,10 +840,10 @@ object Transactions {
val all: Seq[ClosingTx] = Seq(localAndRemote_opt, localOnly_opt, remoteOnly_opt).flatten
}

def makeSimpleClosingTxs(input: InputInfo, spec: CommitmentSpec, fee: SimpleClosingTxFee, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector): ClosingTxs = {
def makeSimpleClosingTxs(input: InputInfo, spec: CommitmentSpec, fee: SimpleClosingTxFee, sequence: Long, lockTime: Long, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector): ClosingTxs = {
require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs")

val txNoOutput = Transaction(2, Seq(TxIn(input.outPoint, ByteVector.empty, sequence = 0xFFFFFFFDL)), Nil, 0)
val txNoOutput = Transaction(2, Seq(TxIn(input.outPoint, ByteVector.empty, sequence)), Nil, lockTime)

val (toLocalAmount, toRemoteAmount) = fee match {
case SimpleClosingTxFee.PaidByUs(fee) => (spec.toLocal.truncateToSatoshi - fee, spec.toRemote.truncateToSatoshi)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ object LightningMessageCodecs {
val closingCompleteCodec: Codec[ClosingComplete] = (
("channelId" | bytes32) ::
("fees" | satoshi) ::
("sequence" | uint32) ::
("lockTime" | uint32) ::
("tlvStream" | ClosingTlv.closingTlvCodec)).as[ClosingComplete]

val closingSigCodec: Codec[ClosingSig] = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ case class ClosingSigned(channelId: ByteVector32,
val feeRange_opt = tlvStream.get[ClosingSignedTlv.FeeRange]
}

case class ClosingComplete(channelId: ByteVector32, fees: Satoshi, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
case class ClosingComplete(channelId: ByteVector32, fees: Satoshi, sequence: Long, lockTime: Long, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
val closerNoCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserNoClosee].map(_.sig)
val noCloserCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.NoCloserClosee].map(_.sig)
val closerAndCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndClosee].map(_.sig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package fr.acinq.eclair.transactions
import fr.acinq.bitcoin.SigHash._
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, ripemd160, sha256}
import fr.acinq.bitcoin.scalacompat.Script.{pay2wpkh, pay2wsh, write}
import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi}
import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi}
import fr.acinq.eclair.TestUtils.randomTxId
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
Expand Down Expand Up @@ -831,7 +831,7 @@ class TransactionsSpec extends AnyFunSuite with Logging {
{
// Different amounts, both outputs untrimmed, local is closer (option_simple_close):
val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 250_000_000 msat)
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(5_000 sat), localPubKeyScript, remotePubKeyScript)
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(5_000 sat), 0xFFFFFFFDL, 0, localPubKeyScript, remotePubKeyScript)
assert(closingTxs.localAndRemote_opt.nonEmpty)
assert(closingTxs.localOnly_opt.nonEmpty)
assert(closingTxs.remoteOnly_opt.isEmpty)
Expand Down Expand Up @@ -868,7 +868,7 @@ class TransactionsSpec extends AnyFunSuite with Logging {
{
// Their output is trimmed (option_simple_close):
val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_000_000 msat)
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(800 sat), localPubKeyScript, remotePubKeyScript)
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(800 sat), 0xFFFFFFFDL, 0, localPubKeyScript, remotePubKeyScript)
assert(closingTxs.all.size == 1)
assert(closingTxs.localOnly_opt.nonEmpty)
val toLocal = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get
Expand All @@ -886,7 +886,7 @@ class TransactionsSpec extends AnyFunSuite with Logging {
{
// Our output is trimmed (option_simple_close):
val spec = CommitmentSpec(Set.empty, feeratePerKw, 1_000_000 msat, 150_000_000 msat)
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(800 sat), localPubKeyScript, remotePubKeyScript)
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(800 sat), 0xFFFFFFFDL, 0, localPubKeyScript, remotePubKeyScript)
assert(closingTxs.all.size == 1)
assert(closingTxs.remoteOnly_opt.nonEmpty)
assert(closingTxs.remoteOnly_opt.flatMap(_.toLocalOutput).isEmpty)
Expand Down
Loading

0 comments on commit 3b6879b

Please sign in to comment.