Skip to content
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
15 changes: 3 additions & 12 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import akka.actor.{ActorRef, typed}
import akka.pattern._
import akka.util.Timeout
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.Musig2.{IndividualNonce, LocalNonce}
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, Script, Transaction, TxId, addressToPublicKeyScript}
import fr.acinq.eclair.ApiTypes.ChannelNotFound
import fr.acinq.eclair.balance.CheckBalance.GlobalBalance
Expand Down Expand Up @@ -71,11 +71,8 @@ case class VerifiedMessage(valid: Boolean, publicKey: PublicKey)
case class SendOnionMessageResponsePayload(tlvs: TlvStream[OnionMessagePayloadTlv])
case class SendOnionMessageResponse(sent: Boolean, failureMessage: Option[String], response: Option[SendOnionMessageResponsePayload])

case class SpendFromChannelPrep(fundingTxIndex: Long, localFundingPubkey: PublicKey, inputAmount: Satoshi, unsignedTx: Transaction)
case class SpendFromChannelPrep(fundingTxIndex: Long, localFundingPubkey: PublicKey, localNonce_opt: Option[IndividualNonce], inputAmount: Satoshi, unsignedTx: Transaction)
case class SpendFromChannelResult(signedTx: Transaction)

case class SpendFromTaprootChannelPrep(fundingTxIndex: Long, localFundingPubkey: PublicKey, inputAmount: Satoshi, unsignedTx: Transaction, nonce: LocalNonce)
case class SpendFromTaprootChannelPartialSign(localFundingPubkey: PublicKey, partialSignatureWithNonce: PartialSignatureWithNonce)
// @formatter:on

case class EnableFromFutureHtlcResponse(enabled: Boolean, failureMessage: Option[String])
Expand Down Expand Up @@ -217,13 +214,7 @@ trait Eclair {

def spendFromChannelAddressPrep(outPoint: OutPoint, fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, address: String, feerate: FeeratePerKw): Future[SpendFromChannelPrep]

def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, remoteSig: ByteVector64, unsignedTx: Transaction): Future[SpendFromChannelResult]

def spendFromTaprootChannelAddressPrep(outPoint: OutPoint, fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, address: String, feerate: FeeratePerKw, randomSessionId: ByteVector32): Future[SpendFromTaprootChannelPrep]

def spendFromTaprootChannelAddressPartialSign(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, unsignedTx: Transaction, amount: Satoshi, remoteFundingPubkey: PublicKey, remoteNonce: IndividualNonce): Future[SpendFromTaprootChannelPartialSign]

def spendFromTaprootChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, randomSessionId: ByteVector32, remotePartialSig: PartialSignatureWithNonce, unsignedTx: Transaction): Future[SpendFromChannelResult]
def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, localNonce_opt: Option[IndividualNonce], remoteSig: ChannelSpendSignature, unsignedTx: Transaction): Future[SpendFromChannelResult]
}

class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChannelAddress {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ package fr.acinq.eclair

import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.Musig2.{IndividualNonce, LocalNonce}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, DeterministicWallet, Musig2, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, addressToPublicKeyScript}
import fr.acinq.bitcoin.scalacompat.{DeterministicWallet, Musig2, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, addressToPublicKeyScript}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
import fr.acinq.eclair.channel.{ChannelConfig, ChannelSpendSignature}
import fr.acinq.eclair.transactions.Scripts.Taproot
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import scodec.bits.ByteVector

import scala.concurrent.Future
import scala.jdk.CollectionConverters.ConcurrentMapHasAsScala

