Skip to content

Commit 9de26bb

Browse files
committed
factor p2wsh/p2tr spendfromchanneladdress
1 parent 6fb61ce commit 9de26bb

File tree

4 files changed

+39
-86
lines changed

4 files changed

+39
-86
lines changed

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +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, LocalNonce}
26+
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
2727
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, Script, Transaction, TxId, addressToPublicKeyScript}
2828
import fr.acinq.eclair.ApiTypes.ChannelNotFound
2929
import fr.acinq.eclair.balance.CheckBalance.GlobalBalance
@@ -71,11 +71,8 @@ case class VerifiedMessage(valid: Boolean, publicKey: PublicKey)
7171
case class SendOnionMessageResponsePayload(tlvs: TlvStream[OnionMessagePayloadTlv])
7272
case class SendOnionMessageResponse(sent: Boolean, failureMessage: Option[String], response: Option[SendOnionMessageResponsePayload])
7373

74-
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)
7575
case class SpendFromChannelResult(signedTx: Transaction)
76-
77-
case class SpendFromTaprootChannelPrep(fundingTxIndex: Long, localFundingPubkey: PublicKey, inputAmount: Satoshi, unsignedTx: Transaction, nonce: LocalNonce)
78-
case class SpendFromTaprootChannelPartialSign(localFundingPubkey: PublicKey, partialSignatureWithNonce: PartialSignatureWithNonce)
7976
// @formatter:on
8077

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

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

220-
def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, remoteSig: ByteVector64, unsignedTx: Transaction): Future[SpendFromChannelResult]
221-
222-
def spendFromTaprootChannelAddressPrep(outPoint: OutPoint, fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, address: String, feerate: FeeratePerKw, randomSessionId: ByteVector32): Future[SpendFromTaprootChannelPrep]
223-
224-
def spendFromTaprootChannelAddressPartialSign(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, unsignedTx: Transaction, amount: Satoshi, remoteFundingPubkey: PublicKey, remoteNonce: IndividualNonce): Future[SpendFromTaprootChannelPartialSign]
225-
226-
def spendFromTaprootChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, randomSessionId: ByteVector32, remotePartialSig: PartialSignatureWithNonce, unsignedTx: Transaction): Future[SpendFromChannelResult]
217+
def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, localNonce_opt: Option[IndividualNonce], remoteSig: ChannelSpendSignature, unsignedTx: Transaction): Future[SpendFromChannelResult]
227218
}
228219

229220
class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChannelAddress {

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

Lines changed: 25 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@ package fr.acinq.eclair
22

33
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
44
import fr.acinq.bitcoin.scalacompat.Musig2.{IndividualNonce, LocalNonce}
5-
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, DeterministicWallet, Musig2, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, addressToPublicKeyScript}
5+
import fr.acinq.bitcoin.scalacompat.{DeterministicWallet, Musig2, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, addressToPublicKeyScript}
66
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
7-
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
87
import fr.acinq.eclair.channel.{ChannelConfig, ChannelSpendSignature}
9-
import fr.acinq.eclair.transactions.Scripts.Taproot
108
import fr.acinq.eclair.transactions.Transactions._
119
import fr.acinq.eclair.transactions.{Scripts, Transactions}
1210
import scodec.bits.ByteVector
1311

1412
import scala.concurrent.Future
13+
import scala.jdk.CollectionConverters.ConcurrentMapHasAsScala
1514

1615
trait SpendFromChannelAddress {
1716

1817
this: EclairImpl =>
1918

19+
import java.util.concurrent.ConcurrentHashMap
20+
private val nonces = new ConcurrentHashMap[IndividualNonce, LocalNonce]().asScala
21+
2022
private def buildTx(outPoint: OutPoint, outputAmount: Satoshi, pubKeyScript: ByteVector, witness: ScriptWitness) = Transaction(2,
2123
txIn = Seq(TxIn(outPoint, ByteVector.empty, 0, witness)),
2224
txOut = Seq(TxOut(outputAmount, pubKeyScript)),
@@ -29,71 +31,42 @@ trait SpendFromChannelAddress {
2931
Right(pubKeyScript) = addressToPublicKeyScript(appKit.nodeParams.chainHash, address).map(Script.write)
3032
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
3133
localFundingPubkey = channelKeys.fundingKey(fundingTxIndex).publicKey
34+
isTaproot = Script.isPay2tr(Script.parse(pubKeyScript))
35+
localNonce_opt = if (isTaproot) {
36+
val serverNonce = Musig2.generateNonce(randomBytes32(), Right(localFundingPubkey), Seq(localFundingPubkey), None, None)
37+
nonces.put(serverNonce.publicNonce, serverNonce)
38+
Some(serverNonce.publicNonce)
39+
} else {
40+
None
41+
}
3242
// build the tx a first time with a zero amount to compute the weight
3343
dummyWitness = Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, localFundingPubkey, localFundingPubkey)
3444
fee = Transactions.weight2fee(feerate, buildTx(outPoint, 0.sat, pubKeyScript, dummyWitness).weight())
3545
_ = assert(inputAmount - fee > Scripts.dustLimit(pubKeyScript), s"amount insufficient (fee=$fee)")
3646
unsignedTx = buildTx(outPoint, inputAmount - fee, pubKeyScript, dummyWitness)
37-
} yield SpendFromChannelPrep(fundingTxIndex, localFundingPubkey, inputAmount, unsignedTx)
47+
} yield SpendFromChannelPrep(fundingTxIndex, localFundingPubkey, localNonce_opt, inputAmount, unsignedTx)
3848
}
3949

