Skip to content

Commit

Permalink
Use psbt to fund transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed Sep 26, 2022
1 parent 1b0ce80 commit b5c8560
Show file tree
Hide file tree
Showing 8 changed files with 370 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

package fr.acinq.eclair.blockchain

import fr.acinq.bitcoin.psbt.{Psbt, UpdateFailure}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction}
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, KotlinUtils, Satoshi, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import scodec.bits.ByteVector

Expand Down Expand Up @@ -45,7 +48,7 @@ trait OnChainChannelFunder {
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[ByteVector32]

/** Create a fully signed channel funding transaction with the provided pubkeyScript. */
def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse]
def makeFundingTx(chainHash: ByteVector32, localFundingKey: ExtendedPublicKey, remoteFundingKey: PublicKey, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse]

/**
* Committing *must* include publishing the transaction on the network.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,27 @@

package fr.acinq.eclair.blockchain.bitcoind.rpc

import fr.acinq.bitcoin.psbt.{KeyPathWithMaster, Psbt}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey
import fr.acinq.bitcoin.scalacompat._
import fr.acinq.bitcoin.{Bech32, Block}
import fr.acinq.bitcoin.{Bech32, Block, SigHash}
import fr.acinq.eclair.ShortChannelId.coordinates
import fr.acinq.eclair.blockchain.OnChainWallet
import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, SignTransactionResponse}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult}
import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw}
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import fr.acinq.eclair.wire.protocol.ChannelAnnouncement
import fr.acinq.eclair.{BlockHeight, TimestampSecond, TxCoordinates}
import grizzled.slf4j.Logging
import org.json4s.Formats
import org.json4s.JsonAST._
import scodec.bits.ByteVector

import java.util.Base64
import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters.ListHasAsScala
import scala.jdk.CollectionConverters.{ListHasAsScala, MapHasAsJava}
import scala.util.{Failure, Success, Try}

/**
Expand Down Expand Up @@ -224,25 +227,97 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, lockUtxos))
}

def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = {
val partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
def fundPsbt(inputs: Seq[FundPsbtInput], outputs: Seq[(String, Satoshi)], locktime: Long, options: FundPsbtOptions)(implicit ec: ExecutionContext): Future[FundPsbtResponse] = {
rpcClient.invoke("walletcreatefundedpsbt", inputs.toArray, outputs.map { case (a, b) => a -> b.toBtc.toBigDecimal }, locktime, options).map(json => {
val JString(base64) = json \ "psbt"
val JInt(changePos) = json \ "changepos"
val JDecimal(fee) = json \ "fee"
val bin = Base64.getDecoder.decode(base64)
val decoded = Psbt.read(bin)
require(decoded.isRight, s"cannot decode psbt from $base64")
val psbt = decoded.getRight
val changePos_opt = if (changePos >= 0) Some(changePos.intValue) else None
FundPsbtResponse(psbt, toSatoshi(fee), changePos_opt)
})
}

def fundPsbt(outputs: Seq[(String, Satoshi)], locktime: Long, options: FundPsbtOptions)(implicit ec: ExecutionContext): Future[FundPsbtResponse] =
fundPsbt(Seq(), outputs, locktime, options)

def processPsbt(psbt: Psbt, sign: Boolean = true, sighashType: Int = SigHash.SIGHASH_ALL)(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = {
val sighashStrings = Map(
SigHash.SIGHASH_ALL -> "ALL",
SigHash.SIGHASH_NONE -> "NONE",
SigHash.SIGHASH_SINGLE -> "SINGLE",
(SigHash.SIGHASH_ALL | SigHash.SIGHASH_ANYONECANPAY) -> "ALL|ANYONECANPAY",
(SigHash.SIGHASH_NONE | SigHash.SIGHASH_ANYONECANPAY) -> "NONE|ANYONECANPAY",
(SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY) -> "SINGLE|ANYONECANPAY")
val sighash = sighashStrings.getOrElse(sighashType, throw new IllegalArgumentException(s"invalid sighash flag ${sighashType}"))
rpcClient.invoke("walletprocesspsbt", Base64.getEncoder.encodeToString(Psbt.write(psbt).toByteArray), sign, sighash).map(json => {
val JString(base64) = json \ "psbt"
val JBool(complete) = json \ "complete"
val decoded = Psbt.read(Base64.getDecoder.decode(base64))
require(decoded.isRight, s"cannot decode psbt from $base64")
ProcessPsbtResponse(decoded.getRight, complete)
})
}

private def signPsbtOrUnlock(psbt: Psbt)(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = {
val f = for {
ProcessPsbtResponse(psbt1, complete) <- processPsbt(psbt)
_ = if (!complete) throw JsonRPCError(Error(0, "cannot sign psbt"))
} yield ProcessPsbtResponse(psbt1, complete)
// if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos
f.recoverWith { case _ =>
unlockOutpoints(psbt.getGlobal.getTx.txIn.asScala.toSeq.map(_.outPoint).map(KotlinUtils.kmp2scala))
.recover { case t: Throwable => // no-op, just add a log in case of failure
logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${psbt.getGlobal.getTx.txid}", t)
t
}
.flatMap(_ => f) // return signTransaction error
.recoverWith { case _ => f } // return signTransaction error
}
}

override def makeFundingTx(chainHash: ByteVector32, localFundingKey: ExtendedPublicKey, remoteFundingKey: PublicKey, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = {
logger.info(s"funding psbt with local_funding_key=$localFundingKey and remote_funding_key=$remoteFundingKey")
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingKey.publicKey, remoteFundingKey)))
val Some(fundingAddress) = computeScriptAddress(chainHash, fundingPubkeyScript)

def updatePsbt(psbt: Psbt, changepos_opt: Option[Int], ourbip32path: Seq[Long]): Psbt = {
val outputIndex = changepos_opt match {
case None => 0
case Some(changePos) => 1 - changePos
}
val updated = psbt.updateWitnessOutput(outputIndex, null, null, Map(
KotlinUtils.scala2kmp(localFundingKey.publicKey) -> new KeyPathWithMaster(localFundingKey.parent, DeterministicWallet.KeyPath(ourbip32path).keyPath),
KotlinUtils.scala2kmp(remoteFundingKey) -> new KeyPathWithMaster(0L, DeterministicWallet.KeyPath("1/2/3/4").keyPath)
).asJava
)
require(updated.isRight, s"cannot update psbt (${updated.getLeft.toString}")
updated.getRight
}

for {
feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(targetFeerate))
// we ask bitcoin core to add inputs to the funding tx, and use the specified change address
fundTxResponse <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate, lockUtxos = true))
// we ask bitcoin core to create and fund the funding tx
feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(feeRatePerKw))
FundPsbtResponse(psbt, fee, changePos_opt) <- fundPsbt(Seq(fundingAddress -> amount), 0, FundPsbtOptions(feerate, lockUtxos = true, changePosition = Some(1)))
ourbip32path = localFundingKey.path.path.drop(2)
_ = logger.info(s"funded psbt = $psbt")
psbt1 = updatePsbt(psbt, changePos_opt, ourbip32path)
// now let's sign the funding tx
SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(fundTxResponse.tx)
ProcessPsbtResponse(signedPsbt, complete) <- signPsbtOrUnlock(psbt1)
_ = logger.info(s"psbt signing complete = $complete")
extracted = signedPsbt.extract()
_ = if (extracted.isLeft) logger.error(s"psbt failure $extracted")
fundingTx = KotlinUtils.kmp2scala(extracted.getRight)
// there will probably be a change output, so we need to find which output is ours
outputIndex <- Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript) match {
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=${fundTxResponse.fee}")
} yield MakeFundingTxResponse(fundingTx, outputIndex, fundTxResponse.fee)
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=${fee}")
} yield MakeFundingTxResponse(fundingTx, outputIndex, fee)
}

def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = publishTransaction(tx).transformWith {
Expand Down Expand Up @@ -477,6 +552,22 @@ object BitcoinCoreClient {
}
}

case class FundPsbtInput(txid: ByteVector32, vout: Int, sequence_opt: Option[Long] = None)

case class FundPsbtOptions(feeRate: BigDecimal, replaceable: Boolean, lockUnspents: Boolean, changePosition: Option[Int], add_inputs: Boolean)

object FundPsbtOptions {
def apply(feerate: FeeratePerKw, replaceable: Boolean = true, lockUtxos: Boolean = false, changePosition: Option[Int] = None, add_inputs: Boolean = true): FundPsbtOptions = {
FundPsbtOptions(BigDecimal(FeeratePerKB(feerate).toLong).bigDecimal.scaleByPowerOfTen(-8), replaceable, lockUtxos, changePosition, add_inputs)
}
}

case class FundPsbtResponse(psbt: Psbt, fee: Satoshi, changePosition: Option[Int]) {
val amountIn: Satoshi = Option(psbt.computeFees()).map(KotlinUtils.kmp2scala).get + psbt.getGlobal.getTx.txOut.asScala.map(_.amount).map(KotlinUtils.kmp2scala).sum
}

case class ProcessPsbtResponse(psbt: Psbt, complete: Boolean)

case class PreviousTx(txid: ByteVector32, vout: Long, scriptPubKey: String, redeemScript: String, witnessScript: String, amount: BigDecimal)

object PreviousTx {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapte
import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, SupervisorStrategy, typed}
import akka.event.Logging.MDC
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, KotlinUtils, Satoshi, SatoshiLong, Transaction}
import fr.acinq.eclair.Logs.LogCategory
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import akka.actor.Status
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.pattern.pipe
import fr.acinq.bitcoin.ScriptFlags
import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script, Transaction}
import fr.acinq.bitcoin.scalacompat.{KotlinUtils, SatoshiLong, Script, Transaction}
import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.channel.Helpers.Funding
Expand Down Expand Up @@ -189,8 +189,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
log.debug("remote params: {}", remoteParams)
log.info("remote will use fundingMinDepth={}", accept.minimumDepth)
val localFundingPubkey = keyManager.fundingPublicKey(init.localParams.fundingKeyPath)
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey)))
wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate).pipeTo(self)
wallet.makeFundingTx(nodeParams.chainHash, localFundingPubkey, remoteParams.fundingPubKey, init.fundingAmount, init.fundingTxFeerate).pipeTo(self)
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(init.temporaryChannelId, init.localParams, remoteParams, init.fundingAmount, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.firstPerCommitmentPoint, init.channelConfig, channelFeatures, open)
}

Expand Down
Loading

0 comments on commit b5c8560

Please sign in to comment.