diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index a8140190fa..ceb96c1654 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -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 @@ -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]) @@ -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 { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala index 7b7a318496..ff6fe28aaa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala @@ -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)), @@ -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) 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) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala index a8449632d1..a4806d529b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala @@ -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 => @@ -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 } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 688ead9dea..df97be001b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -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._ @@ -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) }