Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anchor output: handle unilateral close #1501

Merged
merged 2 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 10 additions & 21 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
maxAcceptedHtlcs = localParams.maxAcceptedHtlcs,
fundingPubkey = fundingPubKey,
revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey,
paymentBasepoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey),
paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey),
delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey,
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
Expand Down Expand Up @@ -307,7 +307,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
maxAcceptedHtlcs = localParams.maxAcceptedHtlcs,
fundingPubkey = fundingPubkey,
revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey,
paymentBasepoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey),
paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey),
delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey,
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
Expand Down Expand Up @@ -2048,13 +2048,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId

def handleMutualClose(closingTx: Transaction, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = {
log.info(s"closing tx published: closingTxId=${closingTx.txid}")

val nextData = d match {
case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil)
case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx)
}

goto(CLOSING) using nextData storing() calling (doPublish(closingTx))
goto(CLOSING) using nextData storing() calling doPublish(closingTx)
}

def doPublish(closingTx: Transaction): Unit = {
Expand All @@ -2063,29 +2061,24 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
}

def spendLocalCurrent(d: HasCommitments) = {

val outdatedCommitment = d match {
case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true
case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true
case _ => false
}

if (outdatedCommitment) {
log.warning("we have an outdated commitment: will not publish our local tx")
stay
} else {
val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx

val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)

val nextData = d match {
case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished))
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished))
case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = now, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished))
case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished))
}

goto(CLOSING) using nextData storing() calling (doPublish(localCommitPublished))
goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished)
}
}

Expand Down Expand Up @@ -2141,29 +2134,27 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch")

val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)

val nextData = d match {
case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished))
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished))
case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = now, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished))
case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished))
}

goto(CLOSING) using nextData storing() calling (doPublish(remoteCommitPublished))
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished)
}

def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = {
log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}")
d.commitments.channelVersion match {
case v if v.hasStaticRemotekey =>
case v if v.paysDirectlyToWallet =>
val remoteCommitPublished = RemoteCommitPublished(commitTx, None, List.empty, List.empty, Map.empty)
val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished))
goto(CLOSING) using nextData storing() // we don't need to claim our main output in the remote commit because it already spends to our wallet address
case _ =>
val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint
val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished))
goto(CLOSING) using nextData storing() calling (doPublish(remoteCommitPublished))
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished)
}
}

Expand All @@ -2174,15 +2165,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
require(commitTx.txid == remoteCommit.txid, "txid mismatch")

val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)

val nextData = d match {
case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished))
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished))
// NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined
case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished))
}

goto(CLOSING) using nextData storing() calling (doPublish(remoteCommitPublished))
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished)
}

def doPublish(remoteCommitPublished: RemoteCommitPublished): Unit = {
Expand Down Expand Up @@ -2217,7 +2206,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
// NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined
case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil)
}
goto(CLOSING) using nextData storing() calling (doPublish(revokedCommitPublished)) sending error
goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error
case None =>
// the published tx was neither their current commitment nor a revoked one
log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!")
Expand Down Expand Up @@ -2252,7 +2241,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx
val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)

goto(ERR_INFORMATION_LEAK) calling (doPublish(localCommitPublished)) sending error
goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished) sending error
}

def handleSync(channelReestablish: ChannelReestablish, d: HasCommitments): (Commitments, Queue[LightningMessage]) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ final case class LocalParams(nodeId: PublicKey,
maxAcceptedHtlcs: Int,
isFunder: Boolean,
defaultFinalScriptPubKey: ByteVector,
staticPaymentBasepoint: Option[PublicKey],
walletStaticPaymentBasepoint: Option[PublicKey],
pm47 marked this conversation as resolved.
Show resolved Hide resolved
features: Features)

final case class RemoteParams(nodeId: PublicKey,
Expand Down Expand Up @@ -351,6 +351,8 @@ case class ChannelVersion(bits: BitVector) {
def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT)
def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT)
def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT)
/** True if our main output in the remote commitment is directly sent to one of our wallet addresses. */
pm47 marked this conversation as resolved.
Show resolved Hide resolved
def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs
}