trait SpendFromChannelAddress {

this: EclairImpl =>

import java.util.concurrent.ConcurrentHashMap
private val nonces = new ConcurrentHashMap[IndividualNonce, LocalNonce]().asScala

private def buildTx(outPoint: OutPoint, outputAmount: Satoshi, pubKeyScript: ByteVector, witness: ScriptWitness) = Transaction(2,
txIn = Seq(TxIn(outPoint, ByteVector.empty, 0, witness)),
txOut = Seq(TxOut(outputAmount, pubKeyScript)),
Expand All @@ -29,71 +31,42 @@ trait SpendFromChannelAddress {
Right(pubKeyScript) = addressToPublicKeyScript(appKit.nodeParams.chainHash, address).map(Script.write)
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
localFundingPubkey = channelKeys.fundingKey(fundingTxIndex).publicKey
isTaproot = Script.isPay2tr(Script.parse(pubKeyScript))
localNonce_opt = if (isTaproot) {
val serverNonce = Musig2.generateNonce(randomBytes32(), Right(localFundingPubkey), Seq(localFundingPubkey), None, None)
nonces.put(serverNonce.publicNonce, serverNonce)
Some(serverNonce.publicNonce)
} else {
None
}
// build the tx a first time with a zero amount to compute the weight
dummyWitness = Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, localFundingPubkey, localFundingPubkey)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dummyWitness should be Script.witnessKeyPathPay2tr(PlaceHolderSig) for taproot channels

fee = Transactions.weight2fee(feerate, buildTx(outPoint, 0.sat, pubKeyScript, dummyWitness).weight())
_ = assert(inputAmount - fee > Scripts.dustLimit(pubKeyScript), s"amount insufficient (fee=$fee)")
unsignedTx = buildTx(outPoint, inputAmount - fee, pubKeyScript, dummyWitness)
} yield SpendFromChannelPrep(fundingTxIndex, localFundingPubkey, inputAmount, unsignedTx)
} yield SpendFromChannelPrep(fundingTxIndex, localFundingPubkey, localNonce_opt, inputAmount, unsignedTx)
}

override def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, remoteSig: ByteVector64, unsignedTx: Transaction): Future[SpendFromChannelResult] = {
override def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, localNonce_opt: Option[IndividualNonce], remoteSig: ChannelSpendSignature, unsignedTx: Transaction): Future[SpendFromChannelResult] = {
for {
_ <- Future.successful(())
outPoint = unsignedTx.txIn.head.outPoint
inputTx <- appKit.wallet.getTransaction(outPoint.txid)
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt))
// classify as splice, doesn't really matter
tx = Transactions.SpliceTx(inputInfo, unsignedTx)
localSig = tx.sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
signedTx = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig))
} yield SpendFromChannelResult(signedTx)
}

override def spendFromTaprootChannelAddressPrep(outPoint: OutPoint, fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, address: String, feerate: FeeratePerKw, sessionId: ByteVector32): Future[SpendFromTaprootChannelPrep] = {
for {
inputTx <- appKit.wallet.getTransaction(outPoint.txid)
inputAmount = inputTx.txOut(outPoint.index.toInt).amount
Right(pubKeyScript) = addressToPublicKeyScript(appKit.nodeParams.chainHash, address).map(Script.write)
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
localFundingPubkey = channelKeys.fundingKey(fundingTxIndex).publicKey
// build the tx a first time with a zero amount to compute the weight
dummyWitness = Script.witnessKeyPathPay2tr(PlaceHolderSig)
fee = Transactions.weight2fee(feerate, buildTx(outPoint, 0.sat, pubKeyScript, dummyWitness).weight())
_ = assert(inputAmount - fee > Scripts.dustLimit(pubKeyScript), s"amount insufficient (fee=$fee)")
unsignedTx = buildTx(outPoint, inputAmount - fee, pubKeyScript, dummyWitness)
nonce = Musig2.generateNonce(sessionId, Right(localFundingPubkey), Seq(localFundingPubkey), None, None)
} yield SpendFromTaprootChannelPrep(fundingTxIndex, localFundingPubkey, inputAmount, unsignedTx, nonce)
}

