Skip to content

Commit

Permalink
Update single funded channel to select inputs before sending open
Browse files Browse the repository at this point in the history
 - makeFundingTx now just selects inputs and change outputs
 - (new) signFundingTx replaces the funding output script and signs
  • Loading branch information
remyers committed Aug 5, 2024
1 parent e784ee3 commit 7b8b2ae
Show file tree
Hide file tree
Showing 14 changed files with 332 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,13 @@ trait OnChainChannelFunder {
*/
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[TxId]

/** Create a fully signed channel funding transaction with the provided pubkeyScript. */
/** Create an unsigned channel funding transaction with the provided dummy pubkeyScript. */
def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse]

/**
/** Sign a funding transaction with an updated pubkeyScript. */
def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse]

/**
* Committing *must* include publishing the transaction on the network.
*
* We need to be very careful here, we don't want to consider a commit 'failed' if we are not absolutely sure that the
Expand Down Expand Up @@ -155,6 +158,8 @@ object OnChainWallet {

final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi)

final case class SignFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi)

final case class FundTransactionResponse(tx: Transaction, fee: Satoshi, changePosition: Option[Int]) {
val amountIn: Satoshi = fee + tx.txOut.map(_.amount).sum
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ import fr.acinq.bitcoin.scalacompat._
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, ProcessPsbtResponse}
import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse, SignFundingTxResponse}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult}
import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw}
import fr.acinq.eclair.crypto.keymanager.OnChainKeyManager
import fr.acinq.eclair.json.SatoshiSerializer
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.protocol.ChannelAnnouncement
import fr.acinq.eclair.{BlockHeight, TimestampSecond, TxCoordinates}
Expand Down Expand Up @@ -310,7 +309,28 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag

def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw, feeBudget_opt: Option[Satoshi] = None, maxExcess_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = {

def verifyAndSign(tx: Transaction, fees: Satoshi, requestedFeeRate: FeeratePerKw): Future[MakeFundingTxResponse] = {
val partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)

for {
// TODO: we should check that mempoolMinFee is not dangerously high
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
FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate, add_excess_to_recipient_position = None, max_excess = maxExcess_opt), feeBudget_opt = feeBudget_opt)
fundingOutputIndex = Transactions.findPubKeyScriptIndex(tx, pubkeyScript) match {
case Left(_) => return Future.failed(new RuntimeException("cannot find expected funding output: bitcoin core may be malicious"))
case Right(outputIndex) => outputIndex
}
makeFundingTxResponse = MakeFundingTxResponse(tx, fundingOutputIndex, fee)
} yield makeFundingTxResponse
}

def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = {

def verifyAndSign(tx: Transaction, fees: Satoshi, requestedFeeRate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = {
import KotlinUtils._

val fundingOutputIndex = Transactions.findPubKeyScriptIndex(tx, pubkeyScript) match {
Expand All @@ -331,22 +351,16 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag
maxFeerate = requestedFeeRate * 1.5
_ = require(actualFeerate < maxFeerate, s"actual feerate $actualFeerate is more than 50% above requested feerate $targetFeerate")
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$fundingOutputIndex fee=$fees")
} yield MakeFundingTxResponse(fundingTx, fundingOutputIndex, fees)
} yield SignFundingTxResponse(fundingTx, fundingOutputIndex, fees)
}

val partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
val tx1 = tx.copy(txOut = tx.txOut.updated(outputIndex, tx.txOut(outputIndex).copy(publicKeyScript = pubkeyScript)))

for {
// TODO: we should check that mempoolMinFee is not dangerously high
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
FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate, add_excess_to_recipient_position = None, max_excess = maxExcess_opt), feeBudget_opt = feeBudget_opt)
lockedUtxos = tx.txIn.map(_.outPoint)
signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx, fee, feerate))
lockedUtxos = tx1.txIn.map(_.outPoint)
signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx1, fee, feerate))
} yield signedTx
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel
import akka.actor.{ActorRef, PossiblyHarmful, typed}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut}
import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
Expand Down Expand Up @@ -53,6 +54,7 @@ sealed trait ChannelState
case object WAIT_FOR_INIT_INTERNAL extends ChannelState
// Single-funder channel opening:
case object WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL extends ChannelState
case object WAIT_FOR_FUNDING_SIGNED_INTERNAL extends ChannelState
case object WAIT_FOR_OPEN_CHANNEL extends ChannelState
case object WAIT_FOR_ACCEPT_CHANNEL extends ChannelState
case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState
Expand Down Expand Up @@ -522,17 +524,11 @@ sealed trait ChannelDataWithCommitments extends PersistentChannelData {
final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData {
val channelId: ByteVector32 = initFundee.temporaryChannelId
}
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel) extends TransientChannelData {
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel, fundingTxResponse: MakeFundingTxResponse) extends TransientChannelData {
val channelId: ByteVector32 = initFunder.temporaryChannelId
}
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(params: ChannelParams,
fundingAmount: Satoshi,
pushAmount: MilliSatoshi,
commitTxFeerate: FeeratePerKw,
remoteFundingPubKey: PublicKey,
remoteFirstPerCommitmentPoint: PublicKey,
replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData {
val channelId: ByteVector32 = params.channelId
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(input: INPUT_INIT_CHANNEL_INITIATOR) extends TransientChannelData {
val channelId: ByteVector32 = input.temporaryChannelId
}
final case class DATA_WAIT_FOR_FUNDING_CREATED(params: ChannelParams,
fundingAmount: Satoshi,
Expand Down Expand Up @@ -567,6 +563,11 @@ final case class DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL
final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenDualFundedChannel) extends TransientChannelData {
val channelId: ByteVector32 = lastSent.temporaryChannelId
}
final case class DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL(lastFundingTx: Transaction, params: ChannelParams, fundingAmount: Satoshi,
pushAmount: MilliSatoshi, commitTxFeerate: FeeratePerKw, remoteFundingPubKey: PublicKey,
remoteFirstPerCommitmentPoint: PublicKey, replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData {
val channelId: ByteVector32 = params.channelId
}
final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32,
channelParams: ChannelParams,
secondRemotePerCommitmentPoint: PublicKey,
Expand Down
Loading

0 comments on commit 7b8b2ae

Please sign in to comment.