object ChannelVersion {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ case class Commitments(channelVersion: ChannelVersion,
commitInput: InputInfo,
remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) {

require(!channelVersion.hasStaticRemotekey || (channelVersion.hasStaticRemotekey && localParams.staticPaymentBasepoint.isDefined), s"localParams.localPaymentBasepoint must be defined for commitments with version=$channelVersion")
require(channelVersion.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (version=$channelVersion)")

val commitmentFormat: CommitmentFormat = channelVersion.commitmentFormat

Expand Down Expand Up @@ -617,7 +617,7 @@ object Commitments {
val remotePaymentPubkey = if (channelVersion.hasStaticRemotekey) remoteParams.paymentBasepoint else Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val localPaymentBasepoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelVersion.commitmentFormat)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, outputs)
val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feeratePerKw, outputs, channelVersion.commitmentFormat)
Expand All @@ -634,7 +634,7 @@ object Commitments {
spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val localPaymentBasepoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
val localPaymentPubkey = if (channelVersion.hasStaticRemotekey) localPaymentBasepoint else Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
Expand Down
34 changes: 25 additions & 9 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ object Helpers {
}.toSeq.flatten

channelVersion match {
case v if v.hasStaticRemotekey =>
case v if v.paysDirectlyToWallet =>
RemoteCommitPublished(
commitTx = tx,
claimMainOutputTx = None,
Expand Down Expand Up @@ -661,13 +661,22 @@ object Helpers {
def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = {
val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion)
val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey
val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget)

val mainTx = generateTx("claim-p2wpkh-output") {
Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => {
val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat)
Transactions.addSigs(claimMain, localPubkey, sig)
})
val mainTx = commitments.commitmentFormat match {
case DefaultCommitmentFormat => generateTx("claim-p2wpkh-output") {
Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => {
val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat)
Transactions.addSigs(claimMain, localPubkey, sig)
})
}
case AnchorOutputsCommitmentFormat => generateTx("claim-remote-delayed-output") {
Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => {
pm47 marked this conversation as resolved.
Show resolved Hide resolved
val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat)
Transactions.addSigs(claimMain, sig)
})
}
}

RemoteCommitPublished(
Expand All @@ -693,7 +702,7 @@ object Helpers {
require(tx.txIn.size == 1, "commitment tx should have 1 input")
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn.head.sequence, tx.lockTime)
val localPaymentPoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
// this tx has been published by remote, so we need to invert local/remote params
val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint)
require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long")
Expand All @@ -715,9 +724,15 @@ object Helpers {

// first we will claim our main output right away
val mainTx = channelVersion match {
case v if v.hasStaticRemotekey =>
log.info(s"channel uses option_static_remotekey, not claiming our p2wpkh output")
case v if v.paysDirectlyToWallet =>
log.info(s"channel uses option_static_remotekey to pay directly to our wallet, not claiming our p2wpkh output")
pm47 marked this conversation as resolved.
Show resolved Hide resolved
None
case v if v.hasAnchorOutputs => generateTx("claim-remote-delayed-output") {
Transactions.makeClaimRemoteDelayedOutputTx(tx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => {
val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat)
Transactions.addSigs(claimMain, sig)
})
}
case _ => generateTx("claim-p2wpkh-output") {
Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => {
val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat)
Expand Down Expand Up @@ -1071,6 +1086,7 @@ object Helpers {
// is the commitment tx buried? (we need to check this because we may not have any outputs)
val isCommitTxConfirmed = localCommitPublished.irrevocablySpent.values.toSet.contains(localCommitPublished.commitTx.txid)
// are there remaining spendable outputs from the commitment tx? we just subtract all known spent outputs from the ones we control
// NB: we ignore anchors here, claiming them can be batched later
val commitOutputsSpendableByUs = (localCommitPublished.claimMainDelayedOutputTx.toSeq ++ localCommitPublished.htlcSuccessTxs ++ localCommitPublished.htlcTimeoutTxs)
.flatMap(_.txIn.map(_.outPoint)).toSet -- localCommitPublished.irrevocablySpent.keys
// which htlc delayed txes can we expect to be confirmed?
Expand Down
Loading