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

Implement option_static_remotekey #1141

Merged
merged 15 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ class EclairImpl(appKit: Kit) extends Eclair {

override def newAddress(): Future[String] = {
appKit.wallet match {
case w: BitcoinCoreWallet => w.getFinalAddress
case w: BitcoinCoreWallet => w.getReceiveAddress
case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend"))
}
}
Expand Down
13 changes: 12 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ object Features {
val mandatory = 10
}

case object StaticRemoteKey extends Feature {
val rfcName = "option_static_remotekey"
val mandatory = 12
}

case object PaymentSecret extends Feature {
val rfcName = "payment_secret"
val mandatory = 14
Expand Down Expand Up @@ -196,7 +201,8 @@ object Features {
PaymentSecret,
BasicMultiPartPayment,
Wumbo,
TrampolinePayment
TrampolinePayment,
StaticRemoteKey
)

private val supportedMandatoryFeatures: Set[Feature] = Set(
Expand Down Expand Up @@ -237,4 +243,9 @@ object Features {
}
}

/** returns true if both have at least optional support */
def canUseFeature(localFeatures: Features, remoteFeatures: Features, feature: Feature): Boolean = {
localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature)
}

}
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ class Setup(datadir: File,
implicit val timeout = Timeout(30 seconds)
new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash)
}
_ = wallet.getFinalAddress.map(address => logger.info(s"initial wallet address=$address"))
_ = wallet.getReceiveAddress.map(address => logger.info(s"initial wallet address=$address"))
// do not change the name of this actor. it is used in the configuration to specify a custom bounded mailbox

backupHandler = if (config.getBoolean("enable-db-backup")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package fr.acinq.eclair.blockchain

import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Satoshi, Transaction}
import scodec.bits.ByteVector

Expand All @@ -28,7 +29,9 @@ trait EclairWallet {

def getBalance: Future[Satoshi]

def getFinalAddress: Future[String]
def getReceiveAddress: Future[String]

def getReceivePubkey(receiveAddress: Option[String] = None): Future[PublicKey]

def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package fr.acinq.eclair.blockchain.bitcoind

import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
Expand Down Expand Up @@ -91,10 +92,15 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC

override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDecimal(balance) => Satoshi(balance.bigDecimal.scaleByPowerOfTen(8).longValue) }

override def getFinalAddress: Future[String] = for {
override def getReceiveAddress: Future[String] = for {
JString(address) <- rpcClient.invoke("getnewaddress")
} yield address

override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = for {
address <- receiveAddress.map(Future.successful).getOrElse(getReceiveAddress)
JString(rawKey) <- rpcClient.invoke("getaddressinfo", address).map(_ \ "pubkey")
} yield PublicKey(ByteVector.fromValidHex(rawKey))

private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = {
val f = signTransaction(tx)
// if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.electrum

import akka.actor.{ActorRef, ActorSystem}
import akka.pattern.ask
import fr.acinq.bitcoin.{ByteVector32, Satoshi, Script, Transaction, TxOut}
import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi, Script, Transaction, TxOut}
import fr.acinq.eclair.addressToPublicKeyScript
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.BroadcastTransaction
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._
Expand All @@ -32,7 +32,9 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implic

override def getBalance = (wallet ? GetBalance).mapTo[GetBalanceResponse].map(balance => balance.confirmed + balance.unconfirmed)

override def getFinalAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address)
override def getReceiveAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address)

override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.failed(new RuntimeException("Not implemented"))
t-bast marked this conversation as resolved.
Show resolved Hide resolved

def getXpub: Future[GetXpubResponse] = (wallet ? GetXpub).mapTo[GetXpubResponse]

Expand Down
34 changes: 25 additions & 9 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,6 +21,7 @@ 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 @@ -34,6 +35,7 @@ import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
import scodec.bits.ByteVector
import ChannelVersion._