override def spendFromTaprootChannelAddressPartialSign(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, unsignedTx: Transaction, amount: Satoshi, remoteFundingPubkey: PublicKey, remoteNonce: IndividualNonce): Future[SpendFromTaprootChannelPartialSign] = {
for {
_ <- Future.successful(())
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
outputScript = Script.pay2tr(Taproot.musig2Aggregate(localFundingKey.publicKey, remoteFundingPubkey), None)
inputInfo = InputInfo(unsignedTx.txIn(0).outPoint, TxOut(amount, outputScript))
tx = Transactions.SpliceTx(inputInfo, unsignedTx)
localNonce = Musig2.generateNonce(randomBytes32(), Left(localFundingKey), Seq(localFundingKey.publicKey), None, None)
Right(localSig) = tx.partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce = localNonce, publicNonces = Seq(localNonce.publicNonce, remoteNonce))
} yield SpendFromTaprootChannelPartialSign(localFundingKey.publicKey, localSig)
}

override def spendFromTaprootChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, sessionId: ByteVector32, remotePartialSig: PartialSignatureWithNonce, unsignedTx: Transaction): Future[SpendFromChannelResult] = {
for {
_ <- Future.successful(())
outPoint = unsignedTx.txIn.head.outPoint
inputTx <- appKit.wallet.getTransaction(outPoint.txid)
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt))
// classify as splice, doesn't really matter
tx = Transactions.SpliceTx(inputInfo, unsignedTx)
localNonce = Musig2.generateNonce(sessionId, Right(localFundingKey.publicKey), Seq(localFundingKey.publicKey), None, None)
Right(localSig) = tx.partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce = localNonce, publicNonces = Seq(localNonce.publicNonce, remotePartialSig.nonce))
Right(signedTx) = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, remotePartialSig, extraUtxos = Map.empty)
signedTx = remoteSig match {
case individualRemoteSig: ChannelSpendSignature.IndividualSignature =>
val localSig = tx.sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, individualRemoteSig)
case remotePartialSig: ChannelSpendSignature.PartialSignatureWithNonce =>
val localPrivateNonce = nonces(localNonce_opt.get)
val Right(localSig) = tx.partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce = localPrivateNonce, publicNonces = Seq(localPrivateNonce.publicNonce, remotePartialSig.nonce))
val Right(signedTx) = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, remotePartialSig, extraUtxos = Map.empty)
signedTx
}
} yield SpendFromChannelResult(signedTx)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import akka.http.scaladsl.server.Route
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction, TxId}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Transaction, TxId}
import fr.acinq.eclair.api.Service
import fr.acinq.eclair.api.directives.EclairDirectives
import fr.acinq.eclair.api.serde.FormParamExtractors._
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw}
import fr.acinq.eclair.blockchain.fee.FeeratePerByte
import fr.acinq.eclair.channel.ChannelSpendSignature
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
import scodec.bits.ByteVector

trait Control {
this: Service with EclairDirectives =>
Expand Down Expand Up @@ -67,31 +67,17 @@ trait Control {
val spendFromChannelAddress: Route = postRequest("spendfromchanneladdress") { implicit t =>
formFields("kp", "fi".as[Int], "p".as[PublicKey], "s".as[ByteVector64], "tx") {
(keyPath, fundingTxIndex, remoteFundingPubkey, remoteSig, unsignedTx) =>
complete(eclairApi.spendFromChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, remoteSig, Transaction.read(unsignedTx)))
}
}

val spendFromTaprootChannelAddressPrep: Route = postRequest("spendfromtaprootchanneladdressprep") { implicit t =>
formFields("t".as[ByteVector32], "o".as[Int], "kp", "fi".as[Int], "address", "f".as[FeeratePerByte], "randomSessionId".as[ByteVector32]) {
(txId, outputIndex, keyPath, fundingTxIndex, address, feerate, sessionId) =>
complete(eclairApi.spendFromTaprootChannelAddressPrep(OutPoint(TxId(txId), outputIndex), KeyPath(keyPath), fundingTxIndex, address, feerate.perKw, sessionId))
}
}

val spendFromTaprootChannelAddressPartialSign: Route = postRequest("spendfromtaprootchanneladdresspartialsign") { implicit t =>
formFields("kp", "fi".as[Int], "tx", "amount".as[Satoshi], "remoteFundingPubkey".as[PublicKey], "remoteNonce".as[String]) {
(keyPath, fundingTxIndex, tx, amount, remoteFundingPubkey, remoteNonce) =>
complete(eclairApi.spendFromTaprootChannelAddressPartialSign(KeyPath(keyPath), fundingTxIndex, Transaction.read(tx), amount, remoteFundingPubkey, IndividualNonce(ByteVector.fromValidHex(remoteNonce))))
complete(eclairApi.spendFromChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, localNonce_opt = None, ChannelSpendSignature.IndividualSignature(remoteSig), Transaction.read(unsignedTx)))
}
}

