Skip to content

Commit 38dc407

Browse files
sstonepm47
andauthored
Add API methods to spend funds sent to taproot channel addresses (#3220)
Add API methods to spend funds sent to taproot channel addresses This PR adds new API calls that extend spendFromChannelAddress* calls to taproot channels. --------- Co-authored-by: pm47 <[email protected]>
1 parent e1775ee commit 38dc407

File tree

4 files changed

+48
-14
lines changed

4 files changed

+48
-14
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import akka.actor.{ActorRef, typed}
2323
import akka.pattern._
2424
import akka.util.Timeout
2525
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
26+
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
2627
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, Script, Transaction, TxId, addressToPublicKeyScript}
2728
import fr.acinq.eclair.ApiTypes.ChannelNotFound
2829
import fr.acinq.eclair.balance.CheckBalance.GlobalBalance
@@ -32,6 +33,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered
3233
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient
3334
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{AddressType, Descriptors, WalletTx}
3435
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerByte, FeeratePerKw}
36+
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
3537
import fr.acinq.eclair.channel._
3638
import fr.acinq.eclair.crypto.Sphinx
3739
import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats}
@@ -69,7 +71,7 @@ case class VerifiedMessage(valid: Boolean, publicKey: PublicKey)
6971
case class SendOnionMessageResponsePayload(tlvs: TlvStream[OnionMessagePayloadTlv])
7072
case class SendOnionMessageResponse(sent: Boolean, failureMessage: Option[String], response: Option[SendOnionMessageResponsePayload])
7173

72-
case class SpendFromChannelPrep(fundingTxIndex: Long, localFundingPubkey: PublicKey, inputAmount: Satoshi, unsignedTx: Transaction)
74+
case class SpendFromChannelPrep(fundingTxIndex: Long, localFundingPubkey: PublicKey, localNonce_opt: Option[IndividualNonce], inputAmount: Satoshi, unsignedTx: Transaction)
7375
case class SpendFromChannelResult(signedTx: Transaction)
7476
// @formatter:on
7577

@@ -212,7 +214,7 @@ trait Eclair {
212214

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

215-
def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, remoteSig: ByteVector64, unsignedTx: Transaction): Future[SpendFromChannelResult]
217+
def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, localNonce_opt: Option[IndividualNonce], remoteSig: ChannelSpendSignature, unsignedTx: Transaction): Future[SpendFromChannelResult]
216218
}
217219

218220
class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChannelAddress {
Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
package fr.acinq.eclair
22

33
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
4-
import fr.acinq.bitcoin.scalacompat.{ByteVector64, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, addressToPublicKeyScript}
4+
import fr.acinq.bitcoin.scalacompat.Musig2.{IndividualNonce, LocalNonce}
5+
import fr.acinq.bitcoin.scalacompat.{DeterministicWallet, Musig2, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, addressToPublicKeyScript}
56
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
67
import fr.acinq.eclair.channel.{ChannelConfig, ChannelSpendSignature}
78
import fr.acinq.eclair.transactions.Transactions._
89
import fr.acinq.eclair.transactions.{Scripts, Transactions}
910
import scodec.bits.ByteVector
1011

1112
import scala.concurrent.Future
13+
import scala.jdk.CollectionConverters.ConcurrentMapHasAsScala
1214

1315
trait SpendFromChannelAddress {
1416

1517
this: EclairImpl =>
1618

19+
import java.util.concurrent.ConcurrentHashMap
20+
private val nonces = new ConcurrentHashMap[IndividualNonce, LocalNonce]().asScala
21+
1722
private def buildTx(outPoint: OutPoint, outputAmount: Satoshi, pubKeyScript: ByteVector, witness: ScriptWitness) = Transaction(2,
1823
txIn = Seq(TxIn(outPoint, ByteVector.empty, 0, witness)),
1924
txOut = Seq(TxOut(outputAmount, pubKeyScript)),
@@ -26,27 +31,41 @@ trait SpendFromChannelAddress {
2631
Right(pubKeyScript) = addressToPublicKeyScript(appKit.nodeParams.chainHash, address).map(Script.write)
2732
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
2833
localFundingPubkey = channelKeys.fundingKey(fundingTxIndex).publicKey
34+
isTaproot = Script.isPay2tr(Script.parse(pubKeyScript))
35+
(localNonce_opt, dummyWitness) = if (isTaproot) {
36+
val serverNonce = Musig2.generateNonce(randomBytes32(), Right(localFundingPubkey), Seq(localFundingPubkey), None, None)
37+
nonces.put(serverNonce.publicNonce, serverNonce)
38+
Some(serverNonce.publicNonce) -> Script.witnessKeyPathPay2tr(PlaceHolderSig)
39+
} else {
40+
None -> Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, localFundingPubkey, localFundingPubkey)
41+
}
2942
// build the tx a first time with a zero amount to compute the weight
30-
dummyWitness = Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, localFundingPubkey, localFundingPubkey)
3143
fee = Transactions.weight2fee(feerate, buildTx(outPoint, 0.sat, pubKeyScript, dummyWitness).weight())
3244
_ = assert(inputAmount - fee > Scripts.dustLimit(pubKeyScript), s"amount insufficient (fee=$fee)")
3345
unsignedTx = buildTx(outPoint, inputAmount - fee, pubKeyScript, dummyWitness)
34-
} yield SpendFromChannelPrep(fundingTxIndex, localFundingPubkey, inputAmount, unsignedTx)
46+
} yield SpendFromChannelPrep(fundingTxIndex, localFundingPubkey, localNonce_opt, inputAmount, unsignedTx)
3547
}
3648

37-
override def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, remoteSig: ByteVector64, unsignedTx: Transaction): Future[SpendFromChannelResult] = {
49+
override def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, localNonce_opt: Option[IndividualNonce], remoteSig: ChannelSpendSignature, unsignedTx: Transaction): Future[SpendFromChannelResult] = {
3850
for {
3951
_ <- Future.successful(())
4052
outPoint = unsignedTx.txIn.head.outPoint
4153
inputTx <- appKit.wallet.getTransaction(outPoint.txid)
42-
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
43-
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
4454
inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt))
4555
// classify as splice, doesn't really matter
4656
tx = Transactions.SpliceTx(inputInfo, unsignedTx)
47-
localSig = tx.sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
48-
signedTx = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig))
57+
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
58+
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
59+
signedTx = remoteSig match {
60+
case individualRemoteSig: ChannelSpendSignature.IndividualSignature =>
61+
val localSig = tx.sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
62+
tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, individualRemoteSig)
63+
case remotePartialSig: ChannelSpendSignature.PartialSignatureWithNonce =>
64+
val localPrivateNonce = nonces(localNonce_opt.get)
65+
val Right(localSig) = tx.partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce = localPrivateNonce, publicNonces = Seq(localPrivateNonce.publicNonce, remotePartialSig.nonce))
66+
val Right(signedTx) = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, remotePartialSig, extraUtxos = Map.empty)
67+
signedTx
68+
}
4969
} yield SpendFromChannelResult(signedTx)
5070
}
51-
5271
}

eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ package fr.acinq.eclair.api.handlers
1919
import akka.http.scaladsl.server.Route
2020
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2121
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
22+
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
2223
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Transaction, TxId}
2324
import fr.acinq.eclair.api.Service
2425
import fr.acinq.eclair.api.directives.EclairDirectives
2526
import fr.acinq.eclair.api.serde.FormParamExtractors._
26-
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw}
27+
import fr.acinq.eclair.blockchain.fee.FeeratePerByte
28+
import fr.acinq.eclair.channel.ChannelSpendSignature
29+
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
2730

2831
trait Control {
2932
this: Service with EclairDirectives =>
@@ -64,10 +67,17 @@ trait Control {
6467
val spendFromChannelAddress: Route = postRequest("spendfromchanneladdress") { implicit t =>
6568
formFields("kp", "fi".as[Int], "p".as[PublicKey], "s".as[ByteVector64], "tx") {
6669
(keyPath, fundingTxIndex, remoteFundingPubkey, remoteSig, unsignedTx) =>
67-
complete(eclairApi.spendFromChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, remoteSig, Transaction.read(unsignedTx)))
70+
complete(eclairApi.spendFromChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, localNonce_opt = None, ChannelSpendSignature.IndividualSignature(remoteSig), Transaction.read(unsignedTx)))
6871
}
6972
}
7073

71-
val controlRoutes: Route = enableFromFutureHtlc ~ resetBalance ~ forceCloseResetFundingIndex ~ manualWatchFundingSpent ~ spendFromChannelAddressPrep ~ spendFromChannelAddress
74+
val spendFromTaprootChannelAddress: Route = postRequest("spendfromtaprootchanneladdress") { implicit t =>
75+
formFields("kp", "fi".as[Int], "p".as[PublicKey], "localNonce".as[IndividualNonce], "remoteNonce".as[IndividualNonce], "remoteSig".as[ByteVector32], "tx".as[String]) {
76+
(keyPath, fundingTxIndex, remoteFundingPubkey, localNonce, remoteNonce, remoteSig, unsignedTx) =>
77+
complete(eclairApi.spendFromChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, localNonce_opt = Some(localNonce), PartialSignatureWithNonce(remoteSig, remoteNonce), Transaction.read(unsignedTx)))
78+
}
79+
}
80+
81+
val controlRoutes: Route = enableFromFutureHtlc ~ resetBalance ~ forceCloseResetFundingIndex ~ manualWatchFundingSpent ~ spendFromChannelAddressPrep ~ spendFromChannelAddress ~ spendFromTaprootChannelAddress
7282

7383
}

eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package fr.acinq.eclair.api.serde
1919
import akka.http.scaladsl.unmarshalling.Unmarshaller
2020
import akka.util.Timeout
2121
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
22+
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
2223
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Satoshi, TxId}
2324
import fr.acinq.eclair.api.directives.RouteFormat
2425
import fr.acinq.eclair.api.serde.JsonSupport._
@@ -39,6 +40,8 @@ object FormParamExtractors {
3940

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

43+
implicit val individualNonceUnmarshaller: Unmarshaller[String, IndividualNonce] = Unmarshaller.strict { str => IndividualNonce(ByteVector.fromValidHex(str)) }
44+
4245
implicit val bytesUnmarshaller: Unmarshaller[String, ByteVector] = Unmarshaller.strict { str => ByteVector.fromValidHex(str) }
4346

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

0 commit comments

Comments
 (0)