import scala.collection.immutable.Queue
import scala.compat.Platform
Expand Down Expand Up @@ -183,7 +185,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
maxAcceptedHtlcs = localParams.maxAcceptedHtlcs,
fundingPubkey = fundingPubKey,
revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey,
paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey,
paymentBasepoint = channelVersion match {
case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get
case _ => keyManager.paymentPoint(channelKeyPath).publicKey
},
sstone marked this conversation as resolved.
Show resolved Hide resolved
delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey,
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
Expand Down Expand Up @@ -297,7 +302,10 @@ 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 = ChannelVersion.STANDARD
val channelVersion = Features.canUseFeature(localParams.features, remoteInit.features, StaticRemoteKey) match {
case false => ChannelVersion.STANDARD
case true => ChannelVersion.STATIC_REMOTEKEY
}
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
// TODO: maybe also check uniqueness of temporary channel id
val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis)
Expand All @@ -311,7 +319,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
maxAcceptedHtlcs = localParams.maxAcceptedHtlcs,
fundingPubkey = fundingPubkey,
revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey,
paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey,
paymentBasepoint = channelVersion match {
case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get
case _ => keyManager.paymentPoint(channelKeyPath).publicKey
},
sstone marked this conversation as resolved.
Show resolved Hide resolved
delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey,
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
Expand Down Expand Up @@ -2093,12 +2104,17 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId

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}")
// if we are in this state, then this field is defined
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))
d.commitments.channelVersion match {
case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) =>
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))
}
}

def handleRemoteSpentNext(commitTx: Transaction, d: HasCommitments) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,7 @@ final case class DATA_CLOSING(commitments: Commitments,
nextRemoteCommitPublished: Option[RemoteCommitPublished] = None,
futureRemoteCommitPublished: Option[RemoteCommitPublished] = None,
revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends Data with HasCommitments {
val spendingTxes = mutualClosePublished ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx)
require(spendingTxes.nonEmpty, "there must be at least one tx published in this state")
def spendingTxes = mutualClosePublished ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx)
}

final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments
Expand All @@ -230,6 +229,7 @@ final case class LocalParams(nodeId: PublicKey,
maxAcceptedHtlcs: Int,
isFunder: Boolean,
defaultFinalScriptPubKey: ByteVector,
localPaymentBasepoint: Option[PublicKey],
sstone marked this conversation as resolved.
Show resolved Hide resolved
features: Features)

final case class RemoteParams(nodeId: PublicKey,
Expand Down Expand Up @@ -265,11 +265,14 @@ object ChannelVersion {
val LENGTH_BITS = 4 * 8
val ZEROES = ChannelVersion(bin"00000000000000000000000000000000")
val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0
val USE_STATIC_REMOTEKEY_BIT = 1

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

val USE_PUBKEY_KEYPATH = fromBit(USE_PUBKEY_KEYPATH_BIT)
val USE_STATIC_REMOTEKEY = fromBit(USE_STATIC_REMOTEKEY_BIT)

val STANDARD = ZEROES | USE_PUBKEY_KEYPATH
val STATIC_REMOTEKEY = STANDARD | USE_STATIC_REMOTEKEY // USE_PUBKEY_KEYPATH + USE_STATIC_REMOTEKEY
}
// @formatter:on
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package fr.acinq.eclair.channel

import akka.event.LoggingAdapter
import fr.acinq.eclair.channel.ChannelVersion._
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256}
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets}
Expand Down Expand Up @@ -62,6 +63,8 @@ case class Commitments(channelVersion: ChannelVersion,
commitInput: InputInfo,
remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) {

require(!channelVersion.isSet(USE_STATIC_REMOTEKEY_BIT) || (channelVersion.isSet(USE_STATIC_REMOTEKEY_BIT) && localParams.localPaymentBasepoint.isDefined), s"localParams.localPaymentBasepoint must be defined for commitments with version=$channelVersion")

def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight

def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = {
Expand Down Expand Up @@ -598,11 +601,18 @@ object Commitments {
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint)
val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint)
val remotePaymentPubkey = channelVersion match {
case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => remoteParams.paymentBasepoint
case _ => Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint)
}
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val localPaymentBasepoint = channelVersion match {
case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get
case _ => keyManager.paymentPoint(channelKeyPath).publicKey
}
val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, outputs)
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)
(commitTx, htlcTimeoutTxs, htlcSuccessTxs)
}
Expand All @@ -614,13 +624,20 @@ object Commitments {
remotePerCommitmentPoint: PublicKey,
spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
val localPaymentBasepoint = channelVersion match {
case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get
case _ => keyManager.paymentPoint(channelKeyPath).publicKey
}
val localPaymentPubkey = channelVersion match {
case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localPaymentBasepoint
case _ => Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint)
}
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey, !localParams.isFunder, outputs)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isFunder, outputs)
val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feeratePerKw, outputs)
(commitTx, htlcTimeoutTxs, htlcSuccessTxs)
}
Expand Down
Loading