val spendFromTaprootChannelAddress: Route = postRequest("spendfromtaprootchanneladdress") { implicit t =>
formFields("kp", "fi".as[Int], "p".as[PublicKey], "sessionId".as[ByteVector32], "remoteSig".as[ByteVector32], "remoteNonce".as[String], "tx".as[String]) {
(keyPath, fundingTxIndex, remoteFundingPubkey, sessionId, remoteSig, remoteNonce, unsignedTx) =>
complete(eclairApi.spendFromTaprootChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, sessionId, PartialSignatureWithNonce(remoteSig, IndividualNonce(ByteVector.fromValidHex(remoteNonce))), Transaction.read(unsignedTx)))
formFields("kp", "fi".as[Int], "p".as[PublicKey], "localNonce".as[IndividualNonce], "remoteNonce".as[IndividualNonce], "remoteSig".as[ByteVector32], "tx".as[String]) {
(keyPath, fundingTxIndex, remoteFundingPubkey, localNonce, remoteNonce, remoteSig, unsignedTx) =>
complete(eclairApi.spendFromChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, localNonce_opt = Some(localNonce), PartialSignatureWithNonce(remoteSig, remoteNonce), Transaction.read(unsignedTx)))
}
}

val controlRoutes: Route = enableFromFutureHtlc ~ resetBalance ~ forceCloseResetFundingIndex ~ manualWatchFundingSpent ~ spendFromChannelAddressPrep ~ spendFromChannelAddress ~ spendFromTaprootChannelAddressPrep ~ spendFromTaprootChannelAddressPartialSign ~ spendFromTaprootChannelAddress
val controlRoutes: Route = enableFromFutureHtlc ~ resetBalance ~ forceCloseResetFundingIndex ~ manualWatchFundingSpent ~ spendFromChannelAddressPrep ~ spendFromChannelAddress ~ spendFromTaprootChannelAddress

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package fr.acinq.eclair.api.serde
import akka.http.scaladsl.unmarshalling.Unmarshaller
import akka.util.Timeout
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Satoshi, TxId}
import fr.acinq.eclair.api.directives.RouteFormat
import fr.acinq.eclair.api.serde.JsonSupport._
Expand All @@ -39,6 +40,8 @@ object FormParamExtractors {

implicit val publicKeyUnmarshaller: Unmarshaller[String, PublicKey] = Unmarshaller.strict { rawPubKey => PublicKey(ByteVector.fromValidHex(rawPubKey)) }

implicit val individualNonceUnmarshaller: Unmarshaller[String, IndividualNonce] = Unmarshaller.strict { str => IndividualNonce(ByteVector.fromValidHex(str)) }

implicit val bytesUnmarshaller: Unmarshaller[String, ByteVector] = Unmarshaller.strict { str => ByteVector.fromValidHex(str) }

implicit val bytes32Unmarshaller: Unmarshaller[String, ByteVector32] = Unmarshaller.strict { bin => ByteVector32.fromValidHex(bin) }
Expand Down