From 44dbaf390919c7f063122787f56ed03041e0b3ed Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 16 Aug 2021 18:15:03 +0200 Subject: [PATCH] Add bip32 derivation path to the funding PSBT that will be signed by bitcoin core --- .../eclair/blockchain/EclairWallet.scala | 5 ++- .../bitcoind/BitcoinCoreWallet.scala | 35 ++++++++++++++++++- .../bitcoind/rpc/ExtendedBitcoinClient.scala | 2 +- .../fr/acinq/eclair/channel/Channel.scala | 3 +- .../acinq/eclair/blockchain/TestWallet.scala | 6 +++- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala index 47a36bc43b..d1a09b98e1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.blockchain import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey import fr.acinq.bitcoin.{Psbt, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits.ByteVector @@ -36,7 +37,9 @@ trait EclairWallet { def getReceiveAddress(label: String = ""): Future[String] def getReceivePubkey(receiveAddress: Option[String] = None): Future[PublicKey] - + + def makeFundingTx(localFundingKey: ExtendedPublicKey, remoteFundingKey: PublicKey, amount: Satoshi, feeRatePerKw: FeeratePerKw): Future[MakeFundingTxResponse] + def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw): Future[MakeFundingTxResponse] /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index 1111f01d43..5109de64da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -17,13 +17,14 @@ package fr.acinq.eclair.blockchain.bitcoind import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey import fr.acinq.bitcoin._ import fr.acinq.eclair.addressToPublicKeyScript import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient.{FundPsbtOptions, FundPsbtResponse, FundTransactionOptions, FundTransactionResponse, ProcessPsbtResponse, SignTransactionResponse, toSatoshi} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw} -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.{Scripts, Transactions} import grizzled.slf4j.Logging import org.json4s.JsonAST._ import scodec.bits.ByteVector @@ -180,6 +181,38 @@ class BitcoinCoreWallet(chainHash: ByteVector32, rpcClient: BitcoinJsonRPCClient JString(rawKey) <- rpcClient.invoke("getaddressinfo", address).map(_ \ "pubkey") } yield PublicKey(ByteVector.fromValidHex(rawKey)) + override def makeFundingTx(localFundingKey: ExtendedPublicKey, remoteFundingKey: PublicKey, amount: Satoshi, feeRatePerKw: FeeratePerKw): Future[MakeFundingTxResponse] = { + val hrp = chainHash match { + case Block.RegtestGenesisBlock.hash => "bcrt" + case Block.TestnetGenesisBlock.hash => "tb" + case Block.LivenetGenesisBlock.hash => "bc" + case _ => return Future.failed(new IllegalArgumentException(s"invalid chain hash ${chainHash}")) + } + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingKey.publicKey, remoteFundingKey))) + val fundingAddress = Script.parse(fundingPubkeyScript) match { + case OP_0 :: OP_PUSHDATA(data, _) :: Nil if data.size == 20 || data.size == 32 => Bech32.encodeWitnessAddress(hrp, 0, data) + case _ => return Future.failed(new IllegalArgumentException("invalid pubkey script")) + } + + for { + // we ask bitcoin core to create and fund the funding tx + actualFeeRate <- getMinFeerate(feeRatePerKw) + FundPsbtResponse(psbt, fee, Some(changePos)) <- bitcoinClient.fundPsbt(Map(fundingAddress -> amount), 0, FundPsbtOptions(FeeratePerKw(actualFeeRate), lockUtxos = true)) + + output = psbt.outputs(1 - changePos).copy(derivationPaths = Map(localFundingKey.publicKey -> Psbt.KeyPathWithMaster(localFundingKey.parent, localFundingKey.path))) + psbt1 = psbt.copy(outputs = psbt.outputs.updated(1 - changePos, output)) + + // now let's sign the funding tx + ProcessPsbtResponse(signedPsbt, true) <- signPsbtOrUnlock(psbt1) + Success(fundingTx) = signedPsbt.extract() + // there will probably be a change output, so we need to find which output is ours + outputIndex <- Transactions.findPubKeyScriptIndex(fundingTx, fundingPubkeyScript) match { + case Right(outputIndex) => Future.successful(outputIndex) + case Left(skipped) => Future.failed(new RuntimeException(skipped.toString)) + } + _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=${fee}") + } yield MakeFundingTxResponse(signedPsbt, outputIndex, fee) + } override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feerate: FeeratePerKw): Future[MakeFundingTxResponse] = { val hrp = chainHash match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala index 8cf5fd4f26..9faea461a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala @@ -349,7 +349,7 @@ object ExtendedBitcoinClient { } case class FundPsbtResponse(psbt: Psbt, fee: Satoshi, changePosition: Option[Int]) { - val amountIn: Satoshi = fee + psbt.computeFees().get + val amountIn: Satoshi = psbt.computeFees().get + psbt.global.tx.txOut.map(_.amount).sum } case class PreviousTx(txid: ByteVector32, vout: Long, scriptPubKey: String, redeemScript: String, witnessScript: String, amount: BigDecimal) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 7ccf9c03b6..6ee14863aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -413,8 +413,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId shutdownScript = remoteShutdownScript) log.debug("remote params: {}", remoteParams) val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) - wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) + wallet.makeFundingTx(localFundingPubkey, remoteParams.fundingPubKey, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala index e5cc64c6b5..18fcf95b8d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala @@ -17,9 +17,10 @@ package fr.acinq.eclair.blockchain import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Crypto, OutPoint, Psbt, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{ByteVector32, Crypto, DeterministicWallet, OutPoint, Psbt, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.randomKey +import fr.acinq.eclair.transactions.Scripts import scodec.bits._ import scala.concurrent.Future @@ -41,6 +42,9 @@ class TestWallet extends EclairWallet { override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw): Future[MakeFundingTxResponse] = Future.successful(TestWallet.makeDummyFundingTx(pubkeyScript, amount)) + override def makeFundingTx(localFundingKey: DeterministicWallet.ExtendedPublicKey, remoteFundingKey: PublicKey, amount: Satoshi, feeRatePerKw: FeeratePerKw): Future[MakeFundingTxResponse] = + makeFundingTx(Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingKey.publicKey, remoteFundingKey))), amount, feeRatePerKw) + override def commit(tx: Transaction): Future[Boolean] = Future.successful(true) override def rollback(tx: Transaction): Future[Boolean] = {