1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 01:43:22 +01:00

Update Bitcoin Core to v26.1 (#2851)

Bitcoin Core 26.1 contains ancestor-aware funding: it will automatically
fetch unconfirmed ancestors during funding and adapt the fee to apply
the target feerate to the whole unconfirmed package.

We had custom code to implement this entirely in eclair, which we can
now remove.
This commit is contained in:
Bastien Teinturier 2024-04-23 10:28:25 +02:00 committed by GitHub
parent c6e586ab89
commit 35295af73c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 63 additions and 64 deletions

View File

@ -62,7 +62,7 @@ This means that instead of re-implementing them, Eclair benefits from the verifi
* Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node.
* You must configure your Bitcoin node to use `bech32` or `bech32m` (segwit) addresses. If your wallet has "non-segwit UTXOs" (outputs that are neither `p2sh-segwit`, `bech32` or `bech32m`), you must send them to a `bech32` or `bech32m` address before running Eclair.
* Eclair requires Bitcoin Core 24.1 or higher. If you are upgrading an existing wallet, you may need to create a new address and send all your funds to that address.
* Eclair requires Bitcoin Core 26.1 or higher. If you are upgrading an existing wallet, you may need to create a new address and send all your funds to that address.
Run bitcoind with the following minimal `bitcoin.conf`:

View File

@ -4,7 +4,10 @@
## Major changes
<insert changes>
### Update minimal version of Bitcoin Core
With this release, eclair requires using Bitcoin Core 26.1.
Newer versions of Bitcoin Core may be used, but haven't been extensively tested.
### API changes

View File

@ -88,9 +88,9 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-24.1/bitcoin-24.1-x86_64-linux-gnu.tar.gz</bitcoind.url>
<bitcoind.md5>35a2faf826a9d866aa6821832e39231e</bitcoind.md5>
<bitcoind.sha1>a006ef05514b95cf4d78b5ec844e6cf78fabc196</bitcoind.sha1>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-26.1/bitcoin-26.1-x86_64-linux-gnu.tar.gz</bitcoind.url>
<bitcoind.md5>260a8942ca91b3b2da5a3a68e23d9f55</bitcoind.md5>
<bitcoind.sha1>4862009248449fdf41a9a6dcc8b4833c43527c10</bitcoind.sha1>
</properties>
</profile>
<profile>
@ -101,9 +101,9 @@
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-24.1/bitcoin-24.1-x86_64-apple-darwin.tar.gz</bitcoind.url>
<bitcoind.md5>eba2d59fc9b81c0f6a9ccf77639b8b57</bitcoind.md5>
<bitcoind.sha1>b75f30dfa9095c733a9402e243e18174de4522d6</bitcoind.sha1>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-26.1/bitcoin-26.1-x86_64-apple-darwin.tar.gz</bitcoind.url>
<bitcoind.md5>61e272c43dcc8dcb0296d9bfec16acb6</bitcoind.md5>
<bitcoind.sha1>4aa320f6128e378c88462d7bfecb5fe412208b2e</bitcoind.sha1>
</properties>
</profile>
<profile>
@ -114,9 +114,9 @@
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-24.1/bitcoin-24.1-win64.zip</bitcoind.url>
<bitcoind.md5>3a6cff40522e392f4ab1d8aef1274a9d</bitcoind.md5>
<bitcoind.sha1>588a60fe5d0b4d8fd09fae6bf366c3f0fc9336b8</bitcoind.sha1>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-26.1/bitcoin-26.1-win64.zip</bitcoind.url>
<bitcoind.md5>5bdbc41e2ce04b30739e480e17c6e022</bitcoind.md5>
<bitcoind.sha1>5f797638eca9af85edb135723f6a10941c171478</bitcoind.sha1>
</properties>
</profile>
</profiles>

View File

@ -23,7 +23,7 @@ import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy, typed}
import akka.pattern.after
import akka.util.Timeout
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, BlockId, ByteVector32, Satoshi}
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, BlockId, ByteVector32, Satoshi, Script, addressToPublicKeyScript}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener}
import fr.acinq.eclair.blockchain._
@ -187,8 +187,11 @@ class Setup(val datadir: File,
await(getBitcoinStatus(bitcoinClient), 30 seconds, "bitcoind did not respond after 30 seconds")
}
logger.info(s"bitcoind version=${bitcoinStatus.version}")
assert(bitcoinStatus.version >= 240100, "Eclair requires Bitcoin Core 24.1 or higher")
assert(bitcoinStatus.unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Your wallet contains non-segwit UTXOs. You must send those UTXOs to a bech32 address to use Eclair (check out our README for more details).")
assert(bitcoinStatus.version >= 260100, "Eclair requires Bitcoin Core 26.1 or higher")
bitcoinStatus.unspentAddresses.foreach { address =>
val isSegwit = addressToPublicKeyScript(bitcoinStatus.chainHash, address).map(script => Script.isNativeWitnessScript(script)).getOrElse(false)
assert(isSegwit, s"Your wallet contains non-segwit UTXOs (e.g. address=$address). You must send those UTXOs to a segwit address to use Eclair (check out our README for more details).")
}
if (bitcoinStatus.chainHash != Block.RegtestGenesisBlock.hash) {
assert(!bitcoinStatus.initialBlockDownload, s"bitcoind should be synchronized (initialblockdownload=${bitcoinStatus.initialBlockDownload})")
assert(bitcoinStatus.verificationProgress > 0.999, s"bitcoind should be synchronized (progress=${bitcoinStatus.verificationProgress})")

View File

@ -235,7 +235,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag
//------------------------- FUNDING -------------------------//
/**
* @param feeBudget max allowed fee, if the transaction returned by bitcoin core has a higher fee a funding error is returned.
* @param feeBudget_opt max allowed fee, if the transaction returned by bitcoin core has a higher fee a funding error is returned.
*/
def fundTransaction(tx: Transaction, options: FundTransactionOptions, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
rpcClient.invoke("fundrawtransaction", tx.toString(), options).flatMap(json => {

View File

@ -427,34 +427,26 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
}
private def addInputs(anchorTx: ClaimLocalAnchorWithWitnessData, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ClaimLocalAnchorWithWitnessData, Satoshi)] = {
import fr.acinq.bitcoin.scalacompat.KotlinUtils
val dustLimit = commitment.localParams.dustLimit
val commitTxWeight = commitWeight(commitment)
// NB: fundrawtransaction requires at least one output, and may add at most one additional change output.
// Since the purpose of this transaction is just to do a CPFP, the resulting tx should have a single change output
// (note that bitcoind doesn't let us publish a transaction with no outputs). To work around these limitations, we
// start with a dummy output and later merge that dummy output with the optional change output added by bitcoind.
val dummyChangeOutput = TxOut(dustLimit, Script.pay2wpkh(PlaceHolderPubKey))
val txNotFunded = anchorTx.txInfo.tx.copy(txOut = dummyChangeOutput :: Nil)
// The anchor transaction is paying for the weight of the commitment transaction.
// We remove the weight of the artificially added change output, because we will remove that output after funding.
val anchorWeight = Seq(InputWeight(anchorTx.txInfo.input.outPoint, anchorInputWeight + commitTxWeight - KotlinUtils.scala2kmp(dummyChangeOutput).weight()))
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 =>
// 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)
// The commitment transaction was already paying some fees that we're paying again in the anchor transaction since
// we included the commit weight, so we need to increase our change output to avoid overshooting the feerate.
val commitFee = commitment.localCommit.commitTxAndRemoteSig.commitTx.fee
// We merge our dummy change output with the one added by Bitcoin Core, if any.
fundTxResponse.changePosition match {
case Some(changePos) =>
val changeOutput = fundTxResponse.tx.txOut(changePos).copy(amount = fundTxResponse.tx.txOut.map(_.amount).sum + commitFee)
val changeOutput = fundTxResponse.tx.txOut(changePos).copy(amount = fundTxResponse.tx.txOut.map(_.amount).sum)
val txSingleOutput = fundTxResponse.tx.copy(txIn = txIn, txOut = Seq(changeOutput))
Future.successful(anchorTx.updateTx(txSingleOutput), fundTxResponse.amountIn)
case None =>
bitcoinClient.getP2wpkhPubkeyHashForChange().map(pubkeyHash => {
// replace PlaceHolderPubKey with a real wallet key
val txSingleOutput = fundTxResponse.tx.copy(txIn = txIn, txOut = Seq(TxOut(dustLimit + commitFee, Script.pay2wpkh(pubkeyHash))))
// We must have a change output, otherwise the transaction is invalid: we replace the PlaceHolderPubKey with a real wallet key.
val txSingleOutput = fundTxResponse.tx.copy(txIn = txIn, txOut = Seq(TxOut(dustLimit, Script.pay2wpkh(pubkeyHash))))
(anchorTx.updateTx(txSingleOutput), fundTxResponse.amountIn)
})
}

View File

@ -50,12 +50,10 @@ package object eclair {
}
def serializationResult(attempt: Attempt[BitVector]): ByteVector = attempt match {
case Attempt.Successful(bin) => bin.toByteVector
case Attempt.Successful(bin) => bin.bytes
case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause")
}
def isPay2PubkeyHash(address: String): Boolean = address.startsWith("1") || address.startsWith("m") || address.startsWith("n")
/**
* Tests whether the binary data is composed solely of printable ASCII characters (see BOLT 1)
*

View File

@ -253,12 +253,12 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
assert(amountIn2 == fundedTx2.amountIn)
// We sign our external input.
val externalSig = Transaction.signInput(fundedTx2.tx, 0, inputScript1, SigHash.SIGHASH_ALL, 250_000 sat, SigVersion.SIGVERSION_WITNESS_V0, alicePriv)
// And let bitcoind sign the wallet input.
walletExternalFunds.signPsbt(new Psbt(fundedTx2.tx), fundedTx2.tx.txIn.indices, Nil).pipeTo(sender.ref)
val psbt = sender.expectMsgType[ProcessPsbtResponse].psbt
val signedTx: Transaction = psbt.updateWitnessInput(outpoint1, txOut1, null, null, null, java.util.Map.of()).getRight
val psbt = new Psbt(fundedTx2.tx)
.updateWitnessInput(outpoint1, txOut1, null, null, null, java.util.Map.of()).getRight
.finalizeWitnessInput(0, Script.witnessMultiSigMofN(Seq(alicePriv, bobPriv).map(_.publicKey), Seq(externalSig))).getRight
.extract().getRight
// And let bitcoind sign the wallet input.
walletExternalFunds.signPsbt(psbt, fundedTx2.tx.txIn.indices, Nil).pipeTo(sender.ref)
val signedTx: Transaction = sender.expectMsgType[ProcessPsbtResponse].psbt.extract().getRight
walletExternalFunds.publishTransaction(signedTx).pipeTo(sender.ref)
sender.expectMsg(signedTx.txid)
@ -281,15 +281,14 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = Seq(InputWeight(externalOutpoint, externalInputWeight)), changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref)
val fundedTx = sender.expectMsgType[FundTransactionResponse]
assert(fundedTx.tx.txIn.length >= 2)
// bitcoind signs the wallet input.
walletExternalFunds.signPsbt(new Psbt(fundedTx.tx), fundedTx.tx.txIn.indices, Nil).pipeTo(sender.ref)
val psbt = sender.expectMsgType[ProcessPsbtResponse].psbt
// We sign our external input.
val externalSig = Transaction.signInput(fundedTx.tx, 0, inputScript2, SigHash.SIGHASH_ALL, 300_000 sat, SigVersion.SIGVERSION_WITNESS_V0, alicePriv)
val signedTx: Transaction = psbt.updateWitnessInput(externalOutpoint, tx2.txOut(0), null, null, null, java.util.Map.of()).getRight
val psbt = new Psbt(fundedTx.tx)
.updateWitnessInput(externalOutpoint, tx2.txOut(0), null, null, null, java.util.Map.of()).getRight
.finalizeWitnessInput(0, Script.witnessMultiSigMofN(Seq(alicePriv, carolPriv).map(_.publicKey), Seq(externalSig))).getRight
.extract().getRight
// bitcoind signs the wallet input.
walletExternalFunds.signPsbt(psbt, fundedTx.tx.txIn.indices, Nil).pipeTo(sender.ref)
val signedTx: Transaction = sender.expectMsgType[ProcessPsbtResponse].psbt.extract().getRight
walletExternalFunds.publishTransaction(signedTx).pipeTo(sender.ref)
sender.expectMsg(signedTx.txid)
@ -298,10 +297,9 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
val mempoolTx = sender.expectMsgType[MempoolTx]
assert(mempoolTx.fees == fundedTx.fee)
assert(mempoolTx.fees < mempoolTx.ancestorFees)
// TODO: uncomment the lines below once bitcoind takes into account unconfirmed ancestors in feerate estimation (expected in Bitcoin Core 25).
// val actualFee = mempoolTx.ancestorFees
// val expectedFee = Transactions.weight2fee(targetFeerate, signedTx.weight() + tx2.weight())
// assert(expectedFee * 0.9 <= actualFee && actualFee <= expectedFee * 1.1, s"expected fee=$expectedFee actual fee=$actualFee")
val actualFee = mempoolTx.ancestorFees
val expectedFee = Transactions.weight2fee(targetFeerate, signedTx.weight() + tx2.weight())
assert(expectedFee * 0.9 <= actualFee && actualFee <= expectedFee * 1.1, s"expected fee=$expectedFee actual fee=$actualFee")
signedTx
}
@ -319,15 +317,15 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
assert(fundedTx.tx.txIn.length >= 2)
assert(fundedTx.tx.txOut.length == 2)
// bitcoind signs the wallet input.
walletExternalFunds.signPsbt(new Psbt(fundedTx.tx), fundedTx.tx.txIn.indices, Nil).pipeTo(sender.ref)
val psbt = sender.expectMsgType[ProcessPsbtResponse].psbt
// We sign our external input.
val externalSig = Transaction.signInput(fundedTx.tx, 0, inputScript2, SigHash.SIGHASH_ALL, 300_000 sat, SigVersion.SIGVERSION_WITNESS_V0, alicePriv)
val signedTx: Transaction = psbt.updateWitnessInput(OutPoint(tx2, 0), tx2.txOut(0), null, null, null, java.util.Map.of()).getRight
val psbt = new Psbt(fundedTx.tx)
.updateWitnessInput(OutPoint(tx2, 0), tx2.txOut(0), null, null, null, java.util.Map.of()).getRight
.finalizeWitnessInput(0, Script.witnessMultiSigMofN(Seq(alicePriv, carolPriv).map(_.publicKey), Seq(externalSig))).getRight
.extract().getRight
// bitcoind signs the wallet input.
walletExternalFunds.signPsbt(psbt, fundedTx.tx.txIn.indices, Nil).pipeTo(sender.ref)
val signedTx: Transaction = sender.expectMsgType[ProcessPsbtResponse].psbt.extract().getRight
walletExternalFunds.publishTransaction(signedTx).pipeTo(sender.ref)
sender.expectMsg(signedTx.txid)
// We have replaced the previous transaction.
@ -338,10 +336,9 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
val mempoolTx = sender.expectMsgType[MempoolTx]
assert(mempoolTx.fees == fundedTx.fee)
assert(mempoolTx.fees < mempoolTx.ancestorFees)
// TODO: uncomment the lines below once bitcoind takes into account unconfirmed ancestors in feerate estimation (expected in Bitcoin Core 25).
// val actualFee = mempoolTx.ancestorFees
// val expectedFee = Transactions.weight2fee(targetFeerate, signedTx.weight() + tx2.weight())
// assert(expectedFee * 0.9 <= actualFee && actualFee <= expectedFee * 1.1, s"expected fee=$expectedFee actual fee=$actualFee")
val actualFee = mempoolTx.ancestorFees
val expectedFee = Transactions.weight2fee(targetFeerate, signedTx.weight() + tx2.weight())
assert(expectedFee * 0.9 <= actualFee && actualFee <= expectedFee * 1.1, s"expected fee=$expectedFee actual fee=$actualFee")
}
}

View File

@ -62,7 +62,7 @@ trait BitcoindService extends Logging {
val PATH_BITCOIND = sys.env.get("BITCOIND_DIR") match {
case Some(customBitcoinDir) => new File(customBitcoinDir, "bitcoind")
case None => new File(TestUtils.BUILD_DIRECTORY, "bitcoin-24.1/bin/bitcoind")
case None => new File(TestUtils.BUILD_DIRECTORY, "bitcoin-26.1/bin/bitcoind")
}
logger.info(s"using bitcoind: $PATH_BITCOIND")
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")

View File

@ -34,7 +34,7 @@ import fr.acinq.eclair.blockchain.{OnChainWallet, SingleKeyOnChainWallet}
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
import fr.acinq.eclair.io.OpenChannelInterceptor.makeChannelParams
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import fr.acinq.eclair.transactions.Transactions.InputInfo
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, UInt64, randomBytes32, randomKey}
@ -1469,7 +1469,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
val successA3 = alice2bob.expectMsgType[Succeeded]
val successB3 = bob2alice.expectMsgType[Succeeded]
val (spliceTxA2, _, _, _) = fixtureParams.exchangeSigsBobFirst(fundingParamsB2, successA3, successB3)
assert(fundingParamsB2.targetFeerate * 0.9 <= spliceTxA2.feerate && spliceTxA2.feerate <= fundingParamsB2.targetFeerate * 1.25)
// The funding transaction isn't confirmed: the splice transaction CPFPs it to the latest feerate.
val packageFeerate = Transactions.fee2rate(txA1.tx.fees + spliceTxA2.tx.fees, txA1.signedTx.weight() + spliceTxA2.signedTx.weight())
assert(fundingParamsB2.targetFeerate * 0.9 <= packageFeerate && packageFeerate <= fundingParamsB2.targetFeerate * 1.25)
assert(spliceTxA1.signedTx.txIn.map(_.outPoint).toSet == spliceTxA2.signedTx.txIn.map(_.outPoint).toSet)
(spliceOutputsA ++ spliceOutputsB).foreach(txOut => assert(spliceTxA2.signedTx.txOut.contains(txOut)))
assert(spliceTxA1.txId != spliceTxA2.txId)
@ -1561,8 +1563,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
probe.expectMsg(spliceTxA1.txId)
// Alice wants to make a large increase to the feerate of the splice transaction, which requires additional inputs.
val fundingParamsA2 = fundingParamsA1.copy(targetFeerate = FeeratePerKw(10_000 sat))
val fundingParamsB2 = fundingParamsB1.copy(targetFeerate = FeeratePerKw(10_000 sat))
val fundingParamsA2 = fundingParamsA1.copy(targetFeerate = FeeratePerKw(5_000 sat))
val fundingParamsB2 = fundingParamsB1.copy(targetFeerate = FeeratePerKw(5_000 sat))
val aliceRbf = fixtureParams.spawnTxBuilderSpliceRbfAlice(fundingParamsA2, parentCommitment = commitmentA1, replacedCommitment = commitmentA2, Seq(spliceTxA1), walletA)
val bobRbf = fixtureParams.spawnTxBuilderSpliceRbfBob(fundingParamsB2, parentCommitment = commitmentB1, replacedCommitment = commitmentB2, Seq(spliceTxB1), walletB)
val fwdRbf = TypeCheckedForwarder(aliceRbf, bobRbf, alice2bob, bob2alice)
@ -1600,7 +1602,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
val successA3 = alice2bob.expectMsgType[Succeeded]
val successB3 = bob2alice.expectMsgType[Succeeded]
val (spliceTxA2, _, _, _) = fixtureParams.exchangeSigsBobFirst(fundingParamsB2, successA3, successB3)
assert(fundingParamsB2.targetFeerate * 0.9 <= spliceTxA2.feerate && spliceTxA2.feerate <= fundingParamsB2.targetFeerate * 1.25)
// The funding transaction isn't confirmed: the splice transaction CPFPs it to the latest feerate.
val packageFeerate = Transactions.fee2rate(txA1.tx.fees + spliceTxA2.tx.fees, txA1.signedTx.weight() + spliceTxA2.signedTx.weight())
assert(fundingParamsB2.targetFeerate * 0.9 <= packageFeerate && packageFeerate <= fundingParamsB2.targetFeerate * 1.25)
// Alice and Bob both added a new input to fund the feerate increase.
assert(spliceTxA2.signedTx.txIn.length == spliceTxA1.signedTx.txIn.length + 2)
assert(spliceTxA1.signedTx.txIn.map(_.outPoint).toSet.subsetOf(spliceTxA2.signedTx.txIn.map(_.outPoint).toSet))
@ -1757,7 +1761,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
walletA.getMempoolTx(spliceTxA2.txId).pipeTo(probe.ref)
val mempoolTx = probe.expectMsgType[MempoolTx]
assert(mempoolTx.fees == spliceTxA2.tx.fees)
assert(fundingParamsB2.targetFeerate * 0.9 <= spliceTxA2.feerate && spliceTxA2.feerate <= fundingParamsB2.targetFeerate * 1.25)
// The funding transaction isn't confirmed: the splice transaction CPFPs it to the latest feerate.
val packageFeerate = Transactions.fee2rate(txA1.tx.fees + spliceTxA2.tx.fees, txA1.signedTx.weight() + spliceTxA2.signedTx.weight())
assert(fundingParamsB2.targetFeerate * 0.9 <= packageFeerate && packageFeerate <= fundingParamsB2.targetFeerate * 1.25)
assert(spliceTxA1.signedTx.txIn.map(_.outPoint).toSet.subsetOf(spliceTxA2.signedTx.txIn.map(_.outPoint).toSet))
assert(spliceTxA1.txId != spliceTxA2.txId)
assert(spliceTxA1.tx.fees < spliceTxA2.tx.fees)