mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-23 14:40:34 +01:00
Use confirmed inputs for anchor transactions
In order to use opportunistic package relay (with 1-parent-1-child) we must use confirmed inputs when funding our anchor transactions. This will also be a requirement when using v3 transactions. We also take this opportunity to honor the `require_confirmed_inputs` parameter set by our peer during `interactive-tx`.
This commit is contained in:
parent
3dbe75f8c2
commit
1735a9fd1a
6 changed files with 60 additions and 12 deletions
|
@ -37,7 +37,7 @@ trait OnChainChannelFunder {
|
|||
* Fund the provided transaction by adding inputs (and a change output if necessary).
|
||||
* Callers must verify that the resulting transaction isn't sending funds to unexpected addresses (malicious bitcoin node).
|
||||
*/
|
||||
def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse]
|
||||
def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi], minConfirmations_opt: Option[Int])(implicit ec: ExecutionContext): Future[FundTransactionResponse]
|
||||
|
||||
/**
|
||||
* Sign a PSBT. Result may be partially signed: only inputs known to our bitcoin wallet will be signed. *
|
||||
|
|
|
@ -26,7 +26,6 @@ import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFu
|
|||
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}
|
||||
|
@ -262,8 +261,8 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag
|
|||
})
|
||||
}
|
||||
|
||||
def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
|
||||
fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, inputWeights = externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq), feeBudget_opt = feeBudget_opt)
|
||||
def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi] = None, minConfirmations_opt: Option[Int] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
|
||||
fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, inputWeights = externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq, minConfirmations = minConfirmations_opt), feeBudget_opt = feeBudget_opt)
|
||||
}
|
||||
|
||||
private def processPsbt(psbt: Psbt, sign: Boolean = true, sighashType: Option[Int] = None)(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = {
|
||||
|
@ -704,10 +703,10 @@ object BitcoinCoreClient {
|
|||
def apply(outPoint: OutPoint, weight: Long): InputWeight = InputWeight(outPoint.txid.value.toHex, outPoint.index, weight)
|
||||
}
|
||||
|
||||
case class FundTransactionOptions(feeRate: BigDecimal, replaceable: Boolean, lockUnspents: Boolean, changePosition: Option[Int], input_weights: Option[Seq[InputWeight]])
|
||||
case class FundTransactionOptions(feeRate: BigDecimal, replaceable: Boolean, lockUnspents: Boolean, changePosition: Option[Int], minconf: Option[Int], input_weights: Option[Seq[InputWeight]])
|
||||
|
||||
object FundTransactionOptions {
|
||||
def apply(feerate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, inputWeights: Seq[InputWeight] = Nil): FundTransactionOptions = {
|
||||
def apply(feerate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, minConfirmations: Option[Int] = None, inputWeights: Seq[InputWeight] = Nil): FundTransactionOptions = {
|
||||
FundTransactionOptions(
|
||||
BigDecimal(FeeratePerKB(feerate).toLong).bigDecimal.scaleByPowerOfTen(-8),
|
||||
replaceable,
|
||||
|
@ -721,6 +720,7 @@ object BitcoinCoreClient {
|
|||
// potentially be double-spent.
|
||||
lockUnspents = true,
|
||||
changePosition,
|
||||
minConfirmations,
|
||||
if (inputWeights.isEmpty) None else Some(inputWeights)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -237,7 +237,8 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response
|
|||
case p: SpliceTxRbf => p.feeBudget_opt
|
||||
case _ => None
|
||||
}
|
||||
context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight, feeBudget_opt = feeBudget_opt)) {
|
||||
val minConfirmations_opt = if (fundingParams.requireConfirmedInputs.forLocal) Some(1) else None
|
||||
context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight, feeBudget_opt = feeBudget_opt, minConfirmations_opt)) {
|
||||
case Failure(t) => WalletFailure(t)
|
||||
case Success(result) => FundTransactionResult(result.tx, result.changePosition)
|
||||
}
|
||||
|
|
|
@ -443,7 +443,8 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
|
|||
// start with a dummy output and later merge that dummy output with the optional change output added by bitcoind.
|
||||
val txNotFunded = anchorTx.txInfo.tx.copy(txOut = TxOut(dustLimit, Script.pay2wpkh(PlaceHolderPubKey)) :: Nil)
|
||||
val anchorWeight = Seq(InputWeight(anchorTx.txInfo.input.outPoint, anchorInputWeight))
|
||||
bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = anchorWeight), feeBudget_opt = None).flatMap { fundTxResponse =>
|
||||
// We only use confirmed inputs for anchor transactions to be able to leverage 1-parent-1-child package relay.
|
||||
bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, minConfirmations = Some(1), inputWeights = anchorWeight), feeBudget_opt = None).flatMap { fundTxResponse =>
|
||||
// Bitcoin Core may not preserve the order of inputs, we need to make sure the anchor is the first input.
|
||||
val txIn = anchorTx.txInfo.tx.txIn ++ fundTxResponse.tx.txIn.filterNot(_.outPoint == anchorTx.txInfo.input.outPoint)
|
||||
// We merge our dummy change output with the one added by Bitcoin Core, if any.
|
||||
|
|
|
@ -50,7 +50,7 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache {
|
|||
|
||||
override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey)
|
||||
|
||||
override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
|
||||
override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi], minConfirmations_opt: Option[Int])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
|
||||
funded += (tx.txid -> tx)
|
||||
Future.successful(FundTransactionResponse(tx, 0 sat, None))
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache {
|
|||
|
||||
override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey)
|
||||
|
||||
override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Promise().future // will never be completed
|
||||
override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi], minConfirmations_opt: Option[Int])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Promise().future // will never be completed
|
||||
|
||||
override def signPsbt(psbt: Psbt, ourInputs: Seq[Int], ourOutputs: Seq[Int])(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = Promise().future // will never be completed
|
||||
|
||||
|
@ -152,7 +152,7 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache {
|
|||
|
||||
override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(pubkey)
|
||||
|
||||
override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = synchronized {
|
||||
override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi], minConfirmations_opt: Option[Int])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = synchronized {
|
||||
val currentAmountIn = tx.txIn.flatMap(txIn => inputs.find(_.txid == txIn.outPoint.txid).flatMap(_.txOut.lift(txIn.outPoint.index.toInt))).map(_.amount).sum
|
||||
val amountOut = tx.txOut.map(_.amount).sum
|
||||
// We add a single input to reach the desired feerate.
|
||||
|
@ -225,7 +225,7 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache {
|
|||
override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = {
|
||||
val tx = Transaction(2, Nil, Seq(TxOut(amount, pubkeyScript)), 0)
|
||||
for {
|
||||
fundedTx <- fundTransaction(tx, feeRatePerKw, replaceable = true, feeBudget_opt = feeBudget_opt)
|
||||
fundedTx <- fundTransaction(tx, feeRatePerKw, replaceable = true, feeBudget_opt = feeBudget_opt, minConfirmations_opt = None)
|
||||
signedTx <- signTransaction(fundedTx.tx)
|
||||
} yield MakeFundingTxResponse(signedTx.tx, 0, fundedTx.fee)
|
||||
}
|
||||
|
|
|
@ -194,6 +194,52 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
|
|||
}
|
||||
}
|
||||
|
||||
test("fund transactions with confirmed inputs") {
|
||||
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
|
||||
|
||||
val sender = TestProbe()
|
||||
val miner = makeBitcoinCoreClient()
|
||||
val wallet = new BitcoinCoreClient(createWallet("funding_confirmed_inputs", sender))
|
||||
wallet.getReceiveAddress().pipeTo(sender.ref)
|
||||
val address = sender.expectMsgType[String]
|
||||
val pubkeyScript = Script.write(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).toOption.get)
|
||||
|
||||
// We first receive some confirmed funds.
|
||||
miner.sendToPubkeyScript(pubkeyScript, 150_000 sat, FeeratePerKw(FeeratePerByte(5 sat))).pipeTo(sender.ref)
|
||||
val externalTxId = sender.expectMsgType[TxId]
|
||||
generateBlocks(1)
|
||||
|
||||
// Our utxo has 1 confirmation: we can spend it if we allow this confirmation count.
|
||||
val tx1 = {
|
||||
val txNotFunded = Transaction(2, Nil, Seq(TxOut(125_000 sat, pubkeyScript)), 0)
|
||||
wallet.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(1_000 sat), minConfirmations = Some(2)), feeBudget_opt = None).pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[Failure].cause.getMessage.contains("Insufficient funds"))
|
||||
wallet.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(1_000 sat), minConfirmations = Some(1)), feeBudget_opt = None).pipeTo(sender.ref)
|
||||
val unsignedTx = sender.expectMsgType[FundTransactionResponse].tx
|
||||
wallet.signPsbt(new Psbt(unsignedTx), unsignedTx.txIn.indices, Nil).pipeTo(sender.ref)
|
||||
val signedTx = sender.expectMsgType[ProcessPsbtResponse].finalTx_opt.toOption.get
|
||||
wallet.publishTransaction(signedTx).pipeTo(sender.ref)
|
||||
sender.expectMsg(signedTx.txid)
|
||||
signedTx
|
||||
}
|
||||
assert(tx1.txIn.map(_.outPoint.txid).toSet == Set(externalTxId))
|
||||
|
||||
// We now have an unconfirmed utxo, which we can spend if we allow spending unconfirmed transactions.
|
||||
val tx2 = {
|
||||
val txNotFunded = Transaction(2, Nil, Seq(TxOut(100_000 sat, pubkeyScript)), 0)
|
||||
wallet.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(1_000 sat), minConfirmations = Some(1)), feeBudget_opt = None).pipeTo(sender.ref)
|
||||
assert(sender.expectMsgType[Failure].cause.getMessage.contains("Insufficient funds"))
|
||||
wallet.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(1_000 sat), minConfirmations = None), feeBudget_opt = None).pipeTo(sender.ref)
|
||||
val unsignedTx = sender.expectMsgType[FundTransactionResponse].tx
|
||||
wallet.signPsbt(new Psbt(unsignedTx), unsignedTx.txIn.indices, Nil).pipeTo(sender.ref)
|
||||
val signedTx = sender.expectMsgType[ProcessPsbtResponse].finalTx_opt.toOption.get
|
||||
wallet.publishTransaction(signedTx).pipeTo(sender.ref)
|
||||
sender.expectMsg(signedTx.txid)
|
||||
signedTx
|
||||
}
|
||||
assert(tx2.txIn.map(_.outPoint.txid).toSet == Set(tx1.txid))
|
||||
}
|
||||
|
||||
test("fund transactions with external inputs") {
|
||||
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue