Skip to content

Commit

Permalink
Activate anchor output in channels (#1491)
Browse files Browse the repository at this point in the history
Allow activating anchor outputs and have fully operating channels
during normal operation (open, add/fulfill/fail htlcs, close).

Interop testing has been done with lnd, and there is only one pending
issue during mutual close, where they incorrectly compute the closing
amounts, which they should fix soon.

However, anchor outputs should NOT be activated yet as unilateral
close scenario are not fully handled yet.

We don't do any kind of automatic fee bumping either; this will be done
later, once we have PSBT support and once bitcoind offers the
`psbtbumpfee` RPC (see bitcoin/bitcoin#18654).
  • Loading branch information
t-bast authored Jul 27, 2020
1 parent 8cd2644 commit 3d4e00f
Show file tree
Hide file tree
Showing 23 changed files with 516 additions and 284 deletions.
17 changes: 12 additions & 5 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ case class Features(activated: Set[ActivatedFeature], unknown: Set[UnknownFeatur
}

/**
* Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask
* off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue.
* We use a long enough mask to account for future features.
* TODO: remove that once eclair-mobile is patched.
*/
* Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask
* off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue.
* We use a long enough mask to account for future features.
* TODO: remove that once eclair-mobile is patched.
*/
def maskFeaturesForEclairMobile(): Features = {
Features(
activated = activated.filter {
Expand Down Expand Up @@ -190,6 +190,11 @@ object Features {
val mandatory = 18
}

case object AnchorOutputs extends Feature {
val rfcName = "option_anchor_outputs"
val mandatory = 20
}

// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
// We're not advertising these bits yet in our announcements, clients have to assume support.
// This is why we haven't added them yet to `areSupported`.
Expand All @@ -214,6 +219,7 @@ object Features {
Wumbo,
TrampolinePayment,
StaticRemoteKey,
AnchorOutputs,
KeySend
)

Expand All @@ -234,6 +240,7 @@ object Features {
// invoices in their payment history. We choose to treat such invoices as valid; this is a harmless spec violation.
// PaymentSecret -> (VariableLengthOnion :: Nil),
BasicMultiPartPayment -> (PaymentSecret :: Nil),
AnchorOutputs -> (StaticRemoteKey :: Nil),
TrampolinePayment -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil)
)
Expand Down
17 changes: 7 additions & 10 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import akka.event.Logging.MDC
import akka.pattern.pipe
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Script, ScriptFlags, Transaction}
import fr.acinq.eclair.Features.StaticRemoteKey
import fr.acinq.eclair.Logs.LogCategory
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
Expand All @@ -33,6 +32,7 @@ import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.{Origin, Relayer}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Transactions.TxOwner
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
import scodec.bits.ByteVector
Expand Down Expand Up @@ -292,10 +292,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Success(_) =>
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None))
val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val channelVersion = Features.canUseFeature(localParams.features, remoteInit.features, StaticRemoteKey) match {
case false => ChannelVersion.STANDARD
case true => ChannelVersion.STATIC_REMOTEKEY
}
val channelVersion = ChannelVersion.pickChannelVersion(localParams.features, remoteInit.features)
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
// TODO: maybe also check uniqueness of temporary channel id
val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis)
Expand Down Expand Up @@ -391,7 +388,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
// let's create the first commitment tx that spends the yet uncommitted funding tx
val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, channelVersion, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint)
require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!")
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath))
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Remote, channelVersion.commitmentFormat)
// signature of their initial commitment tx that pays remote pushMsat
val fundingCreated = FundingCreated(
temporaryChannelId = temporaryChannelId,
Expand Down Expand Up @@ -434,12 +431,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId

// check remote signature validity
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey)
val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelVersion.commitmentFormat)
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
Transactions.checkSpendable(signedLocalCommitTx) match {
case Failure(cause) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None)
case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None)
case Success(_) =>
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey)
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelVersion.commitmentFormat)
val channelId = toLongId(fundingTxHash, fundingTxOutputIndex)
// watch the funding tx transaction
val commitInput = localCommitTx.input
Expand Down Expand Up @@ -477,7 +474,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, channelVersion, fundingCreated)) =>
// we make sure that their sig checks out and that our first commit tx is spendable
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey)
val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelVersion.commitmentFormat)
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
Transactions.checkSpendable(signedLocalCommitTx) match {
case Failure(cause) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction}
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions.{CommitTx, CommitmentFormat, DefaultCommitmentFormat}
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitTx, CommitmentFormat, DefaultCommitmentFormat}
import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
import scodec.bits.{BitVector, ByteVector}
Expand Down Expand Up @@ -257,7 +257,11 @@ case class ChannelVersion(bits: BitVector) {

require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes")

val commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
val commitmentFormat: CommitmentFormat = if (hasAnchorOutputs) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}

def |(other: ChannelVersion) = ChannelVersion(bits | other.bits)
def &(other: ChannelVersion) = ChannelVersion(bits & other.bits)
Expand All @@ -267,19 +271,33 @@ 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)
}

object ChannelVersion {
import scodec.bits._

val LENGTH_BITS: Int = 4 * 8

private val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0
private val USE_STATIC_REMOTEKEY_BIT = 1
private val USE_ANCHOR_OUTPUTS_BIT = 2

private def setBit(bit: Int) = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse)

def pickChannelVersion(localFeatures: Features, remoteFeatures: Features): ChannelVersion = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
ANCHOR_OUTPUTS
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
STATIC_REMOTEKEY
} else {
STANDARD
}
}

val ZEROES = ChannelVersion(bin"00000000000000000000000000000000")
val STANDARD = ZEROES | setBit(USE_PUBKEY_KEYPATH_BIT)
val STATIC_REMOTEKEY = STANDARD | setBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY
val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | setBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS
}
// @formatter:on
Original file line number Diff line number Diff line change
Expand Up @@ -466,23 +466,20 @@ object Commitments {
// remote commitment will includes all local changes + remote acked changes
val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed)
val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, channelVersion, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec)
val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath))
val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Remote, commitmentFormat)

val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index)
val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion)
val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint))
val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, commitmentFormat))

// NB: IN/OUT htlcs are inverted because this is the remote commit
log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.collect(outgoing).map(_.id).mkString(","), spec.htlcs.collect(incoming).map(_.id).mkString(","), remoteCommitTx.tx)
Metrics.recordHtlcsInFlight(spec, remoteCommit.spec)

// don't sign if they don't get paid
val commitSig = CommitSig(
channelId = commitments.channelId,
signature = sig,
htlcSignatures = htlcSigs.toList
)

htlcSignatures = htlcSigs.toList)
val commitments1 = commitments.copy(
remoteNextCommitInfo = Left(WaitingForRevocation(RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint), commitSig, commitments.localCommit.index)),
localChanges = localChanges.copy(proposed = Nil, signed = localChanges.proposed),
Expand Down Expand Up @@ -511,20 +508,14 @@ object Commitments {
log.warning("received a commit sig with no changes (probably coming from lnd)")
}

// check that their signature is valid
// signatures are now optional in the commit message, and will be sent only if the other party is actually
// receiving money i.e its commit tx has one output for them

val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed)
val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 1)
val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, channelVersion, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec)
val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath))
val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat)

log.info(s"built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.collect(incoming).map(_.id).mkString(","), spec.htlcs.collect(outgoing).map(_.id).mkString(","), localCommitTx.tx)

// TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty)

// no need to compute htlc sigs if commit sig doesn't check out
val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature)
if (Transactions.checkSpendable(signedCommitTx).isFailure) {
Expand All @@ -535,7 +526,7 @@ object Commitments {
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
throw HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)
}
val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint))
val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat))
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
// combine the sigs to make signed txes
val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect {
Expand All @@ -546,7 +537,8 @@ object Commitments {
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
case (htlcTx: HtlcSuccessTx, localSig, remoteSig) =>
// we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig
if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey)) {
// we verify the signature from their point of view, where it is a remote tx
if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, TxOwner.Remote, commitmentFormat)) {
throw InvalidHtlcSignature(commitments.channelId, htlcTx.tx)
}
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
Expand Down Expand Up @@ -705,4 +697,5 @@ object Commitments {
| htlcs:
|${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin
}

}
Loading

0 comments on commit 3d4e00f

Please sign in to comment.