40-
override def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, remoteSig: ByteVector64, unsignedTx: Transaction): Future[SpendFromChannelResult] = {
50+
override def spendFromChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, localNonce_opt: Option[IndividualNonce], remoteSig: ChannelSpendSignature, unsignedTx: Transaction): Future[SpendFromChannelResult] = {
4151
for {
4252
_ <- Future.successful(())
4353
outPoint = unsignedTx.txIn.head.outPoint
4454
inputTx <- appKit.wallet.getTransaction(outPoint.txid)
45-
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
46-
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
4755
inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt))
4856
// classify as splice, doesn't really matter
4957
tx = Transactions.SpliceTx(inputInfo, unsignedTx)
50-
localSig = tx.sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
51-
signedTx = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig))
52-
} yield SpendFromChannelResult(signedTx)
53-
}
54-
55-
override def spendFromTaprootChannelAddressPrep(outPoint: OutPoint, fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, address: String, feerate: FeeratePerKw, sessionId: ByteVector32): Future[SpendFromTaprootChannelPrep] = {
56-
for {
57-
inputTx <- appKit.wallet.getTransaction(outPoint.txid)
58-
inputAmount = inputTx.txOut(outPoint.index.toInt).amount
59-
Right(pubKeyScript) = addressToPublicKeyScript(appKit.nodeParams.chainHash, address).map(Script.write)
60-
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
61-
localFundingPubkey = channelKeys.fundingKey(fundingTxIndex).publicKey
62-
// build the tx a first time with a zero amount to compute the weight
63-
dummyWitness = Script.witnessKeyPathPay2tr(PlaceHolderSig)
64-
fee = Transactions.weight2fee(feerate, buildTx(outPoint, 0.sat, pubKeyScript, dummyWitness).weight())
65-
_ = assert(inputAmount - fee > Scripts.dustLimit(pubKeyScript), s"amount insufficient (fee=$fee)")
66-
unsignedTx = buildTx(outPoint, inputAmount - fee, pubKeyScript, dummyWitness)
67-
nonce = Musig2.generateNonce(sessionId, Right(localFundingPubkey), Seq(localFundingPubkey), None, None)
68-
} yield SpendFromTaprootChannelPrep(fundingTxIndex, localFundingPubkey, inputAmount, unsignedTx, nonce)
69-
}
70-
71-
override def spendFromTaprootChannelAddressPartialSign(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, unsignedTx: Transaction, amount: Satoshi, remoteFundingPubkey: PublicKey, remoteNonce: IndividualNonce): Future[SpendFromTaprootChannelPartialSign] = {
72-
for {
73-
_ <- Future.successful(())
74-
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
75-
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
76-
outputScript = Script.pay2tr(Taproot.musig2Aggregate(localFundingKey.publicKey, remoteFundingPubkey), None)
77-
inputInfo = InputInfo(unsignedTx.txIn(0).outPoint, TxOut(amount, outputScript))
78-
tx = Transactions.SpliceTx(inputInfo, unsignedTx)
79-
localNonce = Musig2.generateNonce(randomBytes32(), Left(localFundingKey), Seq(localFundingKey.publicKey), None, None)
80-
Right(localSig) = tx.partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce = localNonce, publicNonces = Seq(localNonce.publicNonce, remoteNonce))
81-
} yield SpendFromTaprootChannelPartialSign(localFundingKey.publicKey, localSig)
82-
}
83-
84-
override def spendFromTaprootChannelAddress(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, sessionId: ByteVector32, remotePartialSig: PartialSignatureWithNonce, unsignedTx: Transaction): Future[SpendFromChannelResult] = {
85-
for {
86-
_ <- Future.successful(())
87-
outPoint = unsignedTx.txIn.head.outPoint
88-
inputTx <- appKit.wallet.getTransaction(outPoint.txid)
8958
channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath)
9059
localFundingKey = channelKeys.fundingKey(fundingTxIndex)
91-
inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt))
92-
// classify as splice, doesn't really matter
93-
tx = Transactions.SpliceTx(inputInfo, unsignedTx)
94-
localNonce = Musig2.generateNonce(sessionId, Right(localFundingKey.publicKey), Seq(localFundingKey.publicKey), None, None)
95-
Right(localSig) = tx.partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce = localNonce, publicNonces = Seq(localNonce.publicNonce, remotePartialSig.nonce))
96-
Right(signedTx) = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, remotePartialSig, extraUtxos = Map.empty)
60+
signedTx = remoteSig match {
61+
case individualRemoteSig: ChannelSpendSignature.IndividualSignature =>
62+
val localSig = tx.sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty)
63+
tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, individualRemoteSig)
64+
case remotePartialSig: ChannelSpendSignature.PartialSignatureWithNonce =>
65+
val localPrivateNonce = nonces(localNonce_opt.get)
66+
val Right(localSig) = tx.partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce = localPrivateNonce, publicNonces = Seq(localPrivateNonce.publicNonce, remotePartialSig.nonce))
67+
val Right(signedTx) = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, remotePartialSig, extraUtxos = Map.empty)
68+
signedTx
69+
}
9770
} yield SpendFromChannelResult(signedTx)
9871
}
9972
}

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

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import akka.http.scaladsl.server.Route
2020
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2121
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
2222
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
23-
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction, TxId}
23+
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Transaction, TxId}
2424
import fr.acinq.eclair.api.Service
2525
import fr.acinq.eclair.api.directives.EclairDirectives
2626
import fr.acinq.eclair.api.serde.FormParamExtractors._
27-
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw}
27+
import fr.acinq.eclair.blockchain.fee.FeeratePerByte
28+
import fr.acinq.eclair.channel.ChannelSpendSignature
2829
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
29-
import scodec.bits.ByteVector
3030

3131
trait Control {
3232
this: Service with EclairDirectives =>
@@ -67,31 +67,17 @@ trait Control {
6767
val spendFromChannelAddress: Route = postRequest("spendfromchanneladdress") { implicit t =>
6868
formFields("kp", "fi".as[Int], "p".as[PublicKey], "s".as[ByteVector64], "tx") {
6969
(keyPath, fundingTxIndex, remoteFundingPubkey, remoteSig, unsignedTx) =>
70-
complete(eclairApi.spendFromChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, remoteSig, Transaction.read(unsignedTx)))
71-
}
72-
}
73-
74-
val spendFromTaprootChannelAddressPrep: Route = postRequest("spendfromtaprootchanneladdressprep") { implicit t =>
75-
formFields("t".as[ByteVector32], "o".as[Int], "kp", "fi".as[Int], "address", "f".as[FeeratePerByte], "randomSessionId".as[ByteVector32]) {
76-
(txId, outputIndex, keyPath, fundingTxIndex, address, feerate, sessionId) =>
77-
complete(eclairApi.spendFromTaprootChannelAddressPrep(OutPoint(TxId(txId), outputIndex), KeyPath(keyPath), fundingTxIndex, address, feerate.perKw, sessionId))
78-
}
79-
}
80-
81-
val spendFromTaprootChannelAddressPartialSign: Route = postRequest("spendfromtaprootchanneladdresspartialsign") { implicit t =>
82-
formFields("kp", "fi".as[Int], "tx", "amount".as[Satoshi], "remoteFundingPubkey".as[PublicKey], "remoteNonce".as[String]) {
83-
(keyPath, fundingTxIndex, tx, amount, remoteFundingPubkey, remoteNonce) =>
84-
complete(eclairApi.spendFromTaprootChannelAddressPartialSign(KeyPath(keyPath), fundingTxIndex, Transaction.read(tx), amount, remoteFundingPubkey, IndividualNonce(ByteVector.fromValidHex(remoteNonce))))
70+
complete(eclairApi.spendFromChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, localNonce_opt = None, ChannelSpendSignature.IndividualSignature(remoteSig), Transaction.read(unsignedTx)))
8571
}
8672
}
8773

8874
val spendFromTaprootChannelAddress: Route = postRequest("spendfromtaprootchanneladdress") { implicit t =>
89-
formFields("kp", "fi".as[Int], "p".as[PublicKey], "sessionId".as[ByteVector32], "remoteSig".as[ByteVector32], "remoteNonce".as[String], "tx".as[String]) {
90-
(keyPath, fundingTxIndex, remoteFundingPubkey, sessionId, remoteSig, remoteNonce, unsignedTx) =>
91-
complete(eclairApi.spendFromTaprootChannelAddress(KeyPath(keyPath), fundingTxIndex, remoteFundingPubkey, sessionId, PartialSignatureWithNonce(remoteSig, IndividualNonce(ByteVector.fromValidHex(remoteNonce))), Transaction.read(unsignedTx)))
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)))
9278
}
9379
}
9480

95-
val controlRoutes: Route = enableFromFutureHtlc ~ resetBalance ~ forceCloseResetFundingIndex ~ manualWatchFundingSpent ~ spendFromChannelAddressPrep ~ spendFromChannelAddress ~ spendFromTaprootChannelAddressPrep ~ spendFromTaprootChannelAddressPartialSign ~ spendFromTaprootChannelAddress
81+
val controlRoutes: Route = enableFromFutureHtlc ~ resetBalance ~ forceCloseResetFundingIndex ~ manualWatchFundingSpent ~ spendFromChannelAddressPrep ~ spendFromChannelAddress ~ spendFromTaprootChannelAddress
9682

9783
